first
This commit is contained in:
@ -0,0 +1,94 @@
|
||||
<?php
|
||||
namespace ShortPixel\Build;
|
||||
|
||||
class PackageLoader
|
||||
{
|
||||
public $dir;
|
||||
public $composerFile = false;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function setComposerFile($filePath)
|
||||
{
|
||||
$this->composerFile = json_decode(file_get_contents($filePath),1);
|
||||
}
|
||||
|
||||
public function getComposerFile($filePath = false )
|
||||
{
|
||||
if (! $this->composerFile)
|
||||
$this->composerFile = json_decode(file_get_contents($this->dir."/composer.json"), 1);
|
||||
|
||||
return $this->composerFile;
|
||||
}
|
||||
|
||||
public function load($dir)
|
||||
{
|
||||
$this->dir = $dir;
|
||||
$composer = $this->getComposerFile();
|
||||
|
||||
|
||||
if(isset($composer["autoload"]["psr-4"])){
|
||||
$this->loadPSR4($composer['autoload']['psr-4']);
|
||||
}
|
||||
if(isset($composer["autoload"]["psr-0"])){
|
||||
$this->loadPSR0($composer['autoload']['psr-0']);
|
||||
}
|
||||
if(isset($composer["autoload"]["files"])){
|
||||
$this->loadFiles($composer["autoload"]["files"]);
|
||||
}
|
||||
}
|
||||
|
||||
public function loadFiles($files){
|
||||
foreach($files as $file){
|
||||
$fullpath = $this->dir."/".$file;
|
||||
if(file_exists($fullpath)){
|
||||
include_once($fullpath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function loadPSR4($namespaces)
|
||||
{
|
||||
$this->loadPSR($namespaces, true);
|
||||
}
|
||||
|
||||
public function loadPSR0($namespaces)
|
||||
{
|
||||
$this->loadPSR($namespaces, false);
|
||||
}
|
||||
|
||||
public function loadPSR($namespaces, $psr4)
|
||||
{
|
||||
$dir = $this->dir;
|
||||
// Foreach namespace specified in the composer, load the given classes
|
||||
foreach ($namespaces as $namespace => $classpaths) {
|
||||
if (!is_array($classpaths)) {
|
||||
$classpaths = array($classpaths);
|
||||
}
|
||||
spl_autoload_register(function ($classname) use ($namespace, $classpaths, $dir, $psr4) {
|
||||
// Check if the namespace matches the class we are looking for
|
||||
if (preg_match("#^".preg_quote($namespace)."#", $classname)) {
|
||||
// Remove the namespace from the file path since it's psr4
|
||||
if ($psr4) {
|
||||
$classname = str_replace($namespace, "", $classname);
|
||||
}
|
||||
|
||||
// $filename = preg_replace("#\\\\#", "", $classname).".php";
|
||||
// This is fix for nested classes which were losing a /
|
||||
$filename = ltrim($classname .'.php', '\\');
|
||||
$filename = str_replace('\\','/', $filename);
|
||||
|
||||
foreach ($classpaths as $classpath) {
|
||||
$fullpath = trailingslashit($dir) . trailingslashit($classpath) .$filename;
|
||||
if (file_exists($fullpath)) {
|
||||
include_once $fullpath;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
require_once (__DIR__ . "/PackageLoader.php");
|
||||
$loader = new ShortPixel\Build\PackageLoader();
|
||||
$loader->load(__DIR__);
|
||||
|
@ -0,0 +1 @@
|
||||
{"name":"ShortPixel\/shortpixelmodules","description":"ShortPixel submodules","type":"function","autoload":{"psr-4":{"ShortPixel\\ShortPixelLogger":"log\/src","ShortPixel\\Notices":"notices\/src","ShortPixel\\Replacer":"replacer\/src","ShortPixel\\ShortQ":"shortq\/src"}}}
|
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "shortpixel/log",
|
||||
"description": "ShortPixel Logging",
|
||||
"version": "1.1.3",
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Bas",
|
||||
"email": "bas@weblogmechanic.com"
|
||||
}
|
||||
],
|
||||
"minimum-stability": "dev",
|
||||
"require": {},
|
||||
"autoload": {
|
||||
"psr-4": { "ShortPixel\\ShortPixelLogger\\" : "src" }
|
||||
}
|
||||
}
|
@ -0,0 +1,153 @@
|
||||
<?php
|
||||
// The data models.
|
||||
namespace ShortPixel\ShortPixelLogger;
|
||||
|
||||
|
||||
class DebugItem
|
||||
{
|
||||
protected $time;
|
||||
protected $level;
|
||||
protected $message;
|
||||
protected $data = array();
|
||||
protected $caller = false; // array when filled
|
||||
|
||||
protected $model;
|
||||
|
||||
const LEVEL_ERROR = 1;
|
||||
const LEVEL_WARN = 2;
|
||||
const LEVEL_INFO = 3;
|
||||
const LEVEL_DEBUG = 4;
|
||||
|
||||
public function __construct($message, $args)
|
||||
{
|
||||
$this->level = $args['level'];
|
||||
$data = $args['data'];
|
||||
|
||||
$this->message = $message;
|
||||
$this->time = microtime(true);
|
||||
|
||||
$this->setCaller();
|
||||
|
||||
// Add message to data if it seems to be some debug variable.
|
||||
if (is_object($this->message) || is_array($this->message))
|
||||
{
|
||||
$data[] = $this->message;
|
||||
$this->message = __('[Data]');
|
||||
}
|
||||
if (is_array($data) && count($data) > 0)
|
||||
{
|
||||
$dataType = $this->getDataType($data);
|
||||
if ($dataType == 1) // singular
|
||||
{
|
||||
$this->data[] = print_r($data, true);
|
||||
}
|
||||
if ($dataType == 2) //array or object.
|
||||
{
|
||||
$count = false;
|
||||
if (gettype($data) == 'array')
|
||||
$count = count($data);
|
||||
elseif(gettype($data) == 'object')
|
||||
$count = count(get_object_vars($data));
|
||||
|
||||
$firstLine = ucfirst(gettype($data)) . ':';
|
||||
if ($count !== false)
|
||||
$firstLine .= ' (' . $count . ')';
|
||||
|
||||
$this->data[] = $firstLine;
|
||||
|
||||
foreach($data as $index => $item)
|
||||
{
|
||||
if (is_object($item) || is_array($item))
|
||||
{
|
||||
$this->data[] = print_r($index, true) . ' ( ' . ucfirst(gettype($item)) . ') => ' . print_r($item, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
} // if
|
||||
elseif (! is_array($data)) // this leaves out empty default arrays
|
||||
{
|
||||
$this->data[] = print_r($data, true);
|
||||
}
|
||||
}
|
||||
|
||||
public function getData()
|
||||
{
|
||||
return array('time' => $this->time, 'level' => $this->level, 'message' => $this->message, 'data' => $this->data, 'caller' => $this->caller);
|
||||
}
|
||||
|
||||
/** Test Data Array for possible values
|
||||
*
|
||||
* Data can be a collection of several debug vars, a single var, or just an normal array. Test if array has single types,
|
||||
* which is a sign the array is not a collection.
|
||||
*/
|
||||
protected function getDataType($data)
|
||||
{
|
||||
$single_type = array('integer', 'boolean', 'string');
|
||||
if (in_array(gettype(reset($data)), $single_type))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
public function getForFormat()
|
||||
{
|
||||
$data = $this->getData();
|
||||
switch($this->level)
|
||||
{
|
||||
case self::LEVEL_ERROR:
|
||||
$level = 'ERR';
|
||||
$color = "\033[31m";
|
||||
break;
|
||||
case self::LEVEL_WARN:
|
||||
$level = 'WRN';
|
||||
$color = "\033[33m";
|
||||
break;
|
||||
case self::LEVEL_INFO:
|
||||
$level = 'INF';
|
||||
$color = "\033[37m";
|
||||
break;
|
||||
case self::LEVEL_DEBUG:
|
||||
$level = 'DBG';
|
||||
$color = "\033[37m";
|
||||
break;
|
||||
|
||||
}
|
||||
$color_end = "\033[0m";
|
||||
|
||||
$data['color'] = $color;
|
||||
$data['color_end'] = $color_end;
|
||||
$data['level'] = $level;
|
||||
|
||||
return $data;
|
||||
|
||||
//return array('time' => $this->time, 'level' => $level, 'message' => $this->message, 'data' => $this->data, 'color' => $color, 'color_end' => $color_end, 'caller' => $this->caller);
|
||||
|
||||
}
|
||||
|
||||
protected function setCaller()
|
||||
{
|
||||
if(PHP_VERSION_ID < 50400) {
|
||||
$debug=debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
|
||||
} else {
|
||||
$debug=debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS,5);
|
||||
}
|
||||
|
||||
$i = 4;
|
||||
if (isset($debug[$i]))
|
||||
{
|
||||
$info = $debug[$i];
|
||||
$line = isset($info['line']) ? $info['line'] : 'Line unknown';
|
||||
$file = isset($info['file']) ? basename($info['file']) : 'File not set';
|
||||
|
||||
$this->caller = array('line' => $line, 'file' => $file, 'function' => $info['function']);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,390 @@
|
||||
<?php
|
||||
namespace ShortPixel\ShortPixelLogger;
|
||||
|
||||
/*** Logger class
|
||||
*
|
||||
* Class uses the debug data model for keeping log entries.
|
||||
* Logger should not be called before init hook!
|
||||
*/
|
||||
class ShortPixelLogger
|
||||
{
|
||||
static protected $instance = null;
|
||||
protected $start_time;
|
||||
|
||||
protected $is_active = false;
|
||||
protected $is_manual_request = false;
|
||||
protected $show_debug_view = false;
|
||||
|
||||
protected $items = array();
|
||||
protected $logPath = false;
|
||||
protected $logMode = FILE_APPEND;
|
||||
|
||||
protected $logLevel;
|
||||
protected $format = "[ %%time%% ] %%color%% %%level%% %%color_end%% \t %%message%% \t %%caller%% ( %%time_passed%% )";
|
||||
protected $format_data = "\t %%data%% ";
|
||||
|
||||
protected $hooks = array();
|
||||
|
||||
private $logFile; // pointer resource to the logFile.
|
||||
/* protected $hooks = array(
|
||||
'shortpixel_image_exists' => array('numargs' => 3),
|
||||
'shortpixel_webp_image_base' => array('numargs' => 2),
|
||||
'shortpixel_image_urls' => array('numargs' => 2),
|
||||
); // @todo monitor hooks, but this should be more dynamic. Do when moving to module via config.
|
||||
*/
|
||||
|
||||
// utility
|
||||
private $namespace;
|
||||
private $view;
|
||||
|
||||
protected $template = 'view-debug-box';
|
||||
|
||||
/** Debugger constructor
|
||||
* Two ways to activate the debugger. 1) Define SHORTPIXEL_DEBUG in wp-config.php. Either must be true or a number corresponding to required LogLevel
|
||||
* 2) Put SHORTPIXEL_DEBUG in the request. Either true or number.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->start_time = microtime(true);
|
||||
$this->logLevel = DebugItem::LEVEL_WARN;
|
||||
|
||||
$ns = __NAMESPACE__;
|
||||
$this->namespace = substr($ns, 0, strpos($ns, '\\')); // try to get first part of namespace
|
||||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is not a form
|
||||
if (isset($_REQUEST['SHORTPIXEL_DEBUG'])) // manual takes precedence over constants
|
||||
{
|
||||
$this->is_manual_request = true;
|
||||
$this->is_active = true;
|
||||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is not a form
|
||||
if ($_REQUEST['SHORTPIXEL_DEBUG'] === 'true')
|
||||
{
|
||||
$this->logLevel = DebugItem::LEVEL_INFO;
|
||||
}
|
||||
else {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is not a form
|
||||
$this->logLevel = intval($_REQUEST['SHORTPIXEL_DEBUG']);
|
||||
}
|
||||
|
||||
}
|
||||
else if ( (defined('SHORTPIXEL_DEBUG') && SHORTPIXEL_DEBUG > 0) )
|
||||
{
|
||||
$this->is_active = true;
|
||||
if (SHORTPIXEL_DEBUG === true)
|
||||
$this->logLevel = DebugItem::LEVEL_INFO;
|
||||
else {
|
||||
$this->logLevel = intval(SHORTPIXEL_DEBUG);
|
||||
}
|
||||
}
|
||||
|
||||
if (defined('SHORTPIXEL_DEBUG_TARGET') && SHORTPIXEL_DEBUG_TARGET || $this->is_manual_request)
|
||||
{
|
||||
if (defined('SHORTPIXEL_LOG_OVERWRITE')) // if overwrite, do this on init once.
|
||||
file_put_contents($this->logPath,'-- Log Reset -- ' .PHP_EOL);
|
||||
|
||||
}
|
||||
|
||||
if ($this->is_active)
|
||||
{
|
||||
/* On Early init, this function might not exist, then queue it when needed */
|
||||
if (! function_exists('wp_get_current_user'))
|
||||
add_action('init', array($this, 'initView'));
|
||||
else
|
||||
$this->initView();
|
||||
}
|
||||
|
||||
if ($this->is_active && count($this->hooks) > 0)
|
||||
$this->monitorHooks();
|
||||
}
|
||||
|
||||
/** Init the view when needed. Private function ( public because of WP_HOOK )
|
||||
* Never call directly */
|
||||
public function initView()
|
||||
{
|
||||
$user_is_administrator = (current_user_can('manage_options')) ? true : false;
|
||||
|
||||
if ($this->is_active && $this->is_manual_request && $user_is_administrator )
|
||||
{
|
||||
$content_url = content_url();
|
||||
$logPath = $this->logPath;
|
||||
$pathpos = strpos($logPath, 'wp-content') + strlen('wp-content');
|
||||
$logPart = substr($logPath, $pathpos);
|
||||
$logLink = $content_url . $logPart;
|
||||
|
||||
$this->view = new \stdClass;
|
||||
$this->view->logLink = $logLink;
|
||||
add_action('admin_footer', array($this, 'loadView'));
|
||||
}
|
||||
}
|
||||
|
||||
public static function getInstance()
|
||||
{
|
||||
if ( self::$instance === null)
|
||||
{
|
||||
self::$instance = new ShortPixelLogger();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function setLogPath($logPath)
|
||||
{
|
||||
$this->logPath = $logPath;
|
||||
$this->getWriteFile(true); // reset the writeFile here.
|
||||
}
|
||||
protected function addLog($message, $level, $data = array())
|
||||
{
|
||||
// $log = self::getInstance();
|
||||
|
||||
// don't log anything too low or when not active.
|
||||
if ($this->logLevel < $level || ! $this->is_active)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Force administrator on manuals.
|
||||
if ( $this->is_manual_request )
|
||||
{
|
||||
if (! function_exists('wp_get_current_user')) // not loaded yet
|
||||
return false;
|
||||
|
||||
$user_is_administrator = (current_user_can('manage_options')) ? true : false;
|
||||
if (! $user_is_administrator)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check where to log to.
|
||||
if ($this->logPath === false)
|
||||
{
|
||||
$upload_dir = wp_upload_dir(null,false,false);
|
||||
$this->logPath = $this->setLogPath($upload_dir['basedir'] . '/' . $this->namespace . ".log");
|
||||
}
|
||||
|
||||
$arg = array();
|
||||
$args['level'] = $level;
|
||||
$args['data'] = $data;
|
||||
|
||||
$newItem = new DebugItem($message, $args);
|
||||
$this->items[] = $newItem;
|
||||
|
||||
if ($this->is_active)
|
||||
{
|
||||
$this->write($newItem);
|
||||
}
|
||||
}
|
||||
|
||||
/** Writes to log File. */
|
||||
protected function write($debugItem, $mode = 'file')
|
||||
{
|
||||
$items = $debugItem->getForFormat();
|
||||
$items['time_passed'] = round ( ($items['time'] - $this->start_time), 5);
|
||||
$items['time'] = date('Y-m-d H:i:s', (int) $items['time'] );
|
||||
|
||||
if ( ($items['caller']) && is_array($items['caller']) && count($items['caller']) > 0)
|
||||
{
|
||||
$caller = $items['caller'];
|
||||
$items['caller'] = $caller['file'] . ' in ' . $caller['function'] . '(' . $caller['line'] . ')';
|
||||
}
|
||||
|
||||
$line = $this->formatLine($items);
|
||||
|
||||
$file = $this->getWriteFile();
|
||||
|
||||
// try to write to file. Don't write if directory doesn't exists (leads to notices)
|
||||
if ($file )
|
||||
{
|
||||
fwrite($file, $line);
|
||||
// file_put_contents($this->logPath,$line, FILE_APPEND);
|
||||
}
|
||||
else {
|
||||
// error_log($line);
|
||||
}
|
||||
}
|
||||
|
||||
protected function getWriteFile($reset = false)
|
||||
{
|
||||
if (! is_null($this->logFile) && $reset === false)
|
||||
{
|
||||
return $this->logFile;
|
||||
}
|
||||
elseif(is_object($this->logFile))
|
||||
{
|
||||
fclose($this->logFile);
|
||||
}
|
||||
|
||||
$logDir = dirname($this->logPath);
|
||||
if (! is_dir($logDir) || ! is_writable($logDir))
|
||||
{
|
||||
error_log('ShortpixelLogger: Log Directory is not writable');
|
||||
$this->logFile = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
$file = fopen($this->logPath, 'a');
|
||||
if ($file === false)
|
||||
{
|
||||
error_log('ShortpixelLogger: File could not be opened / created: ' . $this->logPath);
|
||||
$this->logFile = false;
|
||||
return $file;
|
||||
}
|
||||
|
||||
$this->logFile = $file;
|
||||
return $file;
|
||||
}
|
||||
|
||||
protected function formatLine($args = array() )
|
||||
{
|
||||
$line= $this->format;
|
||||
foreach($args as $key => $value)
|
||||
{
|
||||
if (! is_array($value) && ! is_object($value))
|
||||
$line = str_replace('%%' . $key . '%%', $value, $line);
|
||||
}
|
||||
|
||||
$line .= PHP_EOL;
|
||||
|
||||
if (isset($args['data']))
|
||||
{
|
||||
$data = array_filter($args['data']);
|
||||
if (count($data) > 0)
|
||||
{
|
||||
foreach($data as $item)
|
||||
{
|
||||
$line .= $item . PHP_EOL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $line;
|
||||
}
|
||||
|
||||
protected function setLogLevel($level)
|
||||
{
|
||||
$this->logLevel = $level;
|
||||
}
|
||||
|
||||
protected function getEnv($name)
|
||||
{
|
||||
if (isset($this->{$name}))
|
||||
{
|
||||
return $this->{$name};
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static function addError($message, $args = array())
|
||||
{
|
||||
$level = DebugItem::LEVEL_ERROR;
|
||||
$log = self::getInstance();
|
||||
$log->addLog($message, $level, $args);
|
||||
}
|
||||
public static function addWarn($message, $args = array())
|
||||
{
|
||||
$level = DebugItem::LEVEL_WARN;
|
||||
$log = self::getInstance();
|
||||
$log->addLog($message, $level, $args);
|
||||
}
|
||||
// Alias, since it goes wrong so often.
|
||||
public static function addWarning($message, $args = array())
|
||||
{
|
||||
self::addWarn($message, $args);
|
||||
}
|
||||
public static function addInfo($message, $args = array())
|
||||
{
|
||||
$level = DebugItem::LEVEL_INFO;
|
||||
$log = self::getInstance();
|
||||
$log->addLog($message, $level, $args);
|
||||
}
|
||||
public static function addDebug($message, $args = array())
|
||||
{
|
||||
$level = DebugItem::LEVEL_DEBUG;
|
||||
$log = self::getInstance();
|
||||
$log->addLog($message, $level, $args);
|
||||
}
|
||||
|
||||
/** These should be removed every release. They are temporary only for d'bugging the current release */
|
||||
public static function addTemp($message, $args = array())
|
||||
{
|
||||
self::addDebug($message, $args);
|
||||
}
|
||||
|
||||
public static function logLevel($level)
|
||||
{
|
||||
$log = self::getInstance();
|
||||
static::addInfo('Changing Log level' . $level);
|
||||
$log->setLogLevel($level);
|
||||
}
|
||||
|
||||
public static function getLogLevel()
|
||||
{
|
||||
$log = self::getInstance();
|
||||
return $log->getEnv('logLevel');
|
||||
}
|
||||
|
||||
public static function isManualDebug()
|
||||
{
|
||||
$log = self::getInstance();
|
||||
return $log->getEnv('is_manual_request');
|
||||
}
|
||||
|
||||
public static function getLogPath()
|
||||
{
|
||||
$log = self::getInstance();
|
||||
return $log->getEnv('logPath');
|
||||
}
|
||||
|
||||
/** Function to test if the debugger is active
|
||||
* @return boolean true when active.
|
||||
*/
|
||||
public static function debugIsActive()
|
||||
{
|
||||
$log = self::getInstance();
|
||||
return $log->getEnv('is_active');
|
||||
}
|
||||
|
||||
protected function monitorHooks()
|
||||
{
|
||||
|
||||
foreach($this->hooks as $hook => $data)
|
||||
{
|
||||
$numargs = isset($data['numargs']) ? $data['numargs'] : 1;
|
||||
$prio = isset($data['priority']) ? $data['priority'] : 10;
|
||||
|
||||
add_filter($hook, function($value) use ($hook) {
|
||||
$args = func_get_args();
|
||||
return $this->logHook($hook, $value, $args); }, $prio, $numargs);
|
||||
}
|
||||
}
|
||||
|
||||
public function logHook($hook, $value, $args)
|
||||
{
|
||||
array_shift($args);
|
||||
self::addInfo('[Hook] - ' . $hook . ' with ' . var_export($value,true), $args);
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function loadView()
|
||||
{
|
||||
// load either param or class template.
|
||||
$template = $this->template;
|
||||
|
||||
$view = $this->view;
|
||||
$view->namespace = $this->namespace;
|
||||
$controller = $this;
|
||||
|
||||
$template_path = __DIR__ . '/' . $this->template . '.php';
|
||||
if (file_exists($template_path))
|
||||
{
|
||||
|
||||
include($template_path);
|
||||
}
|
||||
else {
|
||||
self::addError("View $template for ShortPixelLogger could not be found in " . $template_path,
|
||||
array('class' => get_class($this)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} // class debugController
|
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
// Debug Box to load Log File
|
||||
namespace ShortPixel\ShortPixelLogger;
|
||||
wp_enqueue_script( 'jquery-ui-draggable' );
|
||||
|
||||
?>
|
||||
|
||||
<style>
|
||||
.sp_debug_wrap
|
||||
{
|
||||
position: relative;
|
||||
clear: both;
|
||||
}
|
||||
.sp_debug_box
|
||||
{
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 50px;
|
||||
background-color: #fff;
|
||||
width: 150px;
|
||||
z-index: 1000000;
|
||||
border: 1px solid #000;
|
||||
|
||||
}
|
||||
.sp_debug_box .header
|
||||
{
|
||||
min-height: 10px;
|
||||
background: #000;
|
||||
color: #fff;
|
||||
padding: 8px
|
||||
}
|
||||
.sp_debug_box .content_box
|
||||
{
|
||||
background: #ccc;
|
||||
}
|
||||
.content_box
|
||||
{
|
||||
padding: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script language='javascript'>
|
||||
jQuery(document).ready(function($)
|
||||
{
|
||||
$( ".sp_debug_box" ).draggable();
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<div class='sp_debug_box'>
|
||||
<div class='header'><?php echo esc_html($view->namespace) ?> Debug Box </div>
|
||||
<a target="_blank" href='<?php echo esc_url($view->logLink) ?>'>Logfile</a>
|
||||
<div class='content_box'>
|
||||
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "shortpixel/notices",
|
||||
"description": "ShortPixel WordPress Notice System",
|
||||
"version": "1.5",
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Bas",
|
||||
"email": "bas@weblogmechanic.com"
|
||||
}
|
||||
],
|
||||
"minimum-stability": "dev",
|
||||
"require": {
|
||||
"shortpixel/log" : "1.1.*"
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
"packagist.org": false,
|
||||
"type": "path",
|
||||
"url": "../modules/",
|
||||
"options": {
|
||||
"symlink": true
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
"autoload": {
|
||||
"psr-4": { "ShortPixel\\Notices\\" : "src" }
|
||||
}
|
||||
}
|
@ -0,0 +1,372 @@
|
||||
<?php
|
||||
namespace ShortPixel\Notices;
|
||||
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
|
||||
|
||||
class NoticeController //extends ShortPixelController
|
||||
{
|
||||
protected static $notices = array();
|
||||
protected static $instance = null;
|
||||
protected static $cssHookLoaded = false; // prevent css output more than once.
|
||||
|
||||
protected $notice_displayed = array();
|
||||
|
||||
public $notice_count = 0;
|
||||
|
||||
protected $has_stored = false;
|
||||
|
||||
protected $notice_option = ''; // The wp_options name for notices here.
|
||||
|
||||
/** For backward compat. Never call constructor directly. */
|
||||
public function __construct()
|
||||
{
|
||||
$ns = __NAMESPACE__;
|
||||
$ns = substr($ns, 0, strpos($ns, '\\')); // try to get first part of namespace
|
||||
$this->notice_option = $ns . '-notices';
|
||||
|
||||
add_action('wp_ajax_' . $this->notice_option, array($this, 'ajax_action'));
|
||||
|
||||
$this->loadNotices();
|
||||
//$this->loadConfig();
|
||||
}
|
||||
|
||||
public static function getInstance()
|
||||
{
|
||||
if ( self::$instance === null)
|
||||
{
|
||||
self::$instance = new NoticeController();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/** Reset all notices, before loading them, to ensure on updates / activations one starts fresh */
|
||||
public static function resetNotices()
|
||||
{
|
||||
$ns = __NAMESPACE__;
|
||||
$ns = substr($ns, 0, strpos($ns, '\\')); // try to get first part of namespace
|
||||
$result = delete_option($ns . '-notices');
|
||||
}
|
||||
|
||||
/** Load Notices Config File, if any
|
||||
*
|
||||
* [ Future Use ]
|
||||
*/
|
||||
public function loadConfig()
|
||||
{
|
||||
return;
|
||||
if (file_exists('../notice_config.json'))
|
||||
{
|
||||
$config = file_get_contents('../notice_config.json');
|
||||
$json_config = json_decode($config);
|
||||
}
|
||||
}
|
||||
|
||||
public function loadIcons($icons)
|
||||
{
|
||||
foreach($icons as $name => $icon)
|
||||
NoticeModel::setIcon($name, $icon);
|
||||
}
|
||||
|
||||
|
||||
protected function loadNotices()
|
||||
{
|
||||
$notices = get_option($this->notice_option, false);
|
||||
$cnotice = (is_array($notices)) ? count($notices) : 0;
|
||||
|
||||
if ($notices !== false && is_array($notices))
|
||||
{
|
||||
$checked = array();
|
||||
foreach($notices as $noticeObj)
|
||||
{
|
||||
if (is_object($noticeObj) && $noticeObj instanceOf NoticeModel)
|
||||
{
|
||||
$checked[] = $noticeObj;
|
||||
}
|
||||
}
|
||||
self::$notices = $checked;
|
||||
$this->has_stored = true;
|
||||
}
|
||||
else {
|
||||
self::$notices = array();
|
||||
$this->has_stored = false;
|
||||
}
|
||||
$this->countNotices();
|
||||
}
|
||||
|
||||
|
||||
protected function addNotice($message, $code, $unique)
|
||||
{
|
||||
$notice = new NoticeModel($message, $code);
|
||||
|
||||
if ($unique)
|
||||
{
|
||||
foreach(self::$notices as $nitem)
|
||||
{
|
||||
if ($nitem->message == $notice->message && $nitem->code == $notice->code) // same message.
|
||||
return $nitem; // return the notice with the same message.
|
||||
}
|
||||
}
|
||||
self::$notices[] = $notice;
|
||||
$this->countNotices();
|
||||
|
||||
$this->update();
|
||||
return $notice;
|
||||
}
|
||||
|
||||
/** Update the notices to store, check what to remove, returns count. */
|
||||
public function update()
|
||||
{
|
||||
if (! is_array(self::$notices) || count(self::$notices) == 0)
|
||||
{
|
||||
if ($this->has_stored)
|
||||
delete_option($this->notice_option);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$new_notices = array();
|
||||
foreach(self::$notices as $item)
|
||||
{
|
||||
if (! $item->isDone() )
|
||||
{
|
||||
$new_notices[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
update_option($this->notice_option, $new_notices);
|
||||
self::$notices = $new_notices;
|
||||
|
||||
return $this->countNotices();
|
||||
}
|
||||
|
||||
public function countNotices()
|
||||
{
|
||||
$this->notice_count = count(self::$notices);
|
||||
return $this->notice_count;
|
||||
}
|
||||
|
||||
|
||||
public function getNotices()
|
||||
{
|
||||
return self::$notices;
|
||||
}
|
||||
|
||||
public function getNoticesForDisplay()
|
||||
{
|
||||
$newNotices = array();
|
||||
|
||||
foreach(self::$notices as $notice)
|
||||
{
|
||||
if ($notice->isDismissed()) // dismissed never displays.
|
||||
continue;
|
||||
|
||||
if ($notice->isPersistent())
|
||||
{
|
||||
$id = $notice->getID();
|
||||
if (! is_null($id) && ! in_array($id, $this->notice_displayed))
|
||||
{
|
||||
$notice->notice_action = $this->notice_option;
|
||||
$newNotices[] = $notice;
|
||||
$this->notice_displayed[] = $id;
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
$newNotices[] = $notice;
|
||||
|
||||
|
||||
}
|
||||
return $newNotices;
|
||||
}
|
||||
|
||||
|
||||
public function getNoticeByID($id)
|
||||
{
|
||||
foreach(self::$notices as $notice)
|
||||
{
|
||||
if ($notice->getID() == $id)
|
||||
return $notice;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function removeNoticeByID($id)
|
||||
{
|
||||
$noticeController = self::getInstance();
|
||||
|
||||
for($i = 0; $i < count(self::$notices); $i++)
|
||||
{
|
||||
$item = self::$notices[$i];
|
||||
if (is_object($item) && $item->getID() == $id)
|
||||
{
|
||||
Log::addDebug('Removing notice with ID ' . $id);
|
||||
unset(self::$notices[$i]);
|
||||
}
|
||||
//if ($notice_item )
|
||||
}
|
||||
$noticeController->update();
|
||||
}
|
||||
|
||||
public function ajax_action()
|
||||
{
|
||||
$response = array('result' => false, 'reason' => '');
|
||||
|
||||
if (isset($_POST['nonce']) && wp_verify_nonce( sanitize_key($_POST['nonce']), 'dismiss') )
|
||||
{
|
||||
if (isset($_POST['plugin_action']) && 'dismiss' == $_POST['plugin_action'] )
|
||||
{
|
||||
$id = (isset($_POST['id'])) ? sanitize_text_field( wp_unslash($_POST['id'])) : null;
|
||||
|
||||
if (! is_null($id))
|
||||
{
|
||||
|
||||
$notice = $this->getNoticeByID($id);
|
||||
}
|
||||
else
|
||||
{
|
||||
$notice = false;
|
||||
}
|
||||
|
||||
if(false !== $notice)
|
||||
{
|
||||
$notice->dismiss();
|
||||
$this->update();
|
||||
$response['result'] = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log::addError('Notice not found when dismissing -> ' . $id, self::$notices);
|
||||
$response['result'] = false;
|
||||
$response['reason'] = ' Notice ' . $id . ' not found. ';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
Log::addError('Wrong Nonce when dismissed notice. ');
|
||||
$response['reason'] = 'wrong nonce';
|
||||
}
|
||||
wp_send_json($response);
|
||||
}
|
||||
|
||||
/** Adds a notice, quick and fast method
|
||||
* @param String $message The Message you want to notify
|
||||
* @param Boolean $unique If unique, check to not repeat notice exact same text in notices. Discard if so
|
||||
* @param int $code A value of messageType as defined in model
|
||||
* @returm Object Instance of noticeModel
|
||||
*/
|
||||
|
||||
public static function addNormal($message, $unique = false)
|
||||
{
|
||||
$noticeController = self::getInstance();
|
||||
$notice = $noticeController->addNotice($message, NoticeModel::NOTICE_NORMAL, $unique);
|
||||
return $notice;
|
||||
|
||||
}
|
||||
|
||||
public static function addError($message, $unique = false)
|
||||
{
|
||||
$noticeController = self::getInstance();
|
||||
$notice = $noticeController->addNotice($message, NoticeModel::NOTICE_ERROR, $unique);
|
||||
return $notice;
|
||||
|
||||
}
|
||||
|
||||
public static function addWarning($message, $unique = false)
|
||||
{
|
||||
$noticeController = self::getInstance();
|
||||
$notice = $noticeController->addNotice($message, NoticeModel::NOTICE_WARNING, $unique);
|
||||
return $notice;
|
||||
}
|
||||
|
||||
public static function addSuccess($message, $unique = false)
|
||||
{
|
||||
$noticeController = self::getInstance();
|
||||
$notice = $noticeController->addNotice($message, NoticeModel::NOTICE_SUCCESS, $unique);
|
||||
return $notice;
|
||||
|
||||
}
|
||||
|
||||
public static function addDetail($notice, $detail)
|
||||
{
|
||||
$noticeController = self::getInstance();
|
||||
$notice->addDetail($detail);
|
||||
|
||||
// $notice_id = spl_object_id($notice);
|
||||
|
||||
$noticeController->update();
|
||||
}
|
||||
|
||||
/** Make a regular notice persistent across multiple page loads
|
||||
* @param $notice NoticeModel The Notice to make Persistent
|
||||
* @param $key String Identifier of the persistent notice.
|
||||
* @param $suppress Int When dismissed, time to stay dismissed
|
||||
* @param $callback Function Callable function
|
||||
*/
|
||||
public static function makePersistent($notice, $key, $suppress = -1, $callback = null)
|
||||
{
|
||||
$noticeController = self::getInstance();
|
||||
$existing = $noticeController->getNoticeByID($key);
|
||||
|
||||
// if this key already exists, don't allow the new notice to be entered into the array. Remove it since it's already created.
|
||||
if ($existing)
|
||||
{
|
||||
for($i = 0; $i < count(self::$notices); $i++)
|
||||
{
|
||||
$item = self::$notices[$i];
|
||||
|
||||
if ($item->message == $notice->message && $item->getID() == null)
|
||||
{
|
||||
if ($item->message != $existing->message) // allow the persistent message to be updated, if something else is served on this ID
|
||||
{
|
||||
$existing->message = $item->message;
|
||||
}
|
||||
unset(self::$notices[$i]);
|
||||
}
|
||||
//if ($notice_item )
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$notice->setPersistent($key, $suppress, $callback); // set this notice persistent.
|
||||
}
|
||||
|
||||
$noticeController->update();
|
||||
}
|
||||
|
||||
public function admin_notices()
|
||||
{
|
||||
if ($this->countNotices() > 0)
|
||||
{
|
||||
if (! self::$cssHookLoaded)
|
||||
{
|
||||
add_action('admin_print_footer_scripts', array($this, 'printNoticeStyle'));
|
||||
self::$cssHookLoaded = true;
|
||||
}
|
||||
foreach($this->getNoticesForDisplay() as $notice)
|
||||
{
|
||||
echo $notice->getForDisplay();
|
||||
}
|
||||
}
|
||||
$this->update(); // puts views, and updates
|
||||
}
|
||||
|
||||
|
||||
public function printNoticeStyle()
|
||||
{
|
||||
if (file_exists(__DIR__ . '/css/notices.css'))
|
||||
{
|
||||
echo '<style>' . esc_html(file_get_contents(__DIR__ . '/css/notices.css')) . '</style>';
|
||||
}
|
||||
else {
|
||||
Log::addDebug('Notices : css/notices.css could not be loaded');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,366 @@
|
||||
<?php
|
||||
namespace ShortPixel\Notices;
|
||||
|
||||
class NoticeModel //extends ShortPixelModel
|
||||
{
|
||||
public $message; // The message we want to convey.
|
||||
public $details = array(); // extra details, like the files involved. Something could be hideable in the future.
|
||||
public $code;
|
||||
|
||||
private $id = null; // used for persistent messages.
|
||||
protected $viewed = false; // was this notice viewed?
|
||||
protected $is_persistent = false; // This is a fatal issue, display until something was fixed.
|
||||
protected $is_dismissed = false; // for persistent notices,
|
||||
protected $suppress_until = null;
|
||||
protected $suppress_period = -1;
|
||||
protected $include_screens = array();
|
||||
protected $exclude_screens = array();
|
||||
public $is_removable = true; // if removable, display a notice dialog with red X or so.
|
||||
public $messageType = self::NOTICE_NORMAL;
|
||||
|
||||
public $notice_action; // empty unless for display. Ajax action to talk back to controller.
|
||||
protected $callback; // empty unless callback is needed
|
||||
|
||||
public static $icons = array();
|
||||
|
||||
private static $jsDismissLoaded;
|
||||
|
||||
const NOTICE_NORMAL = 1;
|
||||
const NOTICE_ERROR = 2;
|
||||
const NOTICE_SUCCESS = 3;
|
||||
const NOTICE_WARNING = 4;
|
||||
|
||||
/** Use this model in conjunction with NoticeController, do not call directly */
|
||||
public function __construct($message, $messageType = self::NOTICE_NORMAL)
|
||||
{
|
||||
$this->message = $message;
|
||||
$this->messageType = $messageType;
|
||||
}
|
||||
|
||||
public function isDone()
|
||||
{
|
||||
// check suppressed
|
||||
if ($this->is_dismissed && ! is_null($this->suppress_until))
|
||||
{
|
||||
if (time() >= $this->suppress_until)
|
||||
{
|
||||
$this->is_persistent = false; // unpersist, so it will be cleaned and dropped.
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->viewed && ! $this->is_persistent)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getID()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function isPersistent()
|
||||
{
|
||||
return $this->is_persistent;
|
||||
}
|
||||
|
||||
public function isDismissed()
|
||||
{
|
||||
return $this->is_dismissed;
|
||||
}
|
||||
|
||||
public function dismiss()
|
||||
{
|
||||
$this->is_dismissed = true;
|
||||
$this->suppress_until = time() + $this->suppress_period;
|
||||
}
|
||||
|
||||
public function unDismiss()
|
||||
{
|
||||
$this->is_dismissed = false;
|
||||
}
|
||||
|
||||
public function setDismissedUntil($timestamp)
|
||||
{
|
||||
$this->suppress_until = $timestamp;
|
||||
}
|
||||
|
||||
/** Support for extra information beyond the message.
|
||||
* Can help to not overwhelm users w/ the same message but different file /circumstances.
|
||||
*/
|
||||
public function addDetail($detail, $clean = false)
|
||||
{
|
||||
if ($clean)
|
||||
$this->details = array();
|
||||
|
||||
if (! in_array($detail, $this->details) )
|
||||
$this->details[] = $detail;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $method String Include or Exclude
|
||||
* @param $includes String|Array Screen Names to Include / Exclude either string, or array
|
||||
*/
|
||||
public function limitScreens($method, $screens)
|
||||
{
|
||||
if ($method == 'exclude')
|
||||
{
|
||||
$var = 'exclude_screens';
|
||||
}
|
||||
else {
|
||||
$var = 'include_screens';
|
||||
}
|
||||
|
||||
if (is_array($screens))
|
||||
{
|
||||
$this->$var = array_merge($this->$var, $screens);
|
||||
}
|
||||
else {
|
||||
$this->{$var}[] = $screens; // strange syntax is PHP 5.6 compat.
|
||||
}
|
||||
}
|
||||
|
||||
/* Checks if Notice is allowed on this screen
|
||||
* @param @screen_id String The screen Id to check ( most likely current one, via EnvironmentModel)
|
||||
*/
|
||||
public function checkScreen($screen_id)
|
||||
{
|
||||
if (in_array($screen_id, $this->exclude_screens))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (in_array($screen_id, $this->include_screens))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// if include is set, don't show if not screen included.
|
||||
if (count($this->include_screens) == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** Set a notice persistent. Meaning it shows every page load until dismissed.
|
||||
* @param $key Unique Key of this message. Required
|
||||
* @param $suppress When dismissed do not show this message again for X amount of time. When -1 it will just be dropped from the Notices and not suppressed
|
||||
*/
|
||||
public function setPersistent($key, $suppress = -1, $callback = null)
|
||||
{
|
||||
$this->id = $key;
|
||||
$this->is_persistent = true;
|
||||
$this->suppress_period = $suppress;
|
||||
if ( ! is_null($callback) && is_callable($callback))
|
||||
{
|
||||
$this->callback = $callback;
|
||||
}
|
||||
}
|
||||
|
||||
public static function setIcon($notice_type, $icon)
|
||||
{
|
||||
switch($notice_type)
|
||||
{
|
||||
case 'error':
|
||||
$type = self::NOTICE_ERROR;
|
||||
break;
|
||||
case 'success':
|
||||
$type = self::NOTICE_SUCCESS;
|
||||
break;
|
||||
case 'warning':
|
||||
$type = self::NOTICE_WARNING;
|
||||
break;
|
||||
case 'normal':
|
||||
default:
|
||||
$type = self::NOTICE_NORMAL;
|
||||
break;
|
||||
}
|
||||
self::$icons[$type] = $icon;
|
||||
}
|
||||
|
||||
public function _debug_getvar($var)
|
||||
{
|
||||
if (property_exists($this, $var))
|
||||
{
|
||||
return $this->$var;
|
||||
}
|
||||
}
|
||||
|
||||
private function checkIncomplete($var)
|
||||
{
|
||||
return ($var instanceof \__PHP_Incomplete_Class);
|
||||
}
|
||||
|
||||
public function getForDisplay()
|
||||
{
|
||||
$this->viewed = true;
|
||||
$class = 'shortpixel shortpixel-notice ';
|
||||
|
||||
$icon = '';
|
||||
|
||||
if ($this->callback)
|
||||
{
|
||||
if (is_array($this->callback))
|
||||
{
|
||||
foreach($this->callback as $part)
|
||||
{
|
||||
if ($this->checkIncomplete($part) === true)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} elseif (is_object($this->callback))
|
||||
{
|
||||
if ($this->checkIncomplete($part) === true)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! is_callable($this->callback))
|
||||
{
|
||||
return;
|
||||
}
|
||||
else {
|
||||
$return = call_user_func($this->callback, $this);
|
||||
if ($return === false) // don't display is callback returns false explicitly.
|
||||
return;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
switch($this->messageType)
|
||||
{
|
||||
case self::NOTICE_ERROR:
|
||||
$class .= 'notice-error ';
|
||||
$icon = isset(self::$icons[self::NOTICE_ERROR]) ? self::$icons[self::NOTICE_ERROR] : '';
|
||||
//$icon = 'scared';
|
||||
break;
|
||||
case self::NOTICE_SUCCESS:
|
||||
$class .= 'notice-success ';
|
||||
$icon = isset(self::$icons[self::NOTICE_SUCCESS]) ? self::$icons[self::NOTICE_SUCCESS] : '';
|
||||
break;
|
||||
case self::NOTICE_WARNING:
|
||||
$class .= 'notice-warning ';
|
||||
$icon = isset(self::$icons[self::NOTICE_WARNING]) ? self::$icons[self::NOTICE_WARNING] : '';
|
||||
break;
|
||||
case self::NOTICE_NORMAL:
|
||||
$class .= 'notice-info ';
|
||||
$icon = isset(self::$icons[self::NOTICE_NORMAL]) ? self::$icons[self::NOTICE_NORMAL] : '';
|
||||
break;
|
||||
default:
|
||||
$class .= 'notice-info ';
|
||||
$icon = '';
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
if ($this->is_removable)
|
||||
{
|
||||
$class .= 'is-dismissible ';
|
||||
}
|
||||
|
||||
if ($this->is_persistent)
|
||||
{
|
||||
$class .= 'is-persistent ';
|
||||
}
|
||||
|
||||
$id = ! is_null($this->id) ? $this->id : uniqid();
|
||||
//'id="' . $this->id . '"'
|
||||
$output = "<div id='$id' class='$class'><span class='icon'> " . $icon . "</span> <span class='content'>" . $this->message;
|
||||
if ($this->hasDetails())
|
||||
{
|
||||
$output .= '<div class="details-wrapper">
|
||||
<input type="checkbox" name="detailhider" id="check-' . $id .'">
|
||||
<label for="check-' . $id . '" class="show-details"><span>' . __('See Details', 'shortpixel-image-optimiser') . '</span>
|
||||
</label>';
|
||||
|
||||
$output .= "<div class='detail-content-wrapper'><p class='detail-content'>" . $this->parseDetails() . "</p></div>";
|
||||
$output .= '<label for="check-' . $id . '" class="hide-details"><span>' . __('Hide Details', 'shortpixel-image-optimiser') . '</span></label>';
|
||||
|
||||
$output .= '</div>'; // detail wrapper
|
||||
|
||||
}
|
||||
$output .= "</span>";
|
||||
|
||||
if ($this->is_removable)
|
||||
{
|
||||
$output .= '<button type="button" id="button-' . $id . '" class="notice-dismiss" data-dismiss="' . $this->suppress_period . '" ><span class="screen-reader-text">' . __('Dismiss this notice', 'shortpixel-image-optimiser') . '</span></button>';
|
||||
|
||||
if (! $this->is_persistent)
|
||||
{
|
||||
$output .= "<script type='text/javascript'>\n
|
||||
document.getElementById('button-$id').onclick = function()
|
||||
{
|
||||
var el = document.getElementById('$id');
|
||||
jQuery(el).fadeTo(100,0,function() {
|
||||
jQuery(el).slideUp(100, 0, function () {
|
||||
jQuery(el).remove();
|
||||
})
|
||||
});
|
||||
} </script>";
|
||||
}
|
||||
}
|
||||
|
||||
$output .= "</div>";
|
||||
|
||||
if ($this->is_persistent && $this->is_removable)
|
||||
{
|
||||
$output .= "<script type='text/javascript'>\n" . $this->getDismissJS() . "\n</script>";
|
||||
}
|
||||
return $output;
|
||||
|
||||
}
|
||||
|
||||
protected function hasDetails()
|
||||
{
|
||||
if (is_array($this->details) && count($this->details) > 0)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function parseDetails()
|
||||
{
|
||||
return implode('<BR>', $this->details);
|
||||
}
|
||||
|
||||
private function getDismissJS()
|
||||
{
|
||||
|
||||
$js = '';
|
||||
if (is_null(self::$jsDismissLoaded))
|
||||
{
|
||||
$nonce = wp_create_nonce('dismiss');
|
||||
$url = wp_json_encode(admin_url('admin-ajax.php'));
|
||||
$js = "function shortpixel_notice_dismiss(event) {
|
||||
event.preventDefault();
|
||||
var ev = event.detail;
|
||||
var target = event.target;
|
||||
var parent = target.parentElement;
|
||||
|
||||
var data = {
|
||||
'plugin_action': 'dismiss',
|
||||
'action' : '$this->notice_action',
|
||||
'nonce' : '$nonce',
|
||||
}
|
||||
data.time = target.getAttribute('data-dismiss');
|
||||
data.id = parent.getAttribute('id');
|
||||
jQuery.post($url,data);
|
||||
|
||||
jQuery(parent).fadeTo(100,0,function() {
|
||||
jQuery(parent).slideUp(100, 0, function () {
|
||||
jQuery(parent).remove();
|
||||
})
|
||||
});
|
||||
}";
|
||||
}
|
||||
|
||||
$js .= ' jQuery("#' . $this->id . '").find(".notice-dismiss").on("click", shortpixel_notice_dismiss); ';
|
||||
|
||||
return "\n jQuery(document).ready(function(){ \n" . $js . "\n});";
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
.shortpixel.shortpixel-notice {
|
||||
min-height: 75px;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
padding: 1px 12px;
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04);
|
||||
border: 1px solid #c3c4c7;
|
||||
margin: 15px 0;
|
||||
border-left-width: 4px;
|
||||
border-left-color: #72aee6;
|
||||
position: relative;
|
||||
}
|
||||
.shortpixel.shortpixel-notice span {
|
||||
vertical-align: middle;
|
||||
}
|
||||
.shortpixel.shortpixel-notice span.icon {
|
||||
margin: 0 25px 0 0;
|
||||
width: 80px;
|
||||
}
|
||||
.shortpixel.shortpixel-notice span.content {
|
||||
padding: 8px 0;
|
||||
word-wrap: break-word;
|
||||
overflow: hidden;
|
||||
}
|
||||
.shortpixel.shortpixel-notice img {
|
||||
display: inline-block;
|
||||
margin: 0 25px 0 0;
|
||||
max-height: 50px;
|
||||
}
|
||||
.shortpixel.shortpixel-notice .notice-dismiss {
|
||||
margin-top: 6px;
|
||||
}
|
||||
.shortpixel.shortpixel-notice.notice-success {
|
||||
border-left-color: #00a32a;
|
||||
}
|
||||
.shortpixel.shortpixel-notice.notice-warning {
|
||||
border-left-color: #dba617;
|
||||
}
|
||||
.shortpixel.shortpixel-notice.notice-error {
|
||||
border-left-color: #ff0000;
|
||||
}
|
||||
.shortpixel.shortpixel-notice.notice-info {
|
||||
border-left-color: #72aee6;
|
||||
}
|
||||
|
||||
/* In-view notice ( not on top, between the options ) - styled after WP notice */
|
||||
.view-notice {
|
||||
box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.1);
|
||||
border: 4px solid #fff;
|
||||
padding: 1px 12px;
|
||||
}
|
||||
.view-notice p {
|
||||
margin: 1em 0 !important;
|
||||
}
|
||||
.view-notice.warning {
|
||||
border-left-color: #ffb900;
|
||||
}
|
||||
|
||||
.view-notice-row {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=notices.css.map */
|
@ -0,0 +1 @@
|
||||
{"version":3,"sourceRoot":"","sources":["notices.scss"],"names":[],"mappings":"AACA;EAGC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EAEC;;AACA;EACC;EACA;;AAED;EAEC;EACA;EACA;;AAKA;EAEE;EACA;EACA;;AAEF;EAEE;;AAGH;EAEC;;AAED;EAEC;;AAED;EAEC;;AAED;EAEE;;;AAIJ;AACA;EAGE;EACA;EAEA;;AACA;EACE;;AAEF;EAEE;;;AAIJ;EAEE","file":"notices.css"}
|
@ -0,0 +1,83 @@
|
||||
|
||||
.shortpixel.shortpixel-notice
|
||||
{
|
||||
|
||||
min-height: 75px;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
padding: 1px 12px;
|
||||
box-shadow: 0 1px 1px rgba(0,0,0,0.04);
|
||||
border: 1px solid #c3c4c7;
|
||||
margin: 15px 0;
|
||||
border-left-width: 4px;
|
||||
border-left-color: #72aee6;
|
||||
position: relative;
|
||||
|
||||
span
|
||||
{
|
||||
vertical-align: middle;
|
||||
&.icon {
|
||||
margin: 0 25px 0 0;
|
||||
width: 80px;
|
||||
}
|
||||
&.content
|
||||
{
|
||||
padding: 8px 0;
|
||||
word-wrap: break-word;
|
||||
overflow: hidden;
|
||||
//display: flex; // magically fixes verticality issues
|
||||
}
|
||||
}
|
||||
|
||||
img
|
||||
{
|
||||
display:inline-block;
|
||||
margin: 0 25px 0 0;
|
||||
max-height: 50px;
|
||||
}
|
||||
.notice-dismiss
|
||||
{
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
&.notice-success
|
||||
{
|
||||
border-left-color: #00a32a;
|
||||
}
|
||||
&.notice-warning
|
||||
{
|
||||
border-left-color: #dba617;
|
||||
}
|
||||
&.notice-error
|
||||
{
|
||||
border-left-color: #ff0000;
|
||||
}
|
||||
&.notice-info
|
||||
{
|
||||
border-left-color: #72aee6;
|
||||
}
|
||||
}
|
||||
|
||||
/* In-view notice ( not on top, between the options ) - styled after WP notice */
|
||||
.view-notice
|
||||
{
|
||||
|
||||
box-shadow: 0 1px 1px 0 rgba( 0, 0, 0, 0.1 );
|
||||
border: 4px solid #fff;
|
||||
|
||||
padding: 1px 12px;
|
||||
p {
|
||||
margin: 1em 0 !important;
|
||||
}
|
||||
&.warning
|
||||
{
|
||||
border-left-color: #ffb900;
|
||||
}
|
||||
}
|
||||
|
||||
.view-notice-row
|
||||
{
|
||||
display: none;
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "shortpixel/replacer",
|
||||
"description": "Content Replacer",
|
||||
"version": 1.1,
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Bas",
|
||||
"email": "bas@weblogmechanic.com"
|
||||
}
|
||||
],
|
||||
"minimum-stability": "dev",
|
||||
"require": {},
|
||||
"autoload": {
|
||||
"psr-4": { "ShortPixel\\Replacer\\" : "src" }
|
||||
}
|
||||
}
|
@ -0,0 +1,178 @@
|
||||
<?php
|
||||
namespace ShortPixel\Replacer\Libraries\Unserialize;
|
||||
|
||||
|
||||
/**
|
||||
* Worker implementation for identifying and skipping false-positives
|
||||
* not to be substituted - like nested serializations in string literals.
|
||||
*
|
||||
* @internal This class should only be used by \Brumann\Polyfill\Unserialize
|
||||
*/
|
||||
final class DisallowedClassesSubstitutor
|
||||
{
|
||||
const PATTERN_STRING = '#s:(\d+):(")#';
|
||||
const PATTERN_OBJECT = '#(^|;)O:\d+:"([^"]*)":(\d+):\{#';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $serialized;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private $allowedClasses;
|
||||
|
||||
/**
|
||||
* Each array item consists of `[<offset-start>, <offset-end>]` and
|
||||
* marks start and end positions of items to be ignored.
|
||||
*
|
||||
* @var array[]
|
||||
*/
|
||||
private $ignoreItems = array();
|
||||
|
||||
/**
|
||||
* @param string $serialized
|
||||
* @param string[] $allowedClasses
|
||||
*/
|
||||
public function __construct($serialized, array $allowedClasses)
|
||||
{
|
||||
$this->serialized = $serialized;
|
||||
$this->allowedClasses = $allowedClasses;
|
||||
|
||||
$this->buildIgnoreItems();
|
||||
$this->substituteObjects();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getSubstitutedSerialized()
|
||||
{
|
||||
return $this->serialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies items to be ignored - like nested serializations in string literals.
|
||||
*/
|
||||
private function buildIgnoreItems()
|
||||
{
|
||||
$offset = 0;
|
||||
while (preg_match(self::PATTERN_STRING, $this->serialized, $matches, PREG_OFFSET_CAPTURE, $offset)) {
|
||||
$length = (int)$matches[1][0]; // given length in serialized data (e.g. `s:123:"` --> 123)
|
||||
$start = $matches[2][1]; // offset position of quote character
|
||||
$end = $start + $length + 1;
|
||||
$offset = $end + 1;
|
||||
|
||||
// serialized string nested in outer serialized string
|
||||
if ($this->ignore($start, $end)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->ignoreItems[] = array($start, $end);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Substitutes disallowed object class names and respects items to be ignored.
|
||||
*/
|
||||
private function substituteObjects()
|
||||
{
|
||||
$offset = 0;
|
||||
while (preg_match(self::PATTERN_OBJECT, $this->serialized, $matches, PREG_OFFSET_CAPTURE, $offset)) {
|
||||
$completeMatch = (string)$matches[0][0];
|
||||
$completeLength = strlen($completeMatch);
|
||||
$start = $matches[0][1];
|
||||
$end = $start + $completeLength;
|
||||
$leftBorder = (string)$matches[1][0];
|
||||
$className = (string)$matches[2][0];
|
||||
$objectSize = (int)$matches[3][0];
|
||||
$offset = $end + 1;
|
||||
|
||||
// class name is actually allowed - skip this item
|
||||
if (in_array($className, $this->allowedClasses, true)) {
|
||||
continue;
|
||||
}
|
||||
// serialized object nested in outer serialized string
|
||||
if ($this->ignore($start, $end)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$incompleteItem = $this->sanitizeItem($className, $leftBorder, $objectSize);
|
||||
$incompleteItemLength = strlen($incompleteItem);
|
||||
$offset = $start + $incompleteItemLength + 1;
|
||||
|
||||
$this->replace($incompleteItem, $start, $end);
|
||||
$this->shift($end, $incompleteItemLength - $completeLength);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces sanitized object class names in serialized data.
|
||||
*
|
||||
* @param string $replacement Sanitized object data
|
||||
* @param int $start Start offset in serialized data
|
||||
* @param int $end End offset in serialized data
|
||||
*/
|
||||
private function replace($replacement, $start, $end)
|
||||
{
|
||||
$this->serialized = substr($this->serialized, 0, $start)
|
||||
. $replacement . substr($this->serialized, $end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether given offset positions should be ignored.
|
||||
*
|
||||
* @param int $start
|
||||
* @param int $end
|
||||
* @return bool
|
||||
*/
|
||||
private function ignore($start, $end)
|
||||
{
|
||||
foreach ($this->ignoreItems as $ignoreItem) {
|
||||
if ($ignoreItem[0] <= $start && $ignoreItem[1] >= $end) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shifts offset positions of ignore items by `$size`.
|
||||
* This is necessary whenever object class names have been
|
||||
* substituted which have a different length than before.
|
||||
*
|
||||
* @param int $offset
|
||||
* @param int $size
|
||||
*/
|
||||
private function shift($offset, $size)
|
||||
{
|
||||
foreach ($this->ignoreItems as &$ignoreItem) {
|
||||
// only focus on items starting after given offset
|
||||
if ($ignoreItem[0] < $offset) {
|
||||
continue;
|
||||
}
|
||||
$ignoreItem[0] += $size;
|
||||
$ignoreItem[1] += $size;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes object class item.
|
||||
*
|
||||
* @param string $className
|
||||
* @param int $leftBorder
|
||||
* @param int $objectSize
|
||||
* @return string
|
||||
*/
|
||||
private function sanitizeItem($className, $leftBorder, $objectSize)
|
||||
{
|
||||
return sprintf(
|
||||
'%sO:22:"__PHP_Incomplete_Class":%d:{s:27:"__PHP_Incomplete_Class_Name";%s',
|
||||
$leftBorder,
|
||||
$objectSize + 1, // size of object + 1 for added string
|
||||
\serialize($className)
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016-2019 Denis Brumann
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -0,0 +1,153 @@
|
||||
Polyfill unserialize [](https://travis-ci.org/dbrumann/polyfill-unserialize)
|
||||
===
|
||||
|
||||
Backports unserialize options introduced in PHP 7.0 to older PHP versions.
|
||||
This was originally designed as a Proof of Concept for Symfony Issue
|
||||
[#21090](https://github.com/symfony/symfony/pull/21090).
|
||||
|
||||
You can use this package in projects that rely on PHP versions older than
|
||||
PHP 7.0. In case you are using PHP 7.0+ the original `unserialize()` will be
|
||||
used instead.
|
||||
|
||||
From the [documentation](https://secure.php.net/manual/en/function.unserialize.php):
|
||||
|
||||
> **Warning**
|
||||
>
|
||||
> Do not pass untrusted user input to unserialize() regardless of the options
|
||||
> value of allowed_classes. Unserialization can result in code being loaded and
|
||||
> executed due to object instantiation and autoloading, and a malicious user
|
||||
> may be able to exploit this. Use a safe, standard data interchange format
|
||||
> such as JSON (via json_decode() and json_encode()) if you need to pass
|
||||
> serialized data to the user.
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
- PHP 5.3+
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
You can install this package via composer:
|
||||
|
||||
```bash
|
||||
composer require brumann/polyfill-unserialize "^2.0"
|
||||
```
|
||||
|
||||
Older versions
|
||||
--------------
|
||||
|
||||
You can find the most recent 1.x versions in the branch with the same name:
|
||||
|
||||
* [dbrumann/polyfill-unserialize/tree/1.x](https://github.com/dbrumann/polyfill-unserialize/tree/1.x)
|
||||
|
||||
Upgrading
|
||||
---------
|
||||
|
||||
Upgrading from 1.x to 2.0 should be seamless and require no changes to code
|
||||
using the library. There are no changes to the public API, i.e. the names for
|
||||
classes, methods and arguments as well as argument order and types remain the
|
||||
same. Version 2.x uses a completely different approach for substituting
|
||||
disallowed classes, which is why we chose to use a new major release to prevent
|
||||
issues from unknown side effects in existing installations.
|
||||
|
||||
Known Issues
|
||||
------------
|
||||
|
||||
There is a mismatch in behavior when `allowed_classes` in `$options` is not
|
||||
of the correct type (array or boolean). PHP 7.0 will not issue a warning that
|
||||
an invalid type was provided. This library will trigger a warning, similar to
|
||||
the one PHP 7.1+ will raise and then continue, assuming `false` to make sure
|
||||
no classes are deserialized by accident.
|
||||
|
||||
Tests
|
||||
-----
|
||||
|
||||
You can run the test suite using PHPUnit. It is intentionally not bundled as
|
||||
dev dependency to make sure this package has the lowest restrictions on the
|
||||
implementing system as possible.
|
||||
|
||||
Please read the [PHPUnit Manual](https://phpunit.de/manual/current/en/installation.html)
|
||||
for information how to install it on your system.
|
||||
|
||||
Please make sure to pick a compatible version. If you use PHP 5.6 you should
|
||||
use PHPUnit 5.7.27 and for older PHP versions you should use PHPUnit 4.8.36.
|
||||
Older versions of PHPUnit might not support namespaces, meaning they will not
|
||||
work with the tests. Newer versions only support PHP 7.0+, where this library
|
||||
is not needed anymore.
|
||||
|
||||
You can run the test suite as follows:
|
||||
|
||||
```bash
|
||||
phpunit -c phpunit.xml.dist tests/
|
||||
```
|
||||
|
||||
Contributing
|
||||
------------
|
||||
|
||||
This package is considered feature complete. As such I will likely not update
|
||||
it unless there are security issues.
|
||||
|
||||
Should you find any bugs or have questions, feel free to submit an Issue or a
|
||||
Pull Request on GitHub.
|
||||
|
||||
Development setup
|
||||
-----------------
|
||||
|
||||
This library contains a docker setup for development purposes. This allows
|
||||
running the code on an older PHP version without having to install it locally.
|
||||
|
||||
You can use the setup as follows:
|
||||
|
||||
1. Go into the project directory
|
||||
|
||||
1. Build the docker image
|
||||
|
||||
```
|
||||
docker build -t polyfill-unserialize .
|
||||
```
|
||||
|
||||
This will download a debian/jessie container with PHP 5.6 installed. Then
|
||||
it will download an appropriate version of phpunit for this PHP version.
|
||||
It will also download composer. It will set the working directory to `/opt/app`.
|
||||
The resulting image is tagged as `polyfill-unserialize`, which is the name
|
||||
we will refer to, when running the container.
|
||||
|
||||
1. You can then run a container based on the image, which will run your tests
|
||||
|
||||
```
|
||||
docker run -it --rm --name polyfill-unserialize-dev -v "$PWD":/opt/app polyfill-unserialize
|
||||
```
|
||||
|
||||
This will run a docker container based on our previously built image.
|
||||
The container will automatically be removed after phpunit finishes.
|
||||
We name the image `polyfill-unserialize-dev`. This makes sure only one
|
||||
instance is running and that we can easily identify a running container by
|
||||
its name, e.g. in order to remove it manually.
|
||||
We mount our current directory into the container's working directory.
|
||||
This ensures that tests run on our current project's state.
|
||||
|
||||
You can repeat the final step as often as you like in order to run the tests.
|
||||
The output should look something like this:
|
||||
|
||||
```bash
|
||||
dbr:polyfill-unserialize/ (improvement/dev_setup*) $ docker run -it --rm --name polyfill-unserialize-dev -v "$PWD":/opt/app polyfill-unserialize
|
||||
Loading composer repositories with package information
|
||||
Installing dependencies (including require-dev) from lock file
|
||||
Nothing to install or update
|
||||
Generating autoload files
|
||||
PHPUnit 5.7.27 by Sebastian Bergmann and contributors.
|
||||
|
||||
...................... 22 / 22 (100%)
|
||||
|
||||
Time: 167 ms, Memory: 13.25MB
|
||||
|
||||
OK (22 tests, 31 assertions)
|
||||
```
|
||||
|
||||
When you are done working on the project you can free up disk space by removing
|
||||
the initially built image:
|
||||
|
||||
```
|
||||
docker image rm polyfill-unserialize
|
||||
```
|
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
namespace ShortPixel\Replacer\Libraries\Unserialize;
|
||||
|
||||
// Taken from : https://github.com/dbrumann/polyfill-unserialize/
|
||||
final class Unserialize
|
||||
{
|
||||
/**
|
||||
* @see https://secure.php.net/manual/en/function.unserialize.php
|
||||
*
|
||||
* @param string $serialized Serialized data
|
||||
* @param array $options Associative array containing options
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public static function unserialize($serialized, array $options = array())
|
||||
{
|
||||
if (PHP_VERSION_ID >= 70000) {
|
||||
return \unserialize($serialized, $options);
|
||||
}
|
||||
if (!array_key_exists('allowed_classes', $options) || true === $options['allowed_classes']) {
|
||||
return \unserialize($serialized);
|
||||
}
|
||||
$allowedClasses = $options['allowed_classes'];
|
||||
if (false === $allowedClasses) {
|
||||
$allowedClasses = array();
|
||||
}
|
||||
if (!is_array($allowedClasses)) {
|
||||
$allowedClasses = array();
|
||||
trigger_error(
|
||||
'unserialize(): allowed_classes option should be array or boolean',
|
||||
E_USER_WARNING
|
||||
);
|
||||
}
|
||||
|
||||
$worker = new DisallowedClassesSubstitutor($serialized, $allowedClasses);
|
||||
|
||||
return \unserialize($worker->getSubstitutedSerialized());
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
namespace ShortPixel\Replacer\Modules;
|
||||
|
||||
class Elementor
|
||||
{
|
||||
private static $instance;
|
||||
|
||||
protected $queryKey = 'elementor';
|
||||
|
||||
public static function getInstance()
|
||||
{
|
||||
if (is_null(self::$instance))
|
||||
self::$instance = new Elementor();
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
if ($this->elementor_is_active()) // elementor is active
|
||||
{
|
||||
add_filter('shortpixel/replacer/custom_replace_query', array($this, 'addElementor'), 10, 4); // custom query for elementor \ // problem
|
||||
// @todo Fix this for SPIO
|
||||
//add_action('enable-media-replace-upload-done', array($this, 'removeCache') );
|
||||
}
|
||||
}
|
||||
|
||||
public function addElementor($items, $base_url, $search_urls, $replace_urls)
|
||||
{
|
||||
$base_url = $this->addSlash($base_url);
|
||||
$el_search_urls = $search_urls; //array_map(array($this, 'addslash'), $search_urls);
|
||||
$el_replace_urls = $replace_urls; //array_map(array($this, 'addslash'), $replace_urls);
|
||||
$items[$this->queryKey] = array('base_url' => $base_url, 'search_urls' => $el_search_urls, 'replace_urls' => $el_replace_urls);
|
||||
return $items;
|
||||
}
|
||||
|
||||
public function addSlash($value)
|
||||
{
|
||||
global $wpdb;
|
||||
$value= ltrim($value, '/'); // for some reason the left / isn't picked up by Mysql.
|
||||
$value= str_replace('/', '\/', $value);
|
||||
$value = $wpdb->esc_like(($value)); //(wp_slash) / str_replace('/', '\/', $value);
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
protected function elementor_is_active()
|
||||
{
|
||||
$bool = false;
|
||||
|
||||
if (defined('ELEMENTOR_VERSION'))
|
||||
$bool = true;
|
||||
|
||||
return apply_filters('emr/externals/elementor_is_active', $bool); // manual override
|
||||
}
|
||||
|
||||
public function removeCache()
|
||||
{
|
||||
\Elementor\Plugin::$instance->files_manager->clear_cache();
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
namespace ShortPixel\Replacer\Modules;
|
||||
// Note! This class doubles as integration for both Visual Composer *and* WP Bakery. They both need URLENCODE.
|
||||
class WpBakery
|
||||
{
|
||||
private static $instance;
|
||||
|
||||
protected $queryKey = 'wpbakery';
|
||||
|
||||
public static function getInstance()
|
||||
{
|
||||
if (is_null(self::$instance))
|
||||
self::$instance = new WpBakery();
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
if ($this->bakery_is_active()) // elementor is active
|
||||
{
|
||||
add_filter('shortpixel/replacer/custom_replace_query', array($this, 'addURLEncoded'), 10, 4); // custom query for elementor \ // problem
|
||||
}
|
||||
}
|
||||
|
||||
public function addUrlEncoded($items, $base_url, $search_urls, $replace_urls)
|
||||
{
|
||||
$base_url = $this->addEncode($base_url);
|
||||
$el_search_urls = array_map(array($this, 'addEncode'), $search_urls);
|
||||
$el_replace_urls = array_map(array($this, 'addEncode'), $replace_urls);
|
||||
$items[$this->queryKey] = array('base_url' => $base_url, 'search_urls' => $el_search_urls, 'replace_urls' => $el_replace_urls);
|
||||
return $items;
|
||||
}
|
||||
|
||||
public function addEncode($value)
|
||||
{
|
||||
return urlencode($value);
|
||||
}
|
||||
|
||||
protected function bakery_is_active()
|
||||
{
|
||||
$bool = false;
|
||||
|
||||
// did_action -> wpbakery , VCV_version -> detect Visual Composer
|
||||
if (did_action('vc_plugins_loaded') || defined('VCV_VERSION'))
|
||||
$bool = true;
|
||||
|
||||
return apply_filters('emr/externals/urlencode_is_active', $bool); // manual override
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
namespace ShortPixel\Replacer\Modules;
|
||||
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
|
||||
|
||||
// Integration to reset indexes of Yoast (used for Og:image) when something is converted.
|
||||
class YoastSeo
|
||||
{
|
||||
|
||||
private $yoastTable;
|
||||
private static $instance;
|
||||
|
||||
public static function getInstance()
|
||||
{
|
||||
if (is_null(self::$instance))
|
||||
self::$instance = new YoastSeo();
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
if (true === $this->yoast_is_active()) // elementor is active
|
||||
{
|
||||
global $wpdb;
|
||||
$this->yoastTable = $wpdb->prefix . 'yoast_indexable';
|
||||
|
||||
add_action('shortpixel/replacer/replace_urls', array($this, 'removeIndexes'),10,2);
|
||||
}
|
||||
}
|
||||
|
||||
public function removeIndexes($search_urls, $replace_urls)
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$sql = 'DELETE FROM ' . $this->yoastTable . ' WHERE ';
|
||||
$prepare = array();
|
||||
|
||||
$base = isset($search_urls['base']) ? $search_urls['base'] : null;
|
||||
$file = isset($search_urls['file']) ? $search_urls['file'] : null;
|
||||
|
||||
if (! is_null($base))
|
||||
{
|
||||
$querySQL = $sql . ' twitter_image like %s or open_graph_image like %s ';
|
||||
$querySQL = $wpdb->prepare($querySQL, '%' . $base . '%', '%' . $base . '%');
|
||||
|
||||
$wpdb->query($querySQL);
|
||||
}
|
||||
|
||||
if (! is_null($file))
|
||||
{
|
||||
$querySQL = $sql . ' twitter_image like %s or open_graph_image like %s ';
|
||||
$querySQL = $wpdb->prepare($querySQL, '%' . $file . '%', '%' . $file . '%');
|
||||
|
||||
$wpdb->query($querySQL);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected function yoast_is_active()
|
||||
{
|
||||
if (defined('WPSEO_VERSION'))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,527 @@
|
||||
<?php
|
||||
namespace ShortPixel\Replacer;
|
||||
|
||||
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
|
||||
use ShortPixel\Replacer\Libraries\Unserialize\Unserialize;
|
||||
|
||||
/** Module: Replacer.
|
||||
*
|
||||
* - Able to replace across database
|
||||
* - Only replace thumbnails feature dependent on media library
|
||||
* - Support for page builders / strange data
|
||||
*/
|
||||
|
||||
class Replacer
|
||||
{
|
||||
|
||||
protected $source_url;
|
||||
protected $target_url;
|
||||
protected $source_metadata = array();
|
||||
protected $target_metadata = array();
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
//$this->source_url = $source_url;
|
||||
///$this->target_url = $target_url;
|
||||
$this->loadFormats();
|
||||
}
|
||||
|
||||
// Load classes that handle alternative formats that can occur in the metadata / post data.
|
||||
protected function loadFormats()
|
||||
{
|
||||
Modules\Elementor::getInstance();
|
||||
Modules\WpBakery::getInstance();
|
||||
Modules\YoastSeo::getInstance();
|
||||
}
|
||||
|
||||
public function setSource($url)
|
||||
{
|
||||
$this->source_url = $url;
|
||||
}
|
||||
|
||||
public function getSource()
|
||||
{
|
||||
return $this->source_url;
|
||||
}
|
||||
|
||||
public function setTarget($url)
|
||||
{
|
||||
$this->target_url = $url;
|
||||
}
|
||||
|
||||
public function getTarget()
|
||||
{
|
||||
return $this->target_url;
|
||||
}
|
||||
|
||||
public function setSourceMeta($meta)
|
||||
{
|
||||
$this->source_metadata = $meta;
|
||||
}
|
||||
|
||||
public function setTargetMeta($meta)
|
||||
{
|
||||
$this->target_metadata = $meta;
|
||||
}
|
||||
|
||||
public function replace($args = array())
|
||||
{
|
||||
if (is_null($this->source_url) || is_null($this->target_url))
|
||||
{
|
||||
Log::addWarn('Replacer called without source or target ');
|
||||
return false;
|
||||
}
|
||||
$defaults = array(
|
||||
'thumbnails_only' => false,
|
||||
);
|
||||
|
||||
$errors = array();
|
||||
$args = wp_parse_args($args, $defaults);
|
||||
|
||||
// Search-and-replace filename in post database
|
||||
// @todo Check this with scaled images.
|
||||
$base_url = parse_url($this->source_url, PHP_URL_PATH);// emr_get_match_url( $this->source_url);
|
||||
$base_url = str_replace('.' . pathinfo($base_url, PATHINFO_EXTENSION), '', $base_url);
|
||||
|
||||
/** Fail-safe if base_url is a whole directory, don't go search/replace */
|
||||
if (is_dir($base_url))
|
||||
{
|
||||
Log::addError('Search Replace tried to replace to directory - ' . $base_url);
|
||||
$errors[] = __('Fail Safe :: Source Location seems to be a directory.', 'enable-media-replace');
|
||||
return $errors;
|
||||
}
|
||||
|
||||
if (strlen(trim($base_url)) == 0)
|
||||
{
|
||||
Log::addError('Current Base URL emtpy - ' . $base_url);
|
||||
$errors[] = __('Fail Safe :: Source Location returned empty string. Not replacing content','enable-media-replace');
|
||||
return $errors;
|
||||
}
|
||||
|
||||
// get relurls of both source and target.
|
||||
$urls = $this->getRelativeURLS();
|
||||
|
||||
|
||||
if ($args['thumbnails_only'])
|
||||
{
|
||||
foreach($urls as $side => $data)
|
||||
{
|
||||
if (isset($data['base']))
|
||||
{
|
||||
unset($urls[$side]['base']);
|
||||
}
|
||||
if (isset($data['file']))
|
||||
{
|
||||
unset($urls[$side]['file']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$search_urls = $urls['source'];
|
||||
$replace_urls = $urls['target'];
|
||||
|
||||
/* If the replacement is much larger than the source, there can be more thumbnails. This leads to disbalance in the search/replace arrays.
|
||||
Remove those from the equation. If the size doesn't exist in the source, it shouldn't be in use either */
|
||||
foreach($replace_urls as $size => $url)
|
||||
{
|
||||
if (! isset($search_urls[$size]))
|
||||
{
|
||||
Log::addDebug('Dropping size ' . $size . ' - not found in source urls');
|
||||
unset($replace_urls[$size]);
|
||||
}
|
||||
}
|
||||
|
||||
Log::addDebug('Source', $this->source_metadata);
|
||||
Log::addDebug('Target', $this->target_metadata);
|
||||
/* If on the other hand, some sizes are available in source, but not in target, try to replace them with something closeby. */
|
||||
foreach($search_urls as $size => $url)
|
||||
{
|
||||
if (! isset($replace_urls[$size]))
|
||||
{
|
||||
$closest = $this->findNearestSize($size);
|
||||
if ($closest)
|
||||
{
|
||||
$sourceUrl = $search_urls[$size];
|
||||
$baseurl = trailingslashit(str_replace(wp_basename($sourceUrl), '', $sourceUrl));
|
||||
Log::addDebug('Nearest size of source ' . $size . ' for target is ' . $closest);
|
||||
$replace_urls[$size] = $baseurl . $closest;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log::addDebug('Unset size ' . $size . ' - no closest found in source');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* If source and target are the same, remove them from replace. This happens when replacing a file with same name, and +/- same dimensions generated.
|
||||
|
||||
After previous loops, for every search there should be a replace size.
|
||||
*/
|
||||
foreach($search_urls as $size => $url)
|
||||
{
|
||||
$replace_url = isset($replace_urls[$size]) ? $replace_urls[$size] : false;
|
||||
if ($url == $replace_url) // if source and target as the same, no need for replacing.
|
||||
{
|
||||
unset($search_urls[$size]);
|
||||
unset($replace_urls[$size]);
|
||||
}
|
||||
}
|
||||
|
||||
// If the two sides are disbalanced, the str_replace part will cause everything that has an empty replace counterpart to replace it with empty. Unwanted.
|
||||
if (count($search_urls) !== count($replace_urls))
|
||||
{
|
||||
Log::addError('Unbalanced Replace Arrays, aborting', array($search_urls, $replace_urls, count($search_urls), count($replace_urls) ));
|
||||
$errors[] = __('There was an issue with updating your image URLS: Search and replace have different amount of values. Aborting updating thumbnails', 'enable-media-replace');
|
||||
return $errors;
|
||||
}
|
||||
|
||||
Log::addDebug('Doing meta search and replace -', array($search_urls, $replace_urls) );
|
||||
Log::addDebug('Searching with BaseuRL ' . $base_url);
|
||||
|
||||
do_action('shortpixel/replacer/replace_urls', $search_urls, $replace_urls);
|
||||
$updated = 0;
|
||||
|
||||
$updated += $this->doReplaceQuery($base_url, $search_urls, $replace_urls);
|
||||
|
||||
$replaceRuns = apply_filters('shortpixel/replacer/custom_replace_query', array(), $base_url, $search_urls, $replace_urls);
|
||||
Log::addDebug("REPLACE RUNS", $replaceRuns);
|
||||
foreach($replaceRuns as $component => $run)
|
||||
{
|
||||
Log::addDebug('Running additional replace for : '. $component, $run);
|
||||
$updated += $this->doReplaceQuery($run['base_url'], $run['search_urls'], $run['replace_urls']);
|
||||
}
|
||||
|
||||
Log::addDebug("Updated Records : " . $updated);
|
||||
return $updated;
|
||||
}
|
||||
|
||||
private function doReplaceQuery($base_url, $search_urls, $replace_urls)
|
||||
{
|
||||
global $wpdb;
|
||||
/* Search and replace in WP_POSTS */
|
||||
// Removed $wpdb->remove_placeholder_escape from here, not compatible with WP 4.8
|
||||
|
||||
$posts_sql = $wpdb->prepare(
|
||||
"SELECT ID, post_content FROM $wpdb->posts WHERE post_status in ('publish', 'future', 'draft', 'pending', 'private')
|
||||
AND post_content LIKE %s",
|
||||
'%' . $base_url . '%');
|
||||
|
||||
$rs = $wpdb->get_results( $posts_sql, ARRAY_A );
|
||||
$number_of_updates = 0;
|
||||
|
||||
if ( ! empty( $rs ) ) {
|
||||
foreach ( $rs AS $rows ) {
|
||||
$number_of_updates = $number_of_updates + 1;
|
||||
// replace old URLs with new URLs.
|
||||
|
||||
$post_content = $rows["post_content"];
|
||||
$post_id = $rows['ID'];
|
||||
$replaced_content = $this->replaceContent($post_content, $search_urls, $replace_urls, false, true);
|
||||
|
||||
if ($replaced_content !== $post_content)
|
||||
{
|
||||
|
||||
// $result = wp_update_post($post_ar);
|
||||
$sql = 'UPDATE ' . $wpdb->posts . ' SET post_content = %s WHERE ID = %d';
|
||||
$sql = $wpdb->prepare($sql, $replaced_content, $post_id);
|
||||
|
||||
$result = $wpdb->query($sql);
|
||||
|
||||
if ($result === false)
|
||||
{
|
||||
Notice::addError('Something went wrong while replacing' . $result->get_error_message() );
|
||||
Log::addError('WP-Error during post update', $result);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
$number_of_updates += $this->handleMetaData($base_url, $search_urls, $replace_urls);
|
||||
return $number_of_updates;
|
||||
}
|
||||
|
||||
private function handleMetaData($url, $search_urls, $replace_urls)
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$meta_options = apply_filters('shortpixel/replacer/metadata_tables', array('post', 'comment', 'term', 'user'));
|
||||
$number_of_updates = 0;
|
||||
|
||||
foreach($meta_options as $type)
|
||||
{
|
||||
switch($type)
|
||||
{
|
||||
case "post": // special case.
|
||||
$sql = 'SELECT meta_id as id, meta_key, meta_value FROM ' . $wpdb->postmeta . '
|
||||
WHERE post_id in (SELECT ID from '. $wpdb->posts . ' where post_status in ("publish", "future", "draft", "pending", "private") ) AND meta_value like %s';
|
||||
$type = 'post';
|
||||
|
||||
$update_sql = ' UPDATE ' . $wpdb->postmeta . ' SET meta_value = %s WHERE meta_id = %d';
|
||||
break;
|
||||
default:
|
||||
$table = $wpdb->{$type . 'meta'}; // termmeta, commentmeta etc
|
||||
|
||||
$meta_id = 'meta_id';
|
||||
if ($type == 'user')
|
||||
$meta_id = 'umeta_id';
|
||||
|
||||
$sql = 'SELECT ' . $meta_id . ' as id, meta_value FROM ' . $table . '
|
||||
WHERE meta_value like %s';
|
||||
|
||||
$update_sql = " UPDATE $table set meta_value = %s WHERE $meta_id = %d ";
|
||||
break;
|
||||
}
|
||||
|
||||
$sql = $wpdb->prepare($sql, '%' . $url . '%');
|
||||
|
||||
// This is a desparate solution. Can't find anyway for wpdb->prepare not the add extra slashes to the query, which messes up the query.
|
||||
// $postmeta_sql = str_replace('[JSON_URL]', $json_url, $postmeta_sql);
|
||||
$rsmeta = $wpdb->get_results($sql, ARRAY_A);
|
||||
|
||||
if (! empty($rsmeta))
|
||||
{
|
||||
foreach ($rsmeta as $row)
|
||||
{
|
||||
$number_of_updates++;
|
||||
$content = $row['meta_value'];
|
||||
|
||||
|
||||
$id = $row['id'];
|
||||
|
||||
$content = $this->replaceContent($content, $search_urls, $replace_urls); //str_replace($search_urls, $replace_urls, $content);
|
||||
|
||||
$prepared_sql = $wpdb->prepare($update_sql, $content, $id);
|
||||
|
||||
Log::addDebug('Update Meta SQl' . $prepared_sql);
|
||||
$result = $wpdb->query($prepared_sql);
|
||||
|
||||
}
|
||||
}
|
||||
} // foreach
|
||||
|
||||
return $number_of_updates;
|
||||
} // function
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Replaces Content across several levels of possible data
|
||||
* @param $content String The Content to replace
|
||||
* @param $search String Search string
|
||||
* @param $replace String Replacement String
|
||||
* @param $in_deep Boolean. This is use to prevent serialization of sublevels. Only pass back serialized from top.
|
||||
* @param $strict_check Boolean . If true, remove all classes from serialization check and fail. This should be done on post_content, not on metadata.
|
||||
*/
|
||||
private function replaceContent($content, $search, $replace, $in_deep = false, $strict_check = false)
|
||||
{
|
||||
//$is_serial = false;
|
||||
if ( true === is_serialized($content))
|
||||
{
|
||||
$serialized_content = $content; // use to return content back if incomplete classes are found, prevent destroying the original information
|
||||
|
||||
if (true === $strict_check)
|
||||
{
|
||||
$args = array('allowed_classes' => false);
|
||||
}
|
||||
else
|
||||
{
|
||||
$args = array('allowed_classes' => true);
|
||||
}
|
||||
|
||||
$content = Unserialize::unserialize($content, $args);
|
||||
// bail directly on incomplete classes. In < PHP 7.2 is_object is false on incomplete objects!
|
||||
if (true === $this->checkIncomplete($content))
|
||||
{
|
||||
return $serialized_content;
|
||||
}
|
||||
}
|
||||
|
||||
$isJson = $this->isJSON($content);
|
||||
|
||||
if ($isJson)
|
||||
{
|
||||
$content = json_decode($content);
|
||||
Log::addDebug('JSon Content', $content);
|
||||
}
|
||||
|
||||
if (is_string($content)) // let's check the normal one first.
|
||||
{
|
||||
$content = apply_filters('shortpixel/replacer/content', $content, $search, $replace);
|
||||
|
||||
$content = str_replace($search, $replace, $content);
|
||||
}
|
||||
elseif (is_wp_error($content)) // seen this.
|
||||
{
|
||||
//return $content; // do nothing.
|
||||
}
|
||||
elseif (is_array($content) ) // array metadata and such.
|
||||
{
|
||||
foreach($content as $index => $value)
|
||||
{
|
||||
$content[$index] = $this->replaceContent($value, $search, $replace, true); //str_replace($value, $search, $replace);
|
||||
if (is_string($index)) // If the key is the URL (sigh)
|
||||
{
|
||||
$index_replaced = $this->replaceContent($index, $search,$replace, true);
|
||||
if ($index_replaced !== $index)
|
||||
$content = $this->change_key($content, array($index => $index_replaced));
|
||||
}
|
||||
}
|
||||
}
|
||||
elseif(is_object($content)) // metadata objects, they exist.
|
||||
{
|
||||
// bail directly on incomplete classes.
|
||||
if (true === $this->checkIncomplete($content))
|
||||
{
|
||||
// if it was serialized, return the original as not to corrupt data.
|
||||
if (isset($serialized_content))
|
||||
{
|
||||
return $serialized_content;
|
||||
}
|
||||
else { // else just return the content.
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
foreach($content as $key => $value)
|
||||
{
|
||||
$content->{$key} = $this->replaceContent($value, $search, $replace, true); //str_replace($value, $search, $replace);
|
||||
}
|
||||
}
|
||||
|
||||
if ($isJson && $in_deep === false) // convert back to JSON, if this was JSON. Different than serialize which does WP automatically.
|
||||
{
|
||||
Log::addDebug('Value was found to be JSON, encoding');
|
||||
// wp-slash -> WP does stripslashes_deep which destroys JSON
|
||||
$content = json_encode($content, JSON_UNESCAPED_SLASHES);
|
||||
Log::addDebug('Content returning', array($content));
|
||||
}
|
||||
elseif($in_deep === false && (is_array($content) || is_object($content)))
|
||||
$content = maybe_serialize($content);
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
private function change_key($arr, $set) {
|
||||
if (is_array($arr) && is_array($set)) {
|
||||
$newArr = array();
|
||||
foreach ($arr as $k => $v) {
|
||||
$key = array_key_exists( $k, $set) ? $set[$k] : $k;
|
||||
$newArr[$key] = is_array($v) ? $this->change_key($v, $set) : $v;
|
||||
}
|
||||
return $newArr;
|
||||
}
|
||||
return $arr;
|
||||
}
|
||||
|
||||
private function getRelativeURLS()
|
||||
{
|
||||
$dataArray = array(
|
||||
'source' => array('url' => $this->source_url, 'metadata' => $this->getFilesFromMetadata($this->source_metadata) ),
|
||||
'target' => array('url' => $this->target_url, 'metadata' => $this->getFilesFromMetadata($this->target_metadata) ),
|
||||
);
|
||||
|
||||
// Log::addDebug('Source Metadata', $this->source_metadata);
|
||||
// Log::addDebug('Target Metadata', $this->target_metadata);
|
||||
|
||||
$result = array();
|
||||
|
||||
foreach($dataArray as $index => $item)
|
||||
{
|
||||
$result[$index] = array();
|
||||
$metadata = $item['metadata'];
|
||||
|
||||
$baseurl = parse_url($item['url'], PHP_URL_PATH);
|
||||
$result[$index]['base'] = $baseurl; // this is the relpath of the mainfile.
|
||||
$baseurl = trailingslashit(str_replace( wp_basename($item['url']), '', $baseurl)); // get the relpath of main file.
|
||||
|
||||
foreach($metadata as $name => $filename)
|
||||
{
|
||||
$result[$index][$name] = $baseurl . wp_basename($filename); // filename can have a path like 19/08 etc.
|
||||
}
|
||||
|
||||
}
|
||||
// Log::addDebug('Relative URLS', $result);
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
private function getFilesFromMetadata($meta)
|
||||
{
|
||||
$fileArray = array();
|
||||
if (isset($meta['file']))
|
||||
$fileArray['file'] = $meta['file'];
|
||||
|
||||
if (isset($meta['sizes']))
|
||||
{
|
||||
foreach($meta['sizes'] as $name => $data)
|
||||
{
|
||||
if (isset($data['file']))
|
||||
{
|
||||
$fileArray[$name] = $data['file'];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $fileArray;
|
||||
}
|
||||
|
||||
/** FindNearestsize
|
||||
* This works on the assumption that when the exact image size name is not available, find the nearest width with the smallest possible difference to impact the site the least.
|
||||
*/
|
||||
private function findNearestSize($sizeName)
|
||||
{
|
||||
|
||||
if (! isset($this->source_metadata['sizes'][$sizeName]) || ! isset($this->target_metadata['width'])) // This can happen with non-image files like PDF.
|
||||
{
|
||||
// Check if metadata-less item is a svg file. Just the main file to replace all thumbnails since SVG's don't need thumbnails.
|
||||
if (strpos($this->target_url, '.svg') !== false)
|
||||
{
|
||||
$svg_file = wp_basename($this->target_url);
|
||||
return $svg_file; // this is the relpath of the mainfile.
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
$old_width = $this->source_metadata['sizes'][$sizeName]['width']; // the width from size not in new image
|
||||
$new_width = $this->target_metadata['width']; // default check - the width of the main image
|
||||
|
||||
$diff = abs($old_width - $new_width);
|
||||
// $closest_file = str_replace($this->relPath, '', $this->newMeta['file']);
|
||||
$closest_file = wp_basename($this->target_metadata['file']); // mainfile as default
|
||||
|
||||
foreach($this->target_metadata['sizes'] as $sizeName => $data)
|
||||
{
|
||||
$thisdiff = abs($old_width - $data['width']);
|
||||
|
||||
if ( $thisdiff < $diff )
|
||||
{
|
||||
$closest_file = $data['file'];
|
||||
if(is_array($closest_file)) { $closest_file = $closest_file[0];} // HelpScout case 709692915
|
||||
if(!empty($closest_file)) {
|
||||
$diff = $thisdiff;
|
||||
$found_metasize = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(empty($closest_file)) return false;
|
||||
|
||||
return $closest_file;
|
||||
}
|
||||
|
||||
/* Check if given content is JSON format. */
|
||||
private function isJSON($content)
|
||||
{
|
||||
if (is_array($content) || is_object($content) || is_null($content))
|
||||
return false; // can never be.
|
||||
|
||||
$json = json_decode($content);
|
||||
return $json && $json != $content;
|
||||
}
|
||||
|
||||
private function checkIncomplete($var)
|
||||
{
|
||||
return ($var instanceof \__PHP_Incomplete_Class);
|
||||
}
|
||||
|
||||
|
||||
} // class
|
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "shortpixel/shortq",
|
||||
"description": "Simple Queue",
|
||||
"version": 1.3,
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Bas",
|
||||
"email": "bas@weblogmechanic.com"
|
||||
}
|
||||
],
|
||||
"minimum-stability": "dev",
|
||||
"require": {},
|
||||
"autoload": {
|
||||
"psr-4": { "ShortPixel\\ShortQ\\" : "src" }
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
namespace ShortPixel\ShortQ\DataProvider;
|
||||
use ShortPixel\ShortQ\Item as Item;
|
||||
|
||||
|
||||
/* DataProvider handles where the data is stored, and retrieval upon queue request
|
||||
*
|
||||
* DataProvider is responsible for creating it's own environment, and cleanup when uninstall is being called.
|
||||
*
|
||||
*/
|
||||
interface DataProvider
|
||||
{
|
||||
|
||||
function __construct($pluginSlug, $queueName);
|
||||
|
||||
//function add($items);
|
||||
function enqueue($items);
|
||||
function dequeue($args); // @return Items removed from queue and set to status. Returns Item Object
|
||||
function alterQueue($changes, $conditions, $operators); // @return Item Count / Boolean . Mass alteration of queue. ( changes, what to change, conditions, basically where statement)
|
||||
function itemUpdate(Item $item, $new_status);
|
||||
function getItem($item_id);
|
||||
function getItems($args); // get items on basis of status / updated date /etc
|
||||
|
||||
|
||||
// Returns number of items left in Queue.
|
||||
function itemCount($status = ShortQ::QSTATUS_WAITING);
|
||||
|
||||
// Sum of a arbitrary number of items set by user.
|
||||
function itemSum($status = ShortQ::QSTATUS_ALL);
|
||||
|
||||
function install($nocheck = false);
|
||||
function uninstall();
|
||||
}
|
@ -0,0 +1,610 @@
|
||||
<?php
|
||||
namespace ShortPixel\ShortQ\DataProvider;
|
||||
use ShortPixel\ShortQ\Item as Item;
|
||||
use ShortPixel\ShortQ\ShortQ as ShortQ;
|
||||
|
||||
|
||||
/* WP Mysql DataProvider
|
||||
*
|
||||
*/
|
||||
class MysqlDataProvider implements DataProvider
|
||||
{
|
||||
protected $qName; // Limit is 30 chars!
|
||||
protected $slug; // Limit is 30 chars!
|
||||
|
||||
protected $table;
|
||||
|
||||
protected $query_size_limit = 10000; // in strlen characters.
|
||||
|
||||
/* Constructor */
|
||||
public function __construct($slug, $qName)
|
||||
{
|
||||
global $wpdb;
|
||||
$this->slug = $slug;
|
||||
$this->qName = $qName;
|
||||
|
||||
$this->table = $wpdb->prefix . 'shortpixel_queue';
|
||||
}
|
||||
|
||||
public function enqueue($items)
|
||||
{
|
||||
global $wpdb;
|
||||
if (! is_array($items))
|
||||
return false;
|
||||
// start higher to allow priority additions easily.
|
||||
$list_order = (10 + $this->itemCount());
|
||||
$now = $this->timestamptoSQL();
|
||||
|
||||
$sql = 'INSERT IGNORE INTO ' . $this->table . ' (queue_name, plugin_slug, value, item_count, list_order, item_id, updated, created) VALUES ';
|
||||
$values = array();
|
||||
foreach ($items as $item)
|
||||
{
|
||||
$item_id = (int) $item->item_id;
|
||||
$item_count = (int) $item->item_count;
|
||||
$value = $item->getRaw('value'); // value;
|
||||
|
||||
$order = (! is_null($item->list_order)) ? $item->list_order : $list_order;
|
||||
|
||||
$values[] = $wpdb->prepare('(%s, %s, %s,%d, %d, %d, %s, %s)', $this->qName, $this->slug, $value, $item_count, $order, $item_id, $now, $now);
|
||||
if (! isset($item->list_order))
|
||||
$list_order++;
|
||||
|
||||
}
|
||||
$sql .= implode( ",\n", $values );
|
||||
$result = $wpdb->query($sql, $values);
|
||||
|
||||
if (! $this->checkQueryOK())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/* Check item consistency and check if items are not already in this queue. Must be unique */
|
||||
protected function prepareItems($items)
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$items_ids = array();
|
||||
foreach($items as $key => $item)
|
||||
{
|
||||
if (isset($item['id']))
|
||||
$items_ids[] = $item['id'];
|
||||
else // no id, no q
|
||||
unset($items[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
/* Dequeue an item (return it) via specific parameters. Sets a new status after grabbing these records.
|
||||
*
|
||||
* @param $args Array
|
||||
numitems - number of records to pull
|
||||
status - Array - the statusses to pull
|
||||
newstatus - To which status said items should be put
|
||||
orderby - how to order the records [not implemented]
|
||||
|
||||
@return Recordset of Items gotten.
|
||||
*/
|
||||
public function dequeue($args = array())
|
||||
{
|
||||
$defaults = array(
|
||||
'numitems' => 1, // pass -1 for all.
|
||||
'status' => ShortQ::QSTATUS_WAITING,
|
||||
'newstatus' => ShortQ::QSTATUS_DONE,
|
||||
'orderby' => 'list_order',
|
||||
'order' => 'ASC',
|
||||
'priority' => false, // array('operator' => '<', 'value' => 10);
|
||||
);
|
||||
|
||||
$args = wp_parse_args($args, $defaults);
|
||||
|
||||
if (is_array($args['status']))
|
||||
$args['status'] = implode(',', $args['status']);
|
||||
|
||||
$items = $this->queryItems(array(
|
||||
'numitems' => $args['numitems'],
|
||||
'status' => $args['status'],
|
||||
'orderby' => $args['orderby'],
|
||||
'order' => $args['order'],
|
||||
'priority' => $args['priority'],
|
||||
));
|
||||
|
||||
$id_array = array_keys($items);
|
||||
|
||||
// Update status if results yielded.
|
||||
if ($args['status'] !== $args['newstatus'] && count($id_array) > 0)
|
||||
{
|
||||
$now = $this->timestamptoSQL();
|
||||
$this->updateRecords(array('status' => $args['newstatus'], 'updated' => $now), array('id' => $id_array));
|
||||
foreach($items as $index => $item)
|
||||
{
|
||||
$item->status = $args['newstatus']; // update status to new situation.
|
||||
$item->updated = $now;
|
||||
$items[$index] = $item;
|
||||
}
|
||||
}
|
||||
if ($args['newstatus'] == ShortQ::QSTATUS_DELETE)
|
||||
{
|
||||
$this->removeRecords(array('status' => ShortQ::QSTATUS_DELETE));
|
||||
}
|
||||
// @todo is Status = QSTATUS_DELETE, remove all records after putting them to this status.
|
||||
|
||||
return array_values($items); // array values resets the id index returns by queryItems
|
||||
}
|
||||
|
||||
private function timestampToSQL($timestamp = 0)
|
||||
{
|
||||
if (! is_numeric($timestamp))
|
||||
return $timestamp; // possible already date;
|
||||
|
||||
if ($timestamp == 0)
|
||||
$timestamp = time();
|
||||
|
||||
$date = date('Y-m-d H:i:s', $timestamp);
|
||||
|
||||
return $date;
|
||||
}
|
||||
|
||||
/*
|
||||
* @return Array
|
||||
*/
|
||||
private function queryItems($args = array())
|
||||
{
|
||||
$defaults = array(
|
||||
'status' => ShortQ::QSTATUS_ALL,
|
||||
'orderby' => 'list_order',
|
||||
'order' => 'ASC',
|
||||
'numitems' => -1,
|
||||
'item_id' => false,
|
||||
'updated' => false, // updated since (Unix Timestamp) ( or array with operator)
|
||||
'priority' => false, // number (or array with operator)
|
||||
);
|
||||
|
||||
$args = wp_parse_args($args, $defaults);
|
||||
|
||||
global $wpdb;
|
||||
$prepare = array();
|
||||
|
||||
$sql = 'SELECT * from ' . $this->table . ' where queue_name = %s and plugin_slug = %s ';
|
||||
$prepare[] = $this->qName;
|
||||
$prepare[] = $this->slug;
|
||||
|
||||
if ($args['status'] <> ShortQ::QSTATUS_ALL)
|
||||
{
|
||||
$sql .= 'and status = %d ';
|
||||
$prepare[] = $args['status'];
|
||||
}
|
||||
|
||||
if ($args['item_id'] !== false && intval($args['item_id']) > 0)
|
||||
{
|
||||
$sql .= 'and item_id = %d ';
|
||||
$prepare[] = intval($args['item_id']);
|
||||
}
|
||||
|
||||
if ($args['updated'])
|
||||
{
|
||||
$operator = '=';
|
||||
|
||||
if (is_array($args['updated']))
|
||||
{
|
||||
$operator = isset($args['updated']['operator']) ? ($args['updated']['operator']) : $operator;
|
||||
$value = isset($args['updated']['value']) ? $args['updated']['value'] : false;
|
||||
}
|
||||
else
|
||||
{
|
||||
$value = $args['updated'];
|
||||
}
|
||||
|
||||
$sql .= 'and updated ' . $operator . ' %s ';
|
||||
$prepare[] = $this->timestamptoSQL($value);
|
||||
}
|
||||
|
||||
if ($args['priority'])
|
||||
{
|
||||
$operator = '=';
|
||||
|
||||
if (is_array($args['priority']))
|
||||
{
|
||||
$operator = isset($args['priority']['operator']) ? ($args['priority']['operator']) : $operator;
|
||||
$value = isset($args['priority']['value']) ? $args['priority']['value'] : false;
|
||||
}
|
||||
else
|
||||
{
|
||||
$value = $args['priority'];
|
||||
}
|
||||
|
||||
$sql .= 'and list_order ' . $operator . ' %d ';
|
||||
$prepare[] = $value;
|
||||
}
|
||||
|
||||
if ($args['orderby'])
|
||||
{
|
||||
$order = (strtoupper($args['order']) == 'ASC') ? 'ASC ' : 'DESC ';
|
||||
$sql .= 'order by ' . $args['orderby'] . ' ' . $order;
|
||||
}
|
||||
|
||||
if ($args['numitems'] > 0)
|
||||
{
|
||||
$sql .= 'limit %d ';
|
||||
$prepare[] = $args['numitems'];
|
||||
}
|
||||
|
||||
$sql = $wpdb->prepare($sql, $prepare);
|
||||
|
||||
$result = $wpdb->get_results($sql, ARRAY_A);
|
||||
|
||||
$items = array();
|
||||
|
||||
foreach($result as $index => $row)
|
||||
{
|
||||
$item = new Item();
|
||||
$id = $row['id'];
|
||||
foreach($row as $name => $value)
|
||||
{
|
||||
if (property_exists($item, $name))
|
||||
{
|
||||
$item->$name = $value;
|
||||
}
|
||||
}
|
||||
$items[$id] = $item;
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/** Updates a set of items from queue without pulling or returning those records.
|
||||
*
|
||||
* @return int Number of Records Updated
|
||||
*/
|
||||
public function alterQueue($data, $fields, $operators)
|
||||
{
|
||||
return $this->updateRecords($data, $fields, $operators);
|
||||
}
|
||||
|
||||
/** Updates one queued item, for instance in case of failing, or status update
|
||||
*
|
||||
* @param $item_id int The Uniq Id of the item to update
|
||||
* @param $field Array An array of fields in key => pair format to be updated.
|
||||
*/
|
||||
public function itemUpdate(Item $item, $fields)
|
||||
{
|
||||
$result = $this->updateRecords($fields, array('item_id' => $item->item_id));
|
||||
if ($result == 1 )
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getItem($item_id)
|
||||
{
|
||||
$items = $this->queryItems(array('item_id' => $item_id));
|
||||
|
||||
if (count($items) == 0)
|
||||
return false;
|
||||
else
|
||||
return array_shift($items);
|
||||
}
|
||||
|
||||
public function getItems($args)
|
||||
{
|
||||
return $this->queryItems($args);
|
||||
}
|
||||
|
||||
/* Counts Items in Database Queue
|
||||
* @param Status Mixed When supplied with ShortQ Status Constant it will count this status, will count all with ShortQ:QSTATUS_ALL.
|
||||
* When given 'countbystatus' it will return an array with ShortQ Status as key and the count as value
|
||||
@return Mixed Either count int, or Array.
|
||||
*/
|
||||
public function itemCount($status = ShortQ::QSTATUS_WAITING)
|
||||
{
|
||||
global $wpdb;
|
||||
if (is_numeric($status) && $status != ShortQ::QSTATUS_ALL)
|
||||
{
|
||||
$sql = 'SELECT count(*) FROM ' . $this->table . ' WHERE queue_name = %s and plugin_slug = %s and status = %d ';
|
||||
$count = $wpdb->get_var($wpdb->prepare($sql, $this->qName, $this->slug, $status));
|
||||
}
|
||||
elseif ($status == ShortQ::QSTATUS_ALL) // full queue, with records from all status.
|
||||
{
|
||||
$sql = 'SELECT count(*) FROM ' . $this->table . ' WHERE queue_name = %s and plugin_slug = %s ';
|
||||
$count = $wpdb->get_var($wpdb->prepare($sql, $this->qName, $this->slug));
|
||||
}
|
||||
elseif ($status == 'countbystatus')
|
||||
{
|
||||
$sql = 'SELECT count(id) as count, status FROM ' . $this->table . ' WHERE queue_name = %s and plugin_slug = %s group by status';
|
||||
$rows = $wpdb->get_results($wpdb->prepare($sql, $this->qName, $this->slug), ARRAY_A);
|
||||
$count = array();
|
||||
|
||||
foreach($rows as $row)
|
||||
{
|
||||
$count[$row['status']] = $row['count'];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (!empty($wpdb->last_error))
|
||||
{
|
||||
$this->handleError($wpdb->last_error);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/* Counts Sum of Items in the Database Queue
|
||||
* @param Status Mixed When supplied with ShortQ Status Constant it will count this status, will count all with ShortQ:QSTATUS_ALL.
|
||||
* When given 'countbystatus' it will return an array with ShortQ Status as key and the count as value
|
||||
@return Mixed Either count int, or Array.
|
||||
*/
|
||||
public function itemSum($status = ShortQ::QSTATUS_WAITING)
|
||||
{
|
||||
global $wpdb;
|
||||
if (is_numeric($status) && $status != ShortQ::QSTATUS_ALL)
|
||||
{
|
||||
$sql = 'SELECT SUM(item_count) FROM ' . $this->table . ' WHERE queue_name = %s and plugin_slug = %s and status = %d ';
|
||||
$count = (int) $wpdb->get_var($wpdb->prepare($sql, $this->qName, $this->slug, $status));
|
||||
}
|
||||
elseif ($status == ShortQ::QSTATUS_ALL) // full queue, with records from all status.
|
||||
{
|
||||
$sql = 'SELECT SUM(item_count) FROM ' . $this->table . ' WHERE queue_name = %s and plugin_slug = %s ';
|
||||
$count = (int) $wpdb->get_var($wpdb->prepare($sql, $this->qName, $this->slug));
|
||||
}
|
||||
elseif ($status == 'countbystatus')
|
||||
{
|
||||
$sql = 'SELECT SUM(item_count) as count, status FROM ' . $this->table . ' WHERE queue_name = %s and plugin_slug = %s group by status';
|
||||
$rows = $wpdb->get_results($wpdb->prepare($sql, $this->qName, $this->slug), ARRAY_A);
|
||||
$count = array();
|
||||
|
||||
foreach($rows as $row)
|
||||
{
|
||||
$count[$row['status']] = (int) $row['count'];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (!empty($wpdb->last_error))
|
||||
{
|
||||
$this->handleError($wpdb->last_error);
|
||||
if ($status == 'countbystatus')
|
||||
return array();
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
|
||||
/** Update records
|
||||
*
|
||||
* @param $Data Array. Data array to change, to WP standards
|
||||
* @param $where Array. Data Array on conditions, to WP standards
|
||||
* @param $operators Array. Maps of Field => Operator to use anything else than standard = in the where query.
|
||||
* @return int Amount of records updates, or null|false
|
||||
*/
|
||||
private function updateRecords($data, $where, $operators = array() )
|
||||
{
|
||||
global $wpdb;
|
||||
$update_sql = 'UPDATE ' . $this->table . ' set updated = %s';
|
||||
if (isset($data['updated']))
|
||||
{
|
||||
$placeholders = array($this->timestamptoSQL($data['updated']));
|
||||
unset($data['updated']);
|
||||
}
|
||||
else
|
||||
$placeholders = array($this->timestamptoSQL());
|
||||
|
||||
// Certain older SQL servers like to auto-update created date, creating a mess.
|
||||
if (! isset($data['created']))
|
||||
{
|
||||
$update_sql .= ', created = created';
|
||||
}
|
||||
|
||||
foreach($data as $field => $value)
|
||||
{
|
||||
$update_sql .= ' ,' . $field . ' = %s ';
|
||||
$placeholders[] = $value;
|
||||
}
|
||||
|
||||
$update_sql .= ' WHERE queue_name = %s and plugin_slug = %s ';
|
||||
$placeholders[] = $this->qName;
|
||||
$placeholders[] = $this->slug;
|
||||
|
||||
foreach ($where as $field => $value)
|
||||
{
|
||||
if (is_array($value))
|
||||
{
|
||||
$vals = implode( ', ', array_fill( 0, count( $value ), '%s' ));
|
||||
$update_sql .= ' AND ' . $field . ' in (' . $vals . ' ) ';
|
||||
$placeholders = array_merge($placeholders, $value);
|
||||
}
|
||||
else {
|
||||
$operator = isset($operators[$field]) ? $operators[$field] : '=';
|
||||
$update_sql .= ' AND ' . $field . ' = %s';
|
||||
$placeholders[] = $value;
|
||||
}
|
||||
}
|
||||
$update_sql = $wpdb->prepare($update_sql, $placeholders);
|
||||
|
||||
$result = $wpdb->query($update_sql);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/** @todo Accept array or ItemIDS to remove
|
||||
* @param $args Array . Array of options:
|
||||
* 'Status' : remove items with selected ShortQ QSTATUS
|
||||
* 'All' : Set to true to remove all from this queue ( sep. argument is safety feature )
|
||||
* 'Item_id' : Delete by this item id
|
||||
* 'Items' : Array of Item ID's.
|
||||
*/
|
||||
public function removeRecords($args)
|
||||
{
|
||||
$defaults = array(
|
||||
'status' => null,
|
||||
'all' => false,
|
||||
'item_id' => null,
|
||||
'items' => null,
|
||||
);
|
||||
|
||||
global $wpdb;
|
||||
$args = wp_parse_args($args, $defaults);
|
||||
|
||||
$data = array($this->qName, $this->slug);
|
||||
$delete_sql = 'DELETE FROM ' . $this->table . ' where queue_name = %s and plugin_slug = %s';
|
||||
|
||||
if (! is_null($args['status']))
|
||||
{
|
||||
$data[] = intval($args['status']);
|
||||
$delete_sql .= ' and status = %s';
|
||||
}
|
||||
elseif (! is_null($args['item_id']))
|
||||
{
|
||||
$data[] = $args['item_id'];
|
||||
$delete_sql .= ' and item_id = %s';
|
||||
}
|
||||
elseif(! is_null($args['items']) && count($args['items']) > 0)
|
||||
{
|
||||
$items = $args['items'];
|
||||
$vals = implode( ', ', array_fill( 0, count( $items ), '%d' ));
|
||||
$delete_sql .= ' AND item_id in (' . $vals . ' ) ';
|
||||
$data = array_merge($data, $items);
|
||||
}
|
||||
elseif ($args['all'] === true)
|
||||
{
|
||||
// do nothing, query already here for full delete.
|
||||
}
|
||||
else {
|
||||
return false; // prevent accidents if all is not set explicitly.
|
||||
}
|
||||
|
||||
$delete_sql = $wpdb->prepare($delete_sql, $data);
|
||||
$result = $wpdb->query($delete_sql);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/** Checks if database table properly exists
|
||||
* https://wordpress.stackexchange.com/questions/220275/wordpress-unit-testing-cannot-create-tables
|
||||
* @return Boolean Yes or no
|
||||
*/
|
||||
private function check()
|
||||
{
|
||||
global $wpdb;
|
||||
$sql = $wpdb->prepare("
|
||||
SHOW TABLES LIKE %s
|
||||
", $this->table);
|
||||
|
||||
$result = intval($wpdb->query($sql));
|
||||
|
||||
if ($result == 0)
|
||||
return false;
|
||||
else {
|
||||
return true;
|
||||
}
|
||||
// if something something, install.
|
||||
}
|
||||
|
||||
public function install($nocheck = false)
|
||||
{
|
||||
if ($nocheck === false && true === $this->check())
|
||||
return true;
|
||||
|
||||
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
|
||||
|
||||
global $wpdb;
|
||||
$prefix = $wpdb->prefix;
|
||||
|
||||
$charset = $wpdb->get_charset_collate();
|
||||
$sql = "CREATE TABLE `" . $this->table . "` (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
queue_name VARCHAR(30) NOT NULL,
|
||||
plugin_slug VARCHAR(30) NOT NULL,
|
||||
status int(11) NOT NULL DEFAULT 0,
|
||||
list_order int(11) NOT NULL,
|
||||
item_id bigint unsigned NOT NULL,
|
||||
item_count INT DEFAULT 1,
|
||||
value longtext NOT NULL,
|
||||
tries int(11) NOT NULL DEFAULT 0,
|
||||
created timestamp ,
|
||||
updated timestamp,
|
||||
PRIMARY KEY (id),
|
||||
KEY queue_name (queue_name),
|
||||
KEY plugin_slug (plugin_slug),
|
||||
KEY status (status),
|
||||
KEY item_id (item_id),
|
||||
KEY list_order (list_order)
|
||||
) $charset; ";
|
||||
|
||||
$result = dbDelta($sql);
|
||||
|
||||
$sql = "SHOW INDEX FROM " . $this->table . " WHERE Key_name = 'uq_" . $prefix . "'";
|
||||
$result = $wpdb->get_results($sql);
|
||||
if (is_null($result) || count($result) == 0)
|
||||
{
|
||||
$sql = 'ALTER TABLE '. $this->table . ' ADD CONSTRAINT UNIQUE uq_' . $prefix . '(plugin_slug,queue_name,item_id)';
|
||||
$wpdb->query($sql);
|
||||
}
|
||||
|
||||
return $this->check();
|
||||
}
|
||||
|
||||
public function uninstall()
|
||||
{
|
||||
global $wpdb;
|
||||
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
|
||||
|
||||
// Check if table exists, if not, return.
|
||||
if (! $this->check())
|
||||
return false;
|
||||
|
||||
$sql = 'SELECT count(*) as cnt FROM ' . $this->table;
|
||||
$records = $wpdb->get_var($sql);
|
||||
|
||||
|
||||
// Don't remove table on any doubt.
|
||||
if (is_null($records) || intval($records) <> 0)
|
||||
return false;
|
||||
|
||||
$sql = ' DROP TABLE IF EXISTS ' . $this->table;
|
||||
|
||||
$wpdb->query($sql);
|
||||
|
||||
return $this->check();
|
||||
}
|
||||
|
||||
private function checkQueryOK($override_check = false)
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
if (!empty($wpdb->last_error))
|
||||
{
|
||||
$this->handleError($wpdb->last_error, $override_check);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function handleError($error, $override_check = false)
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
// check if table is there.
|
||||
if (false === $override_check)
|
||||
{
|
||||
if (false === $this->check())
|
||||
$this->install();
|
||||
}
|
||||
|
||||
// If the error contains something 'unknown' a field might be missing, do a hard DbDelta.
|
||||
if (strpos(strtolower($error), 'unknown') !== false)
|
||||
{
|
||||
$this->install(true);
|
||||
}
|
||||
|
||||
$this->install();
|
||||
|
||||
// @todo Add error log here
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,120 @@
|
||||
<?php
|
||||
namespace ShortPixel\ShortQ;
|
||||
|
||||
/* The Queue Item
|
||||
*
|
||||
* The items must correspond 1-on-1 with Storage Items invoked
|
||||
*/
|
||||
class Item
|
||||
{
|
||||
|
||||
protected $id;
|
||||
protected $created; // as a timestamp
|
||||
protected $updated; // as a timestamp
|
||||
protected $item_id; // the item id of the processor.
|
||||
protected $value; // something of value to the processor
|
||||
protected $item_count = 1; // Amount of items this record represents.
|
||||
protected $json_was_array;
|
||||
protected $status = 0;
|
||||
protected $list_order;
|
||||
protected $tries = 0;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
// Without magic conversion. Used for methods such as database insertion of value
|
||||
public function getRaw($name)
|
||||
{
|
||||
if (isset($this->$name))
|
||||
return $this->$name;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function __get($name)
|
||||
{
|
||||
$value= null;
|
||||
|
||||
switch($name)
|
||||
{
|
||||
case 'value':
|
||||
case 'json_was_array':
|
||||
if ($this->isJson($this->value))
|
||||
{
|
||||
$jsonObj = json_decode($this->value);
|
||||
$this->json_was_array = $jsonObj->was_array;
|
||||
|
||||
if ($name == 'value')
|
||||
{
|
||||
if ($this->json_was_array) // since it's being set after decode, redo this.
|
||||
{ $json_array = json_decode($this->value, $this->json_was_array);
|
||||
$value = $json_array['value'];
|
||||
}
|
||||
else
|
||||
{
|
||||
$value = $jsonObj->value;
|
||||
}
|
||||
}
|
||||
elseif($name = 'json_was_array') // this is an internal item that normally shouldn't be requested.
|
||||
$value = $jsonObj->was_array;
|
||||
}
|
||||
elseif (isset($this->$name))
|
||||
$value = $this->$name;
|
||||
break;
|
||||
default:
|
||||
{
|
||||
if (isset($this->$name))
|
||||
$value = $this->$name;
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function __set($name, $value)
|
||||
{
|
||||
switch($name)
|
||||
{
|
||||
case 'created':
|
||||
case 'updated':
|
||||
if (! is_numeric($value))
|
||||
{
|
||||
$dateObj = \DateTime::createFromFormat('Y-m-d H:i:s', $value);
|
||||
$value = $dateObj->format('U');
|
||||
}
|
||||
$this->$name = $value;
|
||||
break;
|
||||
case 'value':
|
||||
if (is_array($value) || is_object($value))
|
||||
{
|
||||
$this->json_was_array = (is_array($value)) ? true : false;
|
||||
$jsonObj = new \stdClass;
|
||||
$jsonObj->was_array = $this->json_was_array;
|
||||
$jsonObj->value = $value;
|
||||
|
||||
$value = json_encode($jsonObj, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
$this->$name = $value;
|
||||
break;
|
||||
default:
|
||||
if (property_exists($this, $name))
|
||||
$this->$name = $value;
|
||||
break;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function isJson($value)
|
||||
{
|
||||
if (is_int($value) || is_numeric($value)) // solo-integer is not a json but will validate.
|
||||
return false;
|
||||
|
||||
json_decode($value);
|
||||
return (json_last_error() == JSON_ERROR_NONE);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
namespace ShortPixel\ShortQ\Queue;
|
||||
use ShortPixel\ShortQ\Item as Item;
|
||||
|
||||
|
||||
|
||||
/* Needed features
|
||||
- Database / Table agnostic Queue system (as in not dependent on specific ID's).
|
||||
- Check if Queue is in Use / Lock system
|
||||
- Multiple ways to say / restore should be possible
|
||||
- Multiplugin ready
|
||||
*/
|
||||
|
||||
/* Queue is the main iterator for the queue */
|
||||
interface Queue
|
||||
{
|
||||
|
||||
// needs to sync with DataProvider!
|
||||
/* const QSTATUS_WAITING = 0; //Waiting for process
|
||||
const QSTATUS_PULLED = 1;
|
||||
const QSTATUS_INPROCESS = 2; // Doing process now
|
||||
const QSTATUS_DONE = 3; // Is Done.
|
||||
|
||||
const QSTATUS_DELETE = -1; // this is a virtual status. If set to this, will be deleted.
|
||||
const QSTATUS_ERROR = -2;
|
||||
const QSTATUS_FATAL = -3; */
|
||||
//private $name; // queue name
|
||||
|
||||
public function __construct($pluginSlug, $queue_name, $dataProvider);
|
||||
public function setOptions($options); // set options via array, such as how much to pull / type of queue
|
||||
|
||||
/* @param Mixed $items Items, or array of items. */
|
||||
public function addItems($items);
|
||||
public function enqueue();
|
||||
public function withOrder($items, $order); // chained method for adding items with an order. Returns Queue Object
|
||||
public function dequeue($args = array());
|
||||
|
||||
/** Functions for async processing. Process should return item_id, put them on done or fail. */
|
||||
public function itemDone(Item $item);
|
||||
public function itemFailed(Item $item, $fatal = false);
|
||||
public function getItem($item_id);
|
||||
|
||||
// return status object, for app to check what is going on.
|
||||
public function getStatus();
|
||||
|
||||
|
||||
|
||||
public function hasItems();
|
||||
public function itemCount();
|
||||
public function itemSum($status = SHORTQ::QSTATUS_ALL);
|
||||
|
||||
// reset, start fresh
|
||||
public function resetQueue();
|
||||
|
||||
//public function setDataProvider($DataProvider); // DataProvider Object
|
||||
public function uninstall();
|
||||
|
||||
} // class
|
@ -0,0 +1,672 @@
|
||||
<?php
|
||||
namespace ShortPixel\ShortQ\Queue;
|
||||
use ShortPixel\ShortQ\Item as Item;
|
||||
use ShortPixel\ShortQ\Status as Status;
|
||||
use \ShortPixel\ShortQ\ShortQ as ShortQ;
|
||||
|
||||
class WPQ implements Queue
|
||||
{
|
||||
private $statusName = 'shortqwp_';
|
||||
|
||||
protected $qName; // queue Name
|
||||
protected $pSlug; // plugin slug
|
||||
protected $DataProvider;
|
||||
|
||||
protected $status; // the status, the whole status and nothing but.
|
||||
protected $currentStatus; // working status, current queue name.
|
||||
protected $items = array();
|
||||
|
||||
protected $options;
|
||||
|
||||
// statistics and status
|
||||
protected $current_ask = 0;
|
||||
|
||||
/*
|
||||
* @param qName - Name of the Queue requested by parent
|
||||
*/
|
||||
public function __construct($slug, $qName, $DataProvider)
|
||||
{
|
||||
if ($slug == '' || $qName == '' )
|
||||
return false;
|
||||
|
||||
$statusName = $this->statusName . $slug;
|
||||
if (strlen($statusName) >= 64) // max for wp_options
|
||||
$statusName = substr($statusName, 0, 64);
|
||||
|
||||
$this->statusName = $statusName;
|
||||
|
||||
$this->qName = $qName;
|
||||
$this->pSlug = $slug;
|
||||
$this->DataProvider = $DataProvider;
|
||||
|
||||
$this->loadStatus();
|
||||
|
||||
$this->options = new \stdclass;
|
||||
$this->options->numitems = 1; //amount to dequeue
|
||||
$this->options->mode = 'direct'; // direct | wait
|
||||
$this->options->enqueue_limit = 1000; // amount of items to deliver to DataProvider in one go.
|
||||
$this->options->process_timeout = 10000; //How long to wait 'IN_PROCESS' for a retry to happen (until retry_limit)
|
||||
$this->options->retry_limit = 5;
|
||||
$this->options->timeout_recount = 20000; // Time to recount and check stuff from datasource in MS
|
||||
$this->options->is_debug = false;
|
||||
|
||||
}
|
||||
|
||||
public function setOptions($options)
|
||||
{
|
||||
foreach($options as $option => $value)
|
||||
{
|
||||
$this->setOption($option, $value);
|
||||
}
|
||||
}
|
||||
|
||||
public function setOption($name, $value)
|
||||
{
|
||||
if (property_exists($this->options, $name))
|
||||
$this->options->$name = $value;
|
||||
}
|
||||
|
||||
public function getOption($name)
|
||||
{
|
||||
if (property_exists($this->options, $name))
|
||||
return $this->options->$name;
|
||||
}
|
||||
|
||||
/** Prepare items for enqueue, if you want to deliver items in batch, but not flush to storage directly
|
||||
* Every Item needs to have an (item)_id and (item)_value. That's the only thing remote app should be aware of.
|
||||
* @param Array Item Array with fields: id, value [order]
|
||||
* @param bool If status should be updated due to adding items.
|
||||
*
|
||||
*
|
||||
*/
|
||||
public function addItems($items, $updateStatus = true)
|
||||
{
|
||||
foreach($items as $item)
|
||||
{
|
||||
|
||||
if (! isset($item['id']))
|
||||
continue;
|
||||
|
||||
$value = isset($item['value']) ? $item['value'] : '';
|
||||
$itemObj = new Item();
|
||||
$itemObj->item_id = $item['id'];
|
||||
$itemObj->value = $value;
|
||||
|
||||
if (isset($item['item_count']))
|
||||
$itemObj->item_count = intval($item['item_count']);
|
||||
|
||||
|
||||
if (isset($item['order']))
|
||||
$itemObj->list_order = $item['order'];
|
||||
|
||||
$this->items[] = $itemObj;
|
||||
|
||||
}
|
||||
if (count($items) > 0 && true === $updateStatus)
|
||||
{
|
||||
$this->setStatus('preparing', true, false);
|
||||
$this->setStatus('finished', false, false); // can't be finished when adding items.
|
||||
}
|
||||
}
|
||||
|
||||
/** Simple Enqueue.
|
||||
* @param $items Array List of Items to add, see @AddItems
|
||||
* @return int $numItems Number of Items in this Queue [Total]
|
||||
* Note that each addition does a save of option retaining the last_item_id. Ideally add as many items that can fit in memory / time limit constraints
|
||||
*/
|
||||
public function enqueue($items = array() )
|
||||
{
|
||||
if (count($items) > 0)
|
||||
$this->addItems($items);
|
||||
|
||||
$chunks = array_chunk($this->items, $this->options->enqueue_limit );
|
||||
$numitems = $this->getStatus('items');
|
||||
|
||||
|
||||
foreach($chunks as $chunknum => $objItems)
|
||||
{
|
||||
$numitems += $this->DataProvider->enqueue($objItems);
|
||||
|
||||
$last_id = end($objItems)->item_id;
|
||||
$this->setStatus('last_item_id', $last_id); // save this, as a script termination safeguard.
|
||||
}
|
||||
|
||||
$this->items = array(); // empty the item cache after inserting
|
||||
$this->setStatus('items', $numitems, false);
|
||||
$this->saveStatus();
|
||||
|
||||
return $numitems;
|
||||
}
|
||||
|
||||
/** Accepts array of items with a certain priority
|
||||
* Usage: $queue->withOrder($items, $order)->enqueue(); will add items with a specific list order number
|
||||
* @param $items Items Array, see AddItems
|
||||
* @param $order Int List Order number to insert.
|
||||
* @return $Queue This Queue Object
|
||||
*/
|
||||
public function withOrder($items, $order)
|
||||
{
|
||||
foreach($items as $index => $item)
|
||||
{
|
||||
$item['order'] = $order;
|
||||
$items[$index] = $item;
|
||||
}
|
||||
$this->addItems($items);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/* Remove from Queue possible duplicates
|
||||
* Chained function. Removed items from queue
|
||||
* *Note* This should be used with small selections of items, not by default. Only when changes to item are needed, or to status.
|
||||
*/
|
||||
public function withRemoveDuplicates()
|
||||
{
|
||||
|
||||
$item_ids = array();
|
||||
|
||||
foreach($this->items as $item)
|
||||
{
|
||||
$item_ids[] = $item->item_id;
|
||||
}
|
||||
|
||||
$count = $this->DataProvider->removeRecords(array('items' => $item_ids));
|
||||
|
||||
if ($count > 0)
|
||||
$this->setStatusCount('items', -$count );
|
||||
|
||||
// Probabably not the full solution, but this can happen if deleted items were already Dequeued with status Done.
|
||||
if ($this->getStatus('items') <= 0)
|
||||
{
|
||||
$this->resetInternalCounts();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Remove Items directly from queue. Expand this function when required (but use dequeue if possible). For now only support for item_id.
|
||||
public function removeItems($args)
|
||||
{
|
||||
if (isset($args['item_id']))
|
||||
{
|
||||
$this->DataProvider->removeRecords(array('item_id' => $args['item_id'] ));
|
||||
}
|
||||
}
|
||||
|
||||
// Dequeue a record, put it on done in queue.
|
||||
public function dequeue($args = array())
|
||||
{
|
||||
// Check if anything has a timeout, do that first.
|
||||
$this->inProcessTimeout();
|
||||
|
||||
if ($this->currentStatus->get('items') <= 0)
|
||||
{
|
||||
$still_here = $this->checkQueue();
|
||||
// @todo if there is queue todo, update items, and go.
|
||||
if (! $still_here)
|
||||
return array();
|
||||
}
|
||||
|
||||
$newstatus = ($this->options->mode == 'wait') ? ShortQ::QSTATUS_INPROCESS : ShortQ::QSTATUS_DONE;
|
||||
|
||||
$defaults = array(
|
||||
'numitems' => $this->options->numitems,
|
||||
'newstatus' => $newstatus,
|
||||
'onlypriority' => false,
|
||||
);
|
||||
|
||||
$args = wp_parse_args($args, $defaults);
|
||||
|
||||
if ($args['onlypriority'])
|
||||
{
|
||||
$args['priority'] = array('operator' => '<', 'value' => 10);
|
||||
// unset($args['onlypriority']);
|
||||
}
|
||||
|
||||
$items = $this->DataProvider->dequeue($args);
|
||||
|
||||
$itemcount = count($items);
|
||||
|
||||
// @todo Ask dprovder for dequeue
|
||||
// Update item count, average ask, last_run for this process
|
||||
// When Q is empty, reask for item count for DataProvider and check if it the same, if not, update number, continue.
|
||||
if ($itemcount == 0 && $args['onlypriority'] == false)
|
||||
{ // This pieces prevents stalling. If the cached count is wrong, reset it, and if empty already will go to items_left / end queue system. Oterhwise resume.
|
||||
$this->resetInternalCounts();
|
||||
$items = $this->DataProvider->dequeue($args);
|
||||
$itemcount = count($items);
|
||||
}
|
||||
|
||||
$items_left = $this->getStatus('items') - $itemcount;
|
||||
$this->setStatus('items', $items_left , false);
|
||||
|
||||
if ($newstatus == ShortQ::QSTATUS_DONE)
|
||||
$this->setStatusCount('done', $itemcount, false);
|
||||
elseif($newstatus == ShortQ::QSTATUS_INPROCESS)
|
||||
$this->setStatusCount('in_process', $itemcount, false);
|
||||
|
||||
$this->current_ask += $itemcount;
|
||||
|
||||
//$queue['average_ask'] = $this->calcAverageAsk($queue['average_ask']);
|
||||
//$this->updateQueue($queue);
|
||||
$this->setStatus('last_run', time(), false);
|
||||
if (! isset($args['priority']))
|
||||
{
|
||||
$this->setStatus('running', true, false);
|
||||
}
|
||||
|
||||
$this->saveStatus();
|
||||
|
||||
if ($items_left == 0)
|
||||
$this->checkQueue(); // possible need to end it.
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Handles in processTimeOuts
|
||||
if TimeOut is reached:
|
||||
- Reset the status back to waiting
|
||||
- Increment Retries by 1
|
||||
-
|
||||
*/
|
||||
public function inProcessTimeout()
|
||||
{
|
||||
// Not waiting for anything.
|
||||
if (! $this->options->mode == 'wait')
|
||||
return;
|
||||
|
||||
$args = array('status' => ShortQ::QSTATUS_INPROCESS, 'updated' => array('value' => time() - ($this->options->process_timeout/1000), 'operator' => '<='));
|
||||
|
||||
$items = $this->DataProvider->getItems($args);
|
||||
$updated = 0;
|
||||
|
||||
foreach($items as $item)
|
||||
{
|
||||
$item->tries++;
|
||||
if ($item->tries > $this->getOption('retry_limit'))
|
||||
{
|
||||
do_action('shortpixel/modules/wpq/item/timeout', $item);
|
||||
$this->itemFailed($item, true); // fatal fail
|
||||
}
|
||||
else
|
||||
{
|
||||
$updated += $this->DataProvider->itemUpdate($item, array('status' => ShortQ::QSTATUS_WAITING, 'tries' => $item->tries));
|
||||
}
|
||||
}
|
||||
|
||||
if ($updated > 0)
|
||||
{
|
||||
if ($this->options->mode == 'wait')
|
||||
{
|
||||
$this->setStatusCount('in_process',- $updated, false);
|
||||
}
|
||||
|
||||
$this->setStatusCount('items', $updated, true);
|
||||
}
|
||||
|
||||
return $updated;
|
||||
|
||||
}
|
||||
|
||||
public function itemDone(Item $item)
|
||||
{
|
||||
if ($this->options->mode == 'direct')
|
||||
{
|
||||
$this->setStatusCount('items', -1, false);
|
||||
}
|
||||
elseif ($this->options->mode == 'wait')
|
||||
{
|
||||
$this->setStatusCount('in_process',-1, false);
|
||||
}
|
||||
|
||||
$this->setStatusCount('done', 1, false);
|
||||
|
||||
$this->saveStatus();
|
||||
|
||||
$this->DataProvider->itemUpdate($item, array('status' => ShortQ::QSTATUS_DONE));
|
||||
}
|
||||
|
||||
public function itemFailed(Item $item, $fatal = false)
|
||||
{
|
||||
$status = ShortQ::QSTATUS_ERROR;
|
||||
if ($fatal)
|
||||
{
|
||||
$status = ShortQ::QSTATUS_FATAL;
|
||||
$this->setStatusCount('fatal_errors', 1, false );
|
||||
if ($this->options->mode == 'direct')
|
||||
{
|
||||
$this->setStatusCount('items', -1, false);
|
||||
}
|
||||
elseif ($this->options->mode == 'wait')
|
||||
{
|
||||
$this->setStatusCount('in_process',-1, false);
|
||||
}
|
||||
}
|
||||
else
|
||||
$this->setStatusCount('errors', 1, false );
|
||||
|
||||
$item->tries++;
|
||||
$this->DataProvider->itemUpdate($item, array('status' => $status, 'tries' => $item->tries));
|
||||
|
||||
$this->saveStatus();
|
||||
}
|
||||
|
||||
public function updateItemValue(Item $item)
|
||||
{
|
||||
if (!property_exists($item, 'value'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->DataProvider->itemUpdate($item, array('value' => $item->getRaw('value') ));
|
||||
}
|
||||
|
||||
public function getItem($item_id)
|
||||
{
|
||||
return $this->DataProvider->getItem($item_id);
|
||||
|
||||
}
|
||||
|
||||
public function hasItems()
|
||||
{
|
||||
//$status = $this->getQueueStatus();
|
||||
$items = $this->itemCount(); // $status->get('items');
|
||||
if ($items > 0)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
public function itemCount()
|
||||
{
|
||||
// $queue = $this->getQueueStatus();
|
||||
$num = $this->getStatus('items'); //$queue->items;
|
||||
if ($num <= 0)
|
||||
{
|
||||
$this->checkQueue(); // check and update left records before checking on Dprovider.
|
||||
$num = $this->DataProvider->itemCount();
|
||||
$this->setStatus('items', $num);
|
||||
}
|
||||
return (int) $num;
|
||||
}
|
||||
|
||||
public function itemSum($status = ShortQ::QSTATUS_ALL)
|
||||
{
|
||||
$row = $this->DataProvider->itemSum($status);
|
||||
|
||||
if ($status === 'countbystatus')
|
||||
{
|
||||
// check if all status are there. If they are unused, they are not in result.
|
||||
$status_ar = array(ShortQ::QSTATUS_WAITING, ShortQ::QSTATUS_DONE, ShortQ::QSTATUS_INPROCESS, ShortQ::QSTATUS_ERROR, ShortQ::QSTATUS_FATAL);
|
||||
|
||||
foreach($status_ar as $stat)
|
||||
{
|
||||
if (! isset($row[$stat]))
|
||||
$row[$stat] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
/** Function to call when ending a queue process. The purpose is to purge all records and statistics.
|
||||
* Those need to be collected before calling this function. Also the reason this is not done automatically.
|
||||
*/
|
||||
public function resetQueue()
|
||||
{
|
||||
$this->DataProvider->removeRecords(array('all' => true));
|
||||
$this->currentStatus = new Status();
|
||||
$this->saveStatus();
|
||||
}
|
||||
|
||||
public function cleanQueue()
|
||||
{
|
||||
|
||||
$this->DataProvider->removeRecords(array('status' => ShortQ::QSTATUS_DONE));
|
||||
$this->DataProvider->removeRecords(array('status' => ShortQ::QSTATUS_FATAL));
|
||||
$this->resetInternalCounts();
|
||||
}
|
||||
|
||||
/** @todo Users must be able to control preparing / running status controls for resume / play the queue, but possibly not the counts. */
|
||||
public function setStatus($name, $value, $savenow = true)
|
||||
{
|
||||
$r = $this->currentStatus->set($name, $value);
|
||||
$this->currentStatus->set('last_update', time());
|
||||
$bool = true;
|
||||
|
||||
if (! $r)
|
||||
$bool = false;
|
||||
|
||||
if ($savenow)
|
||||
$this->saveStatus(); // for now.
|
||||
|
||||
|
||||
return $bool;
|
||||
}
|
||||
|
||||
/** Addition of substraction for the counters */
|
||||
public function setStatusCount($name, $change, $savenow = true)
|
||||
{
|
||||
if (! $this->currentStatus->isCounter($name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$count = $this->getStatus($name);
|
||||
return $this->setStatus($name, $count + $change, $savenow);
|
||||
}
|
||||
|
||||
private function createQueue()
|
||||
{
|
||||
if (is_null($this->status))
|
||||
{
|
||||
$this->status = array();
|
||||
$this->status['queues'] = array();
|
||||
$this->DataProvider->install(true);
|
||||
}
|
||||
|
||||
$this->currentStatus = new Status();
|
||||
$this->saveStatus();
|
||||
|
||||
}
|
||||
|
||||
|
||||
private function loadStatus()
|
||||
{
|
||||
$this->status = get_option($this->statusName, null);
|
||||
|
||||
if (false === $this->status || is_null($this->status) || (! is_object($this->status) && ! is_array($this->status) ))
|
||||
{
|
||||
$this->createQueue();
|
||||
}
|
||||
elseif (! isset($this->status['queues'][$this->qName]))
|
||||
{
|
||||
$this->createQueue();
|
||||
}
|
||||
else {
|
||||
// ONLY status this reference.
|
||||
$this->currentStatus = $this->status['queues'][$this->qName];
|
||||
}
|
||||
}
|
||||
|
||||
public function getStatus($item = false)
|
||||
{
|
||||
if (is_null($this->currentStatus))
|
||||
return false;
|
||||
elseif (! $item)
|
||||
return $this->currentStatus;
|
||||
elseif (is_object($this->currentStatus))
|
||||
return $this->currentStatus->get($item);
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected function saveStatus()
|
||||
{
|
||||
$status = get_option($this->statusName); // two different Q's can run simulanously.
|
||||
|
||||
|
||||
$currentStatus = $this->currentStatus;
|
||||
if( $currentStatus === false && isset($status['queues'][$this->qName]) ) // Don't save status which has been removed.
|
||||
{
|
||||
unset($status['queues'][$this->qName]);
|
||||
}
|
||||
else {
|
||||
if (false === $status)
|
||||
{
|
||||
$status = array();
|
||||
$status['queues'] = array();
|
||||
}
|
||||
$status['queues'][$this->qName] = $currentStatus;
|
||||
}
|
||||
$res = update_option($this->statusName, $status);
|
||||
}
|
||||
|
||||
/** Check Queue. This function intends to keep internal counts consistent with dataprovider without doing queries every run .
|
||||
* Should also be able to spot the moment when there is nothing waiting, but perhaps some tasks as in process with a timeout. (stale)
|
||||
*/
|
||||
private function checkQueue()
|
||||
{
|
||||
$this->resetInternalCounts(); // retrieve accurate count from dataSource.
|
||||
|
||||
$tasks_done = $this->getStatus('done');
|
||||
$tasks_open = $this->getStatus('items');
|
||||
$tasks_inprocess = $this->getStatus('in_process');
|
||||
$tasks_error = $this->getStatus('errors');
|
||||
|
||||
$mode = $this->options->mode;
|
||||
$update_at_end = false;
|
||||
|
||||
if ($tasks_error > 0)
|
||||
{
|
||||
$update_at_end = true;
|
||||
$error_args = array(
|
||||
'numitems' => $tasks_error,
|
||||
'status' => ShortQ::QSTATUS_ERROR,
|
||||
// 'status' => ShortQ::QSTATUS_ERROR,
|
||||
);
|
||||
|
||||
$error_items = $this->DataProvider->dequeue($error_args);
|
||||
$retry_array = array();
|
||||
$failed_array = array();
|
||||
foreach($error_items as $errItem)
|
||||
{
|
||||
$errid = $errItem->item_id;
|
||||
if ($errItem->tries < $this->options->retry_limit)
|
||||
{
|
||||
//$retry_array = $erritem->id;
|
||||
$this->DataProvider->itemUpdate($errItem, array('status' => ShortQ::QSTATUS_WAITING));
|
||||
}
|
||||
else {
|
||||
$this->DataProvider->itemUpdate($errItem, array('status' => ShortQ::QSTATUS_FATAL));
|
||||
|
||||
}
|
||||
}
|
||||
} // tasks_errors
|
||||
|
||||
if($update_at_end)
|
||||
{
|
||||
$this->resetInternalCounts(); // retrieve accurate count from dataSource.
|
||||
$tasks_open = $this->currentStatus->get('items');
|
||||
$tasks_inprocess = $this->currentStatus->get('in_process');
|
||||
}
|
||||
|
||||
if ($tasks_open > 0 || $tasks_inprocess > 0)
|
||||
return true;
|
||||
else {
|
||||
$this->finishQueue();
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function resetInternalCounts()
|
||||
{
|
||||
$dataQ = $this->DataProvider->itemCount('countbystatus');
|
||||
$num_items = $num_done = $num_in_process = $num_errors = $num_fatal = 0;
|
||||
|
||||
if (is_array($dataQ))
|
||||
{
|
||||
foreach($dataQ as $qstatus => $count)
|
||||
{
|
||||
switch($qstatus)
|
||||
{
|
||||
case ShortQ::QSTATUS_WAITING:
|
||||
$num_items = $count;
|
||||
break;
|
||||
case ShortQ::QSTATUS_DONE:
|
||||
$num_done = $count;
|
||||
break;
|
||||
case ShortQ::QSTATUS_INPROCESS:
|
||||
$num_in_process = $count;
|
||||
break;
|
||||
case ShortQ::QSTATUS_ERROR:
|
||||
$num_errors = $count;
|
||||
break;
|
||||
case ShortQ::QSTATUS_FATAL;
|
||||
$num_fatal = $count;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->setStatus('items', $num_items, false);
|
||||
$this->setStatus('done', $num_done, false);
|
||||
$this->setStatus('in_process', $num_in_process, false);
|
||||
$this->setStatus('errors', $num_errors, false);
|
||||
$this->setStatus('fatal_errors', $num_fatal, false);
|
||||
|
||||
$this->saveStatus();
|
||||
// direct, to prevent loop.
|
||||
|
||||
}
|
||||
|
||||
private function calcAverageAsk($avg)
|
||||
{
|
||||
// @todo this is nonsense. Need a counter for X times run.
|
||||
return ($avg / $this->current_ask);
|
||||
}
|
||||
|
||||
private function finishQueue()
|
||||
{
|
||||
$this->setStatus('running', false, false);
|
||||
$this->setStatus('finished', true, false);
|
||||
$this->setStatus('last_run', time(), false);
|
||||
$this->setStatusCount('times_ran', 1, false );
|
||||
$this->saveStatus();
|
||||
|
||||
}
|
||||
|
||||
public function install()
|
||||
{
|
||||
$this->DataProvider->install(true);
|
||||
}
|
||||
|
||||
/** Function to call when uninstalling the plugin. This will remove only this current queue
|
||||
*/
|
||||
public function unInstall()
|
||||
{
|
||||
// Remove the Queued Items
|
||||
|
||||
// @todo this will only remove the records of current queue, probably not good for uninstall
|
||||
$this->DataProvider->removeRecords(array('all' => true));
|
||||
|
||||
// Unset the WP Option queue
|
||||
//unset($this->status
|
||||
unset($this->status['queues'][$this->qName]);
|
||||
|
||||
if (count($this->status['queues']) == 0)
|
||||
delete_option($this->statusName);
|
||||
else
|
||||
$this->saveStatus();
|
||||
|
||||
|
||||
// Signal to DataProvider to check if the whole thing can be removed. Won't happen when there are still records.
|
||||
$this->DataProvider->uninstall();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
namespace ShortPixel\ShortQ;
|
||||
//use \ShortPixel\ShortQ\Queue;
|
||||
//use \ShortPixel\ShortQ\DataProvider;
|
||||
|
||||
// init
|
||||
class ShortQ
|
||||
{
|
||||
|
||||
const QSTATUS_ALL = -1; // special status, for query use
|
||||
const QSTATUS_WAITING = 0;
|
||||
const QSTATUS_PULLED = 1; // not in use atm.
|
||||
const QSTATUS_INPROCESS = 2;
|
||||
const QSTATUS_DONE = 3;
|
||||
|
||||
const QSTATUS_DELETE = -1; // this is a virtual status. If set to this, will be deleted.
|
||||
const QSTATUS_ERROR = -2;
|
||||
const QSTATUS_FATAL = -3;
|
||||
|
||||
protected $pluginSlug; // unique plugin name using Q.
|
||||
protected $queueName;
|
||||
protected $queue;
|
||||
protected $dataProvider;
|
||||
|
||||
protected static $queues = array();
|
||||
|
||||
public function __construct($pluginSlug)
|
||||
{
|
||||
|
||||
$this->pluginSlug = $pluginSlug;
|
||||
//self::$queues[$qname] = $this;
|
||||
}
|
||||
|
||||
public function getQueue($qName, $lock = false)
|
||||
{
|
||||
// @todo get config from main parent file, or so.
|
||||
$this->queue = 'wp';
|
||||
$this->dataProvider = 'mysql';
|
||||
$this->queueName = $qName;
|
||||
|
||||
// if nothing, then create a new Q.
|
||||
$q = $this->QLoader();
|
||||
return $q;
|
||||
}
|
||||
|
||||
protected function QLoader()
|
||||
{
|
||||
$dataProvider = null;
|
||||
switch($this->dataProvider)
|
||||
{
|
||||
case 'mysql':
|
||||
default:
|
||||
$dataProvider = new DataProvider\MysqlDataProvider($this->pluginSlug, $this->queueName);
|
||||
break;
|
||||
}
|
||||
|
||||
switch($this->queue)
|
||||
{
|
||||
case 'wp':
|
||||
default:
|
||||
$newQ = new Queue\WPQ($this->pluginSlug, $this->queueName, $dataProvider);
|
||||
break;
|
||||
}
|
||||
|
||||
self::$queues[$this->queueName] = $this;
|
||||
return $newQ;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
<?php
|
||||
namespace ShortPixel\ShortQ;
|
||||
|
||||
/* Status Object to hold all relevant values for Queue Status like counts, times run etc.
|
||||
* Provider-agnostic so doesn't mingle in saving / loading .
|
||||
*/
|
||||
|
||||
class Status
|
||||
{
|
||||
|
||||
// Protecting against direct writing so at later point we can still add quality checks on setters.
|
||||
protected $items = 0; // number of items waiting.
|
||||
protected $in_process = 0; // amount of items currently in process ..
|
||||
protected $preparing = false; // flag to signal queue is being created and items are being uploaded. Don't run.
|
||||
protected $running = false; // flag to signal the queue is currently running
|
||||
protected $finished = false; // flag to signal nothing can move this queue anymore.
|
||||
protected $bulk_running = false; // external flag to note if a large amount is being more [optional]
|
||||
protected $done = 0; // number of items processed
|
||||
protected $errors = 0;
|
||||
protected $fatal_errors = 0;
|
||||
protected $last_run = 0; // internal
|
||||
protected $last_update = 0; // internal
|
||||
protected $times_ran = 0; // internal
|
||||
protected $average_ask = 0; // internal
|
||||
|
||||
protected $last_item_id = 0;
|
||||
|
||||
protected $custom_data = null; // data for the application, shortq does nothing with it.
|
||||
|
||||
public function isCounter($name)
|
||||
{
|
||||
if ( gettype($this->$name) == 'integer')
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
public function get($name)
|
||||
{
|
||||
if (isset($this->$name))
|
||||
return $this->$name;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
public function set($name, $value)
|
||||
{
|
||||
if(property_exists($this,$name))
|
||||
{
|
||||
if ($this->isCounter($name))
|
||||
$value = intval($value);
|
||||
|
||||
$this->$name = $value;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
<?php
|
||||
namespace ShortPixel\ShortQ\Tests;
|
||||
|
||||
class Tests{
|
||||
|
||||
protected $items = array();
|
||||
// protected $text_items = array('test1','test2','test3','test4','test5','test6','test7');
|
||||
|
||||
protected $q;
|
||||
|
||||
public function __construct($q)
|
||||
{
|
||||
global $wpdb;
|
||||
$wpdb->show_errors();
|
||||
$this->startView();
|
||||
|
||||
$q->setOptions(array('is_debug' => true));
|
||||
|
||||
echo "<PRE> START STATUS "; print_r($q->getStatus()); echo "</PRE>";
|
||||
|
||||
for ($i = 0; $i < 10; $i++)
|
||||
{
|
||||
$id = rand(0, 1000);
|
||||
$this->items[] = array('id' => $id, 'value' => $id);
|
||||
}
|
||||
|
||||
$deq_number = rand(1, 5);
|
||||
$q->setOption('numitems', $deq_number);
|
||||
|
||||
$this->q = $q;
|
||||
// $this->uninstall();
|
||||
|
||||
if ( $this->q->hasItems())
|
||||
{
|
||||
echo "ITEMS FOUND: " . $this->q->itemCount() . "<BR>";
|
||||
$this->runTestQ();
|
||||
}
|
||||
else {
|
||||
$this->addItems();
|
||||
$this->runTestQ();
|
||||
// $this->addItems();
|
||||
}
|
||||
|
||||
$this->results();
|
||||
|
||||
echo "<PRE> END STATUS "; print_r($q->getStatus()); echo "</PRE>";
|
||||
$this->endView();
|
||||
}
|
||||
|
||||
public function addItems()
|
||||
{
|
||||
print_r($this->items);
|
||||
$this->q->enqueue($this->items);
|
||||
}
|
||||
|
||||
public function runTestQ()
|
||||
{
|
||||
$this->deQueueBasic();
|
||||
}
|
||||
|
||||
public function deQueueBasic()
|
||||
{
|
||||
while($this->q->hasItems())
|
||||
{
|
||||
$item = $this->q->deQueue();
|
||||
echo "ITEM FROM THA Q "; var_dump($item);
|
||||
}
|
||||
}
|
||||
|
||||
public function uninstall()
|
||||
{
|
||||
$this->q->uninstall();
|
||||
}
|
||||
|
||||
//public function deQueu
|
||||
|
||||
public function results()
|
||||
{
|
||||
global $wpdb;
|
||||
echo $wpdb->last_error;
|
||||
}
|
||||
|
||||
public function startView()
|
||||
{
|
||||
?>
|
||||
<div class='debug' style='margin: 100px 0 100px 250px; background: #fff;'>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function endView()
|
||||
{
|
||||
?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
} // class
|
1201
wp-content/plugins/shortpixel-image-optimiser/changelog.txt
Normal file
1201
wp-content/plugins/shortpixel-image-optimiser/changelog.txt
Normal file
File diff suppressed because it is too large
Load Diff
2
wp-content/plugins/shortpixel-image-optimiser/class/.gitignore
vendored
Normal file
2
wp-content/plugins/shortpixel-image-optimiser/class/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
#make sure the plugin.json file is loaded for auto-loading the classes, in case json files are excluded via gitignore
|
||||
!plugin.json
|
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
namespace ShortPixel;
|
||||
|
||||
class BuildAutoLoader
|
||||
{
|
||||
|
||||
public static function buildJSON()
|
||||
{
|
||||
echo 'Building Plugin.JSON';
|
||||
$plugin = array(
|
||||
'name' => 'ShortPixel/Plugin',
|
||||
'description' => 'ShortPixel AutoLoader',
|
||||
'type' => 'function',
|
||||
'autoload' => array('psr-4' => array('ShortPixel' => 'class'),
|
||||
'files' => self::getFiles(),
|
||||
),
|
||||
);
|
||||
|
||||
$f = fopen('class/plugin.json', 'w');
|
||||
$result = fwrite($f, json_encode($plugin));
|
||||
|
||||
if ($result === false)
|
||||
echo "!!! Error !!! Could not write Plugin.json";
|
||||
|
||||
fclose($f);
|
||||
}
|
||||
|
||||
public static function getFiles()
|
||||
{
|
||||
$main = array(
|
||||
// 'shortpixel_api.php',
|
||||
// 'class/wp-short-pixel.php',
|
||||
'class/wp-shortpixel-settings.php',
|
||||
// 'class/view/shortpixel_view.php',
|
||||
'class/front/img-to-picture-webp.php',
|
||||
);
|
||||
|
||||
$models = array(
|
||||
);
|
||||
|
||||
$externals = array(
|
||||
'class/external/cloudflare.php',
|
||||
//'class/external/gravityforms.php',
|
||||
'class/external/nextgen/nextGenController.php',
|
||||
'class/external/nextgen/nextGenViewController.php',
|
||||
'class/external/visualcomposer.php',
|
||||
'class/external/offload/Offloader.php',
|
||||
'class/external/offload/wp-offload-media.php',
|
||||
'class/external/offload/virtual-filesystem.php',
|
||||
'class/external/wp-cli/wp-cli-base.php',
|
||||
'class/external/wp-cli/wp-cli-single.php',
|
||||
'class/external/wp-cli/wp-cli-bulk.php',
|
||||
'class/external/image-galleries.php',
|
||||
'class/external/pantheon.php',
|
||||
'class/external/spai.php',
|
||||
'class/external/cache.php',
|
||||
'class/external/uncode.php',
|
||||
'class/external/query-monitor.php',
|
||||
'class/external/Woocommerce.php',
|
||||
'class/external/themes/total-theme.php',
|
||||
);
|
||||
|
||||
echo "Build Plugin.JSON ";
|
||||
return array_merge($main,$models,$externals);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
namespace ShortPixel;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
use ShortPixel\Helper\UiHelper as UiHelper;
|
||||
|
||||
/** Proto parent class for all controllers.
|
||||
*
|
||||
* So far none of the controller need or implement similar enough functions for a parent to make sense. * Perhaps this will change of time, so most are extending this parent.
|
||||
**/
|
||||
|
||||
// @todo Think how to do this better.
|
||||
class Controller
|
||||
{
|
||||
|
||||
protected $model;
|
||||
protected $userIsAllowed = false;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->userIsAllowed = $this->checkUserPrivileges();
|
||||
}
|
||||
|
||||
|
||||
protected function checkUserPrivileges()
|
||||
{
|
||||
if ((current_user_can( 'manage_options' ) || current_user_can( 'upload_files' ) || current_user_can( 'edit_posts' )))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// helper for a helper.
|
||||
protected function formatNumber($number, $precision = 2)
|
||||
{
|
||||
return UIHelper::formatNumber($number, $precision);
|
||||
}
|
||||
|
||||
} // class
|
@ -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') . '"> </span>
|
||||
<span class="dashicons dashicons-controls-play play" title="' . __('Resume', 'shortpixel-image-optimiser') . '"> </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
|
@ -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
@ -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
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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>');
|
||||
}
|
||||
}
|
||||
}
|
@ -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) . '/';
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
@ -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
|
@ -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
|
File diff suppressed because it is too large
Load Diff
@ -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("'", "'", $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
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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);
|
||||
} */
|
||||
|
||||
}
|
@ -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
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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
|
@ -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 don’t 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"> </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
|
@ -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 .
|
@ -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>";
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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) . '"> </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">«</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' ),
|
||||
'«'
|
||||
);
|
||||
}
|
||||
|
||||
if ( $disable_prev ) {
|
||||
$page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">‹</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' ),
|
||||
'‹'
|
||||
);
|
||||
}
|
||||
|
||||
$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">›</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' ),
|
||||
'›'
|
||||
);
|
||||
}
|
||||
|
||||
if ( $disable_last ) {
|
||||
$page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">»</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' ),
|
||||
'»'
|
||||
);
|
||||
}
|
||||
|
||||
$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
|
@ -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
|
@ -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">«</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' ),
|
||||
'«'
|
||||
);
|
||||
}
|
||||
|
||||
if ( $disable_prev ) {
|
||||
$page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">‹</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' ),
|
||||
'‹'
|
||||
);
|
||||
}
|
||||
|
||||
$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">›</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' ),
|
||||
'›'
|
||||
);
|
||||
}
|
||||
|
||||
if ( $disable_last ) {
|
||||
$page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">»</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' ),
|
||||
'»'
|
||||
);
|
||||
}
|
||||
|
||||
$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) . '"> </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;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,183 @@
|
||||
<?php
|
||||
namespace ShortPixel\Helper;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
|
||||
use ShortPixel\Controller\ResponseController as ResponseController;
|
||||
|
||||
class DownloadHelper
|
||||
{
|
||||
private static $instance;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->checkEnv();
|
||||
}
|
||||
|
||||
public static function getInstance()
|
||||
{
|
||||
if (is_null(self::$instance))
|
||||
{
|
||||
self::$instance = new DownloadHelper();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
protected function checkEnv()
|
||||
{
|
||||
if ( ! function_exists( 'download_url' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
}
|
||||
}
|
||||
|
||||
public function downloadFile($url, $args = array())
|
||||
{
|
||||
$defaults = array(
|
||||
'expectedSize' => null,
|
||||
'destinationPath' => null,
|
||||
);
|
||||
|
||||
$args = wp_parse_args($args, $defaults);
|
||||
$success = false;
|
||||
|
||||
Log::addDebug('Downloading file :' . $url, $args);
|
||||
|
||||
$methods = array(
|
||||
"download_url" => array(array($this, 'downloadURLMethod'), $url, false),
|
||||
"download_url_force" => array(array($this, 'downloadURLMethod'), true),
|
||||
"remote_get" => array(array($this, 'remoteGetMethod'), $url)
|
||||
);
|
||||
|
||||
foreach($methods as $name => $data)
|
||||
{
|
||||
$function = $data[0];
|
||||
if (is_callable($function))
|
||||
{
|
||||
$result = call_user_func_array($function, array_slice($data, 1) );
|
||||
|
||||
if (false !== $result)
|
||||
{
|
||||
$tempFile = $result;
|
||||
$success = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (false === $success)
|
||||
{
|
||||
Log::addError('Failed to download File', $result);
|
||||
ResponseController::addData('is_error', true);
|
||||
//Responsecontroller::addData('message', $tempFile->get_error_message());
|
||||
return false;
|
||||
}
|
||||
|
||||
$fs = \wpSPIO()->filesystem();
|
||||
$file = $fs->getFile($tempFile);
|
||||
|
||||
if (! is_null($args['destinationPath']))
|
||||
{
|
||||
$result = $this->moveDownload($file, $args['destinationPath']);
|
||||
if (false === $result)
|
||||
{
|
||||
Log::addError('Failed to move Download', $args);
|
||||
ResponseController::addData('is_error', true);
|
||||
Responsecontroller::addData('message', __('Failed to move download to destination!', 'shortpixel-image-optimiser'));
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
$file = $result;
|
||||
}
|
||||
}
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
protected function moveDownload($fileObj, $destinationPath)
|
||||
{
|
||||
$fs = \wpSPIO()->filesystem();
|
||||
|
||||
$destinationFile = $fs->getFile($destinationPath);
|
||||
// If file is non-existing, check directory and write-permissions.
|
||||
if (false == $destinationFile->exists())
|
||||
{
|
||||
$dirObj = $destinationFile->getFileDir();
|
||||
$dirObj->check(true);
|
||||
}
|
||||
|
||||
$result = $fileObj->copy($destinationFile);
|
||||
|
||||
if ($result === false)
|
||||
return false;
|
||||
|
||||
return $destinationFile;
|
||||
|
||||
}
|
||||
|
||||
private function downloadURLMethod($url, $force = false)
|
||||
{
|
||||
$executionTime = ini_get('max_execution_time');
|
||||
if (! is_numeric($executionTime)) // edge case
|
||||
{
|
||||
$executionTime = 0;
|
||||
}
|
||||
$downloadTimeout = max($executionTime - 10, 15);
|
||||
|
||||
$url = $this->setPreferredProtocol(urldecode($url), $force);
|
||||
$tempFile = \download_url($url, $downloadTimeout);
|
||||
|
||||
if (is_wp_error($tempFile))
|
||||
{
|
||||
Log::addError('Failed to Download File ', $tempFile);
|
||||
Responsecontroller::addData('message', $tempFile->get_error_message());
|
||||
return false;
|
||||
}
|
||||
|
||||
return $tempFile;
|
||||
}
|
||||
|
||||
private function remoteGetMethod($url)
|
||||
{
|
||||
//get_temp_dir
|
||||
$tmpfname = tempnam(get_temp_dir(), 'spiotmp');
|
||||
$downloadTimeout = max(ini_get('max_execution_time') - 10, 15);
|
||||
|
||||
$args_for_get = array(
|
||||
'stream' => true,
|
||||
'filename' => $tmpfname,
|
||||
'timeout' => $downloadTimeout,
|
||||
);
|
||||
|
||||
$response = wp_remote_get( $url, $args_for_get );
|
||||
|
||||
if (wp_remote_retrieve_response_code($response) == 200 && isset($response['filename']))
|
||||
{
|
||||
$filepath = $response['filename'];
|
||||
return $filepath; // body is the full image is all went well.
|
||||
}
|
||||
else {
|
||||
Log::addError('Wp Remote Get failed', $response);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function setPreferredProtocol($url, $reset = false) {
|
||||
//switch protocol based on the formerly detected working protocol
|
||||
$settings = \wpSPIO()->settings();
|
||||
|
||||
if($settings->downloadProto == '' || $reset) {
|
||||
//make a test to see if the http is working
|
||||
$testURL = 'http://' . SHORTPIXEL_API . '/img/connection-test-image.png';
|
||||
$result = download_url($testURL, 10);
|
||||
$settings->downloadProto = is_wp_error( $result ) ? 'https' : 'http';
|
||||
}
|
||||
return $settings->downloadProto == 'http' ?
|
||||
str_replace('https://', 'http://', $url) :
|
||||
str_replace('http://', 'https://', $url);
|
||||
}
|
||||
}
|
@ -0,0 +1,315 @@
|
||||
<?php
|
||||
namespace ShortPixel\Helper;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
|
||||
use ShortPixel\Controller\OptimizeController as OptimizeController;
|
||||
use ShortPixel\Controller\BulkController as BulkController;
|
||||
use ShortPixel\Controller\FileSystemController as FileSystemController;
|
||||
use ShortPixel\Controller\AdminNoticesController as AdminNoticesController;
|
||||
use ShortPixel\Controller\StatsController as StatsController;
|
||||
use ShortPixel\Controller\ApiKeyController as ApiKeyController;
|
||||
use ShortPixel\Helper\UtilHelper as UtilHelper;
|
||||
|
||||
|
||||
class InstallHelper
|
||||
{
|
||||
|
||||
public static function activatePlugin()
|
||||
{
|
||||
self::deactivatePlugin();
|
||||
$settings = \wpSPIO()->settings();
|
||||
|
||||
$env = wpSPIO()->env();
|
||||
|
||||
if(\WPShortPixelSettings::getOpt('deliverWebp') == 3 && ! $env->is_nginx) {
|
||||
UtilHelper::alterHtaccess(true,true); //add the htaccess lines. Both are true because even if one option is now off in the past both fileformats could have been generated.
|
||||
}
|
||||
|
||||
self::checkTables();
|
||||
|
||||
AdminNoticesController::resetOldNotices();
|
||||
\WPShortPixelSettings::onActivate();
|
||||
|
||||
$optimizeController = new OptimizeController();
|
||||
$q = $optimizeController->getQueue('media');
|
||||
$q->getShortQ()->install(); // create table.
|
||||
|
||||
$settings->currentVersion = SHORTPIXEL_IMAGE_OPTIMISER_VERSION;
|
||||
}
|
||||
|
||||
public static function deactivatePlugin()
|
||||
{
|
||||
|
||||
$settings = \wpSPIO()->settings();
|
||||
$settings::onDeactivate();
|
||||
|
||||
$env = wpSPIO()->env();
|
||||
|
||||
if (! $env->is_nginx)
|
||||
{
|
||||
UtilHelper::alterHtaccess(false, false);
|
||||
}
|
||||
|
||||
// save remove.
|
||||
$fs = new FileSystemController();
|
||||
$log = $fs->getFile(SHORTPIXEL_BACKUP_FOLDER . "/shortpixel_log");
|
||||
|
||||
if ($log->exists())
|
||||
$log->delete();
|
||||
|
||||
global $wpdb;
|
||||
$sql = "delete from " . $wpdb->options . " where option_name like '%_transient_shortpixel%'";
|
||||
$wpdb->query($sql); // remove transients.
|
||||
|
||||
// saved in settings object, reset all stats.
|
||||
StatsController::getInstance()->reset();
|
||||
}
|
||||
|
||||
public static function uninstallPlugin()
|
||||
{
|
||||
OptimizeController::uninstallPlugin();
|
||||
ApiKeyController::uninstallPlugin();
|
||||
|
||||
delete_transient('bulk-secret');
|
||||
delete_transient('othermedia_refresh_folder_delay');
|
||||
delete_transient('avif_server_check');
|
||||
delete_transient('quotaData');
|
||||
|
||||
}
|
||||
|
||||
// Removes everything of SPIO 5.x . Not recommended.
|
||||
public static function hardUninstall()
|
||||
{
|
||||
$env = \wpSPIO()->env();
|
||||
$settings = \wpSPIO()->settings();
|
||||
|
||||
|
||||
$nonce = (isset($_POST['tools-nonce'])) ? sanitize_key($_POST['tools-nonce']) : null;
|
||||
if ( ! wp_verify_nonce( $nonce, 'remove-all' ) ) {
|
||||
wp_nonce_ays( '' );
|
||||
}
|
||||
|
||||
self::deactivatePlugin(); // deactivate
|
||||
self::uninstallPlugin(); // uninstall
|
||||
|
||||
// Bulk Log
|
||||
BulkController::uninstallPlugin();
|
||||
|
||||
$settings::resetOptions();
|
||||
|
||||
if (! $env->is_nginx)
|
||||
{
|
||||
insert_with_markers( get_home_path() . '.htaccess', 'ShortPixelWebp', '');
|
||||
}
|
||||
|
||||
self::removeTables();
|
||||
|
||||
// Remove Backups
|
||||
$dir = \wpSPIO()->filesystem()->getDirectory(SHORTPIXEL_BACKUP_FOLDER);
|
||||
$dir->recursiveDelete();
|
||||
|
||||
$plugin = basename(SHORTPIXEL_PLUGIN_DIR) . '/' . basename(SHORTPIXEL_PLUGIN_FILE);
|
||||
deactivate_plugins($plugin);
|
||||
|
||||
}
|
||||
|
||||
|
||||
public static function deactivateConflictingPlugin()
|
||||
{
|
||||
if ( ! isset($_GET['_wpnonce']) || ! wp_verify_nonce( sanitize_key($_GET['_wpnonce']), 'sp_deactivate_plugin_nonce' ) ) {
|
||||
wp_nonce_ays( 'Nononce' );
|
||||
}
|
||||
|
||||
$referrer_url = wp_get_referer();
|
||||
$url = wp_get_referer();
|
||||
$plugin = (isset($_GET['plugin'])) ? sanitize_text_field(wp_unslash($_GET['plugin'])) : null; // our target.
|
||||
|
||||
if (! is_null($plugin))
|
||||
deactivate_plugins($plugin);
|
||||
|
||||
wp_safe_redirect($url);
|
||||
die();
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if TableName exists
|
||||
* @param $tableName The Name of the Table without Prefix.
|
||||
*/
|
||||
public static function checkTableExists($tableName)
|
||||
{
|
||||
global $wpdb;
|
||||
$tableName = $wpdb->prefix . $tableName;
|
||||
$sql = $wpdb->prepare("
|
||||
SHOW TABLES LIKE %s
|
||||
", $tableName);
|
||||
|
||||
$result = intval($wpdb->query($sql));
|
||||
|
||||
if ($result == 0)
|
||||
return false;
|
||||
else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static function checkTables()
|
||||
{
|
||||
global $wpdb;
|
||||
require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
|
||||
|
||||
dbDelta(self::getFolderTableSQL());
|
||||
dbDelta(self::getMetaTableSQL());
|
||||
dbDelta(self::getPostMetaSQL());
|
||||
|
||||
self::checkIndexes();
|
||||
}
|
||||
|
||||
private static function checkIndexes()
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$definitions = array(
|
||||
'shortpixel_meta' => array(
|
||||
'path' => 'path'
|
||||
),
|
||||
'shortpixel_folders' => array(
|
||||
'path' => 'path'
|
||||
),
|
||||
'shortpixel_postmeta' => array(
|
||||
'attach_id' => 'attach_id',
|
||||
'parent' => 'parent',
|
||||
'size' => 'size',
|
||||
'status' => 'status',
|
||||
'compression_type' => 'compression_type'
|
||||
)
|
||||
);
|
||||
|
||||
foreach($definitions as $raw_tableName => $indexes)
|
||||
{
|
||||
$tableName = $wpdb->prefix . $raw_tableName;
|
||||
foreach($indexes as $indexName => $fieldName)
|
||||
{
|
||||
// Check exists
|
||||
$sql = 'SHOW INDEX FROM ' . $tableName . ' WHERE Key_name = %s';
|
||||
$sql = $wpdb->prepare($sql, $indexName);
|
||||
|
||||
$res = $wpdb->get_row($sql);
|
||||
|
||||
if (is_null($res))
|
||||
{
|
||||
// can't prepare for those, also not any user data here.
|
||||
$sql = 'CREATE INDEX ' . $indexName . ' ON ' . $tableName . ' ( ' . $fieldName . ')';
|
||||
$res = $wpdb->query($sql);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function removeTables()
|
||||
{
|
||||
global $wpdb;
|
||||
if (self::checkTableExists('shortpixel_folders') === true)
|
||||
{
|
||||
$sql = 'DROP TABLE ' . $wpdb->prefix . 'shortpixel_folders';
|
||||
$wpdb->query($sql);
|
||||
}
|
||||
if (self::checkTableExists('shortpixel_meta') === true)
|
||||
{
|
||||
$sql = 'DROP TABLE ' . $wpdb->prefix . 'shortpixel_meta';
|
||||
$wpdb->query($sql);
|
||||
}
|
||||
if (self::checkTableExists('shortpixel_postmeta') === true)
|
||||
{
|
||||
$sql = 'DROP TABLE ' . $wpdb->prefix . 'shortpixel_postmeta';
|
||||
error_log('Dropping postmeta' . $sql);
|
||||
$wpdb->query($sql);
|
||||
}
|
||||
}
|
||||
|
||||
public static function getFolderTableSQL() {
|
||||
global $wpdb;
|
||||
$charsetCollate = $wpdb->get_charset_collate();
|
||||
$prefix = $wpdb->prefix;
|
||||
|
||||
return "CREATE TABLE {$prefix}shortpixel_folders (
|
||||
id mediumint(9) NOT NULL AUTO_INCREMENT,
|
||||
path varchar(512),
|
||||
name varchar(150),
|
||||
path_md5 char(32),
|
||||
file_count int,
|
||||
status SMALLINT NOT NULL DEFAULT 0,
|
||||
parent SMALLINT DEFAULT 0,
|
||||
ts_checked timestamp,
|
||||
ts_updated timestamp,
|
||||
ts_created timestamp,
|
||||
PRIMARY KEY id (id)
|
||||
) $charsetCollate;";
|
||||
|
||||
}
|
||||
|
||||
public static function getMetaTableSQL() {
|
||||
global $wpdb;
|
||||
$charsetCollate = $wpdb->get_charset_collate();
|
||||
$prefix = $wpdb->prefix;
|
||||
|
||||
return "CREATE TABLE {$prefix}shortpixel_meta (
|
||||
id mediumint(10) NOT NULL AUTO_INCREMENT,
|
||||
folder_id mediumint(9) NOT NULL,
|
||||
ext_meta_id int(10),
|
||||
path varchar(512),
|
||||
name varchar(150),
|
||||
path_md5 char(32),
|
||||
compressed_size int(10) NOT NULL DEFAULT 0,
|
||||
compression_type tinyint,
|
||||
keep_exif tinyint DEFAULT 0,
|
||||
cmyk2rgb tinyint DEFAULT 0,
|
||||
resize tinyint,
|
||||
resize_width smallint,
|
||||
resize_height smallint,
|
||||
backup tinyint DEFAULT 0,
|
||||
status SMALLINT NOT NULL DEFAULT 0,
|
||||
retries tinyint NOT NULL DEFAULT 0,
|
||||
message varchar(255),
|
||||
ts_added timestamp,
|
||||
ts_optimized timestamp,
|
||||
extra_info LONGTEXT,
|
||||
PRIMARY KEY sp_id (id)
|
||||
) $charsetCollate;";
|
||||
|
||||
}
|
||||
|
||||
public static function getPostMetaSQL()
|
||||
{
|
||||
global $wpdb;
|
||||
$charsetCollate = $wpdb->get_charset_collate();
|
||||
$prefix = $wpdb->prefix;
|
||||
|
||||
$sql = "CREATE TABLE {$prefix}shortpixel_postmeta (
|
||||
id bigint unsigned NOT NULL AUTO_INCREMENT ,
|
||||
attach_id bigint unsigned NOT NULL,
|
||||
parent bigint unsigned NOT NULL,
|
||||
image_type tinyint default 0,
|
||||
size varchar(200),
|
||||
status tinyint default 0,
|
||||
compression_type tinyint,
|
||||
compressed_size int,
|
||||
original_size int,
|
||||
tsAdded timestamp,
|
||||
tsOptimized timestamp,
|
||||
extra_info LONGTEXT,
|
||||
PRIMARY KEY id (id)
|
||||
) $charsetCollate;";
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
|
||||
} // InstallHelper
|
@ -0,0 +1,885 @@
|
||||
<?php
|
||||
namespace ShortPixel\Helper;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
use ShortPixel\Model\Image\ImageModel as ImageModel;
|
||||
use ShortPixel\Controller\ApiKeyController as ApiKeyController;
|
||||
use ShortPixel\Controller\QuotaController as QuotaController;
|
||||
use ShortPixel\Controller\OptimizeController as OptimizeController;
|
||||
|
||||
use ShortPixel\Model\AccessModel as AccessModel;
|
||||
|
||||
class UiHelper
|
||||
{
|
||||
|
||||
private static $outputMode = 'admin';
|
||||
|
||||
private static $knowledge_url = 'https://shortpixel.com/knowledge-base/search?query='; // the URL of all knowledge.
|
||||
|
||||
public static function setOutputHandler($name)
|
||||
{
|
||||
self::$outputMode = $name;
|
||||
}
|
||||
|
||||
public static function renderBurgerList($actions, $imageObj)
|
||||
{
|
||||
$output = "";
|
||||
$id = $imageObj->get('id');
|
||||
$primary = isset($actions['optimizethumbs']) ? 'button-primary' : '';
|
||||
|
||||
$output .= "<div class='sp-column-actions '>
|
||||
<div class='sp-dropdown'>
|
||||
<button onclick='ShortPixel.openImageMenu(event);' class='sp-dropbtn button dashicons dashicons-menu $primary' title='ShortPixel Actions'></button>";
|
||||
$output .= "<div id='sp-dd-$id' class='sp-dropdown-content'>";
|
||||
|
||||
foreach($actions as $actionName => $actionData)
|
||||
{
|
||||
$link = ($actionData['type'] == 'js') ? 'javascript:' . $actionData['function'] : $actionData['function'];
|
||||
$output .= "<a href='" . $link . "' class='" . esc_attr($actionName) . "' >" . esc_html($actionData['text']) . "</a>";
|
||||
|
||||
}
|
||||
|
||||
$output .= "</div> <!--sp-dropdown-content--> </div> <!--sp-dropdown--> </div> <!--sp-column-actions--> ";
|
||||
return $output;
|
||||
}
|
||||
|
||||
public static function renderSuccessText($imageObj)
|
||||
{
|
||||
$output = '';
|
||||
//$percent = $imageObj->getMeta('improvement');
|
||||
$percent = $imageObj->getImprovement();
|
||||
|
||||
if($percent == 999) return ;
|
||||
|
||||
if ($percent == 999 )
|
||||
$output .= __("Reduced by X%(unknown)", 'shortpixel-image-optimiser');
|
||||
|
||||
if ($percent && $percent > 0)
|
||||
{
|
||||
$output .= __('Reduced by','shortpixel-image-optimiser') . ' <strong>' . self::formatNumber($percent,2) . '%</strong> ';
|
||||
}
|
||||
if (intval($percent) < 5)
|
||||
$output .= __('Bonus processing','shortpixel-image-optimiser');
|
||||
|
||||
$type = $imageObj->getMeta('compressionType');
|
||||
$output .= ' ('. self::compressionTypeToText($type) .')';
|
||||
|
||||
$thumbs = $imageObj->get('thumbnails');
|
||||
$thumbsDone = $retinasDone = 0;
|
||||
$thumbsTotal = ($thumbs) ? count($thumbs) : 0;
|
||||
|
||||
$retinas = $imageObj->get('retinas');
|
||||
|
||||
$webpsTotal = $imageObj->count('webps');
|
||||
$avifsTotal = $imageObj->count('avifs');
|
||||
|
||||
if($retinas)
|
||||
{
|
||||
foreach($retinas as $retinaObj)
|
||||
{
|
||||
if ($retinaObj->isOptimized())
|
||||
{
|
||||
$retinasDone++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$improvements = $imageObj->getImprovements();
|
||||
$thumbTotal = $thumbsDone = 0;
|
||||
if ($imageObj->get('thumbnails'))
|
||||
{
|
||||
$thumbsTotal = count($imageObj->get('thumbnails')); //
|
||||
//$thumbsDone = (isset($improvements['thumbnails'])) ? count($improvements['thumbnails']) : 0;
|
||||
$thumbsDone = $imageObj->count('optimized', array('thumbs_only' => true));
|
||||
$excludedThumbs = $imageObj->count('user_excluded', array('thumbs_only' => true));
|
||||
}
|
||||
|
||||
if (isset($improvements['thumbnails']))
|
||||
{
|
||||
$excluded = ($excludedThumbs > 0) ? sprintf(__('(%s excluded)', 'shortpixel-image-optimiser'), $excludedThumbs) : '';
|
||||
|
||||
$output .= '<div class="thumbnails optimized">';
|
||||
if ($thumbsTotal > $thumbsDone)
|
||||
$output .= '<div class="totals">' . sprintf(__('+%s of %s thumbnails optimized','shortpixel-image-optimiser'), self::formatNumber($thumbsDone,0), self::formatNumber($thumbsTotal,0)) . ' ' . $excluded . '</div>';
|
||||
|
||||
elseif ($thumbsDone > 0)
|
||||
$output .= '<div class="totals">' . sprintf(__('+%s thumbnails optimized','shortpixel-image-optimiser'), self::formatNumber($thumbsDone, 0)) . ' ' . $excluded . '</div>';
|
||||
|
||||
$improvs = array();
|
||||
|
||||
uasort($improvements['thumbnails'], function ($a, $b) {
|
||||
//return $b[0] <=> $a[0]; // @todo Efficient code to use once PHP 5 support is done.
|
||||
if ($a == $b) {
|
||||
return 0;
|
||||
}
|
||||
return ($b < $a) ? -1 : 1;
|
||||
});
|
||||
|
||||
$cutoff = false;
|
||||
$thumbCount = count($improvements['thumbnails']);
|
||||
if ($thumbCount > 20)
|
||||
{
|
||||
$improvements['thumbnails'] = array_slice($improvements['thumbnails'], 0, 15, true);
|
||||
$cutoff = true;
|
||||
}
|
||||
|
||||
|
||||
// Quality Check
|
||||
foreach($improvements['thumbnails'] as $thumbName => $thumbStat)
|
||||
{
|
||||
$stat = $thumbStat[0];
|
||||
if (is_numeric($stat) && $stat >= 0)
|
||||
{
|
||||
$improvs[$thumbName] = $stat; //self::formatNumber($stat,2);
|
||||
}
|
||||
}
|
||||
|
||||
if (count($improvs) > 0)
|
||||
{
|
||||
$output .= "<div class='thumb-wrapper'>";
|
||||
$lowrating = 0;
|
||||
foreach($improvs as $thumbName => $stat)
|
||||
{
|
||||
$statText = self::formatNumber($stat, 2);
|
||||
$title = sprintf(__('%s : %s', 'shortpixel-image-optimiser'), $thumbName, $statText . '%');
|
||||
$rating = ceil( round($stat) / 10);
|
||||
if (0 == $rating)
|
||||
{
|
||||
$lowrating++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$blocks_on = str_repeat('<span class="point checked"> </span>', $rating);
|
||||
$blocks_off = str_repeat('<span class="point"> </span>', (10- $rating));
|
||||
|
||||
$output .= "<div class='thumb " . $thumbName . "' title='" . $title . "'>"
|
||||
. "<span class='thumb-name'>" . $thumbName . '</span>' .
|
||||
"<span class='optimize-bar'>" . $blocks_on . $blocks_off . "</span>
|
||||
</div>";
|
||||
}
|
||||
|
||||
if ($lowrating > 0)
|
||||
{
|
||||
$blocks_off = str_repeat('<span class="point"> </span>', 10);
|
||||
|
||||
$output .= "<div class='thumb'>"
|
||||
. "<span class='thumb-name'>" . sprintf(__('+ %d thumbnails ', 'shortpixel-image-optimiser'), $lowrating) . '</span>' .
|
||||
"<span class='optimize-bar'>" . $blocks_off . "</span>
|
||||
</div>";
|
||||
}
|
||||
|
||||
if (true === $cutoff)
|
||||
{
|
||||
$output .= '<div class="thumb"><span class="cutoff">' . sprintf(__('+ %d more', 'shortpixel-image-optimiser'), ($thumbCount - 15)) . '</span></div>';
|
||||
}
|
||||
|
||||
|
||||
$output .= "</div> <!-- /thumb-wrapper -->";
|
||||
}
|
||||
$output .= "</div> <!-- /thumb optimized -->";
|
||||
}
|
||||
|
||||
if ($retinasDone > 0)
|
||||
{
|
||||
$output .= '<div class="filetype retina">' . sprintf(__('+%s Retina images optimized','shortpixel-image-optimiser') , $retinasDone) . '</div>';
|
||||
}
|
||||
if ($webpsTotal > 0)
|
||||
{
|
||||
$output .= '<div class="filetype webp">' . sprintf(__('+%s Webp images ','shortpixel-image-optimiser') , $webpsTotal) . '</div>';
|
||||
}
|
||||
if ($avifsTotal > 0)
|
||||
{
|
||||
$output .= '<div class="filetype avif">' . sprintf(__('+%s Avif images ','shortpixel-image-optimiser') , $avifsTotal) . '</div>';
|
||||
}
|
||||
|
||||
if ($imageObj->isSomethingOptimized() && $imageObj->isProcessable())
|
||||
{
|
||||
list($urls, $optimizable) = $imageObj->getCountOptimizeData('thumbnails');
|
||||
list($webpUrls, $webpCount) = $imageObj->getCountOptimizeData('webp');
|
||||
list($avifUrls, $avifCount) = $imageObj->getCountOptimizeData('avif');
|
||||
|
||||
|
||||
$maxList = 10;
|
||||
|
||||
if (count($urls) > $maxList)
|
||||
{
|
||||
$urls = array_slice($urls, 0, $maxList, true);
|
||||
$urls[] = '...';
|
||||
}
|
||||
if (count($webpUrls) > $maxList)
|
||||
{
|
||||
$webpUrls = array_slice($webpUrls, 0, $maxList, true);
|
||||
$webpUrls[] = '...';
|
||||
}
|
||||
if (count($avifUrls) > $maxList)
|
||||
{
|
||||
$avifUrls = array_slice($avifUrls, 0, $maxList, true);
|
||||
$avifUrls[] = '...';
|
||||
}
|
||||
|
||||
if ($optimizable > 0)
|
||||
{
|
||||
$output .= '<div class="thumbs-todo"><h4>' . sprintf(__('%d images to optimize', 'shortpixel-image-optimiser'), $optimizable) . '</h4>';
|
||||
$output .= "<span>";
|
||||
foreach($urls as $optObj)
|
||||
{
|
||||
if ($optObj === '...')
|
||||
$output .= $optObj;
|
||||
else
|
||||
$output .= substr($optObj, strrpos($optObj, '/')+1) . '<br>';
|
||||
}
|
||||
$output .= "</span>";
|
||||
$output .= '</div>';
|
||||
}
|
||||
|
||||
if ($webpCount > 0 )
|
||||
{
|
||||
|
||||
$output .= '<div class="thumbs-todo"><h4>' . sprintf(__('%d Webp files to create', 'shortpixel-image-optimiser'), $webpCount) . '</h4>';
|
||||
$output .= "<span>";
|
||||
foreach($webpUrls as $optObj)
|
||||
{
|
||||
if ($optObj === '...')
|
||||
$output .= $optObj;
|
||||
else
|
||||
$output .= self::convertImageTypeName(substr($optObj, strrpos($optObj, '/')+1), 'webp') . '<br>';
|
||||
}
|
||||
$output .= "</span>";
|
||||
$output .= '</div>';
|
||||
}
|
||||
if ($avifCount > 0)
|
||||
{
|
||||
$output .= '<div class="thumbs-todo"><h4>' . sprintf(__('%d Avif files to create', 'shortpixel-image-optimiser'), $avifCount) . '</h4>';
|
||||
$output .= "<span>";
|
||||
foreach($avifUrls as $optObj)
|
||||
{
|
||||
$output .= self::convertImageTypeName(substr($optObj, strrpos($optObj, '/')+1), 'avif') . '<br>';
|
||||
}
|
||||
$output .= "</span>";
|
||||
$output .= '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
|
||||
}
|
||||
|
||||
public static function compressionTypeToText($type)
|
||||
{
|
||||
if ($type == ImageModel::COMPRESSION_LOSSLESS )
|
||||
return __('Lossless', 'shortpixel-image-optimiser');
|
||||
|
||||
if ($type == ImageModel::COMPRESSION_LOSSY )
|
||||
return __('Lossy', 'shortpixel-image-optimiser');
|
||||
|
||||
if ($type == ImageModel::COMPRESSION_GLOSSY )
|
||||
return __('Glossy', 'shortpixel-image-optimiser');
|
||||
|
||||
return $type;
|
||||
}
|
||||
|
||||
public static function getListActions($mediaItem)
|
||||
{
|
||||
$list_actions = array();
|
||||
$id = $mediaItem->get('id');
|
||||
|
||||
$keyControl = ApiKeyController::getInstance();
|
||||
if (! $keyControl->keyIsVerified())
|
||||
{
|
||||
return array(); // nothing
|
||||
}
|
||||
|
||||
$quotaControl = QuotaController::getInstance();
|
||||
|
||||
$access = AccessModel::getInstance();
|
||||
if (! $access->imageIsEditable($mediaItem))
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
if ($id === 0)
|
||||
return array();
|
||||
|
||||
if ($mediaItem->isSomethingOptimized() )
|
||||
{
|
||||
list($u, $optimizable) = $mediaItem->getCountOptimizeData('thumbnails');
|
||||
list($u, $optimizableWebp) = $mediaItem->getCountOptimizeData('webp');
|
||||
list($u, $optimizableAvif) = $mediaItem->getCountOptimizeData('avif');
|
||||
|
||||
if ($mediaItem->isProcessable() && ! $mediaItem->isOptimizePrevented())
|
||||
{
|
||||
$action = self::getAction('optimizethumbs', $id);
|
||||
if ($optimizable > 0)
|
||||
{
|
||||
$total = $optimizable + $optimizableWebp + $optimizableAvif;
|
||||
if ($optimizableWebp > 0 || $optimizableAvif > 0)
|
||||
$itemText = __('items', 'shortpixel-image-optimiser');
|
||||
else {
|
||||
$itemText = __('thumbnails', 'shortpixel-image-optimiser');
|
||||
}
|
||||
$action['text'] = sprintf(__('Optimize %s %s','shortpixel-image-optimiser'),$total, $itemText);
|
||||
}
|
||||
else
|
||||
{
|
||||
if ($optimizableWebp > 0 && $optimizableAvif > 0)
|
||||
$text = sprintf(__('Optimize %s webps and %s avif','shortpixel-image-optimiser'),$optimizableWebp, $optimizableAvif);
|
||||
elseif ($optimizableWebp > 0)
|
||||
$text = sprintf(__('Optimize %s webps','shortpixel-image-optimiser'),$optimizableWebp);
|
||||
else
|
||||
$text = sprintf(__('Optimize %s avifs','shortpixel-image-optimiser'),$optimizableAvif);
|
||||
$action['text'] = $text;
|
||||
}
|
||||
$list_actions['optimizethumbs'] = $action;
|
||||
}
|
||||
|
||||
|
||||
if ($mediaItem->isRestorable())
|
||||
{
|
||||
if ($mediaItem->get('type') == 'custom')
|
||||
{
|
||||
if ($mediaItem->getExtension() !== 'pdf') // no support for this
|
||||
$list_actions['comparer'] = self::getAction('compare-custom', $id);
|
||||
}
|
||||
else
|
||||
{
|
||||
// PDF without thumbnail can't be compared.
|
||||
$showCompare = true;
|
||||
if ($mediaItem->getExtension() == 'pdf')
|
||||
{
|
||||
if (! $mediaItem->getThumbnail('full'))
|
||||
$showCompare = false;
|
||||
elseif(! $mediaItem->getThumbnail('full')->hasBackup())
|
||||
$showCompare = false;
|
||||
}
|
||||
|
||||
if ($showCompare)
|
||||
$list_actions['comparer'] = self::getAction('compare', $id);
|
||||
}
|
||||
if ($mediaItem->isRestorable())
|
||||
{
|
||||
|
||||
$compressionType = $mediaItem->getMeta('compressionType');
|
||||
switch($compressionType)
|
||||
{
|
||||
case ImageModel::COMPRESSION_LOSSLESS:
|
||||
$list_actions['reoptimize-lossy'] = self::getAction('reoptimize-lossy', $id);
|
||||
$list_actions['reoptimize-glossy'] = self::getAction('reoptimize-glossy', $id);
|
||||
|
||||
break;
|
||||
case ImageModel::COMPRESSION_LOSSY:
|
||||
$list_actions['reoptimize-lossless'] = self::getAction('reoptimize-lossless', $id);
|
||||
$list_actions['reoptimize-glossy'] = self::getAction('reoptimize-glossy', $id);
|
||||
|
||||
break;
|
||||
case ImageModel::COMPRESSION_GLOSSY:
|
||||
$list_actions['reoptimize-lossy'] = self::getAction('reoptimize-lossy', $id);
|
||||
$list_actions['reoptimize-lossless'] = self::getAction('reoptimize-lossless', $id);
|
||||
break;
|
||||
}
|
||||
|
||||
if ($mediaItem->get('type') === 'media')
|
||||
{
|
||||
$list_actions['reoptimize-smartcrop'] = self::getAction('reoptimize-smartcrop', $id, array('compressionType' => $compressionType));
|
||||
$list_actions['reoptimize-smartcropless'] = self::getAction('reoptimize-smartcropless', $id, array('compressionType' => $compressionType));
|
||||
}
|
||||
$list_actions['restore'] = self::getAction('restore', $id);
|
||||
} // isRestorable
|
||||
else
|
||||
{
|
||||
|
||||
}
|
||||
} // hasBackup
|
||||
|
||||
if (\wpSPIO()->env()->is_debug && $mediaItem->get('type') == 'media')
|
||||
{
|
||||
$list_actions['redo_legacy'] = self::getAction('redo_legacy', $id);
|
||||
}
|
||||
} //isOptimized
|
||||
|
||||
if(! $quotaControl->hasQuota())
|
||||
{
|
||||
$remove = array('reoptimize-lossy' => '', 'reoptimize-glossy' => '', 'reoptimize-lossless' => '', 'optimizethumbs' => '');
|
||||
$list_actions = array_diff_key($list_actions, $remove);
|
||||
|
||||
}
|
||||
return $list_actions;
|
||||
}
|
||||
|
||||
public static function getActions($mediaItem)
|
||||
{
|
||||
$actions = array();
|
||||
$id = $mediaItem->get('id');
|
||||
$quotaControl = QuotaController::getInstance();
|
||||
$optimizeController = new OptimizeController();
|
||||
|
||||
$keyControl = ApiKeyController::getInstance();
|
||||
if (! $keyControl->keyIsVerified())
|
||||
{
|
||||
return array(); // nothing
|
||||
}
|
||||
|
||||
$access = AccessModel::getInstance();
|
||||
if (! $access->imageIsEditable($mediaItem))
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
if ($id === 0)
|
||||
return array();
|
||||
|
||||
if(! $quotaControl->hasQuota())
|
||||
{
|
||||
$actions['extendquota'] = self::getAction('extendquota', $id);
|
||||
$actions['checkquota'] = self::getAction('checkquota', $id);
|
||||
}
|
||||
elseif($mediaItem->isProcessable() && ! $mediaItem->isSomethingOptimized() && ! $mediaItem->isOptimizePrevented() && ! $optimizeController->isItemInQueue($mediaItem))
|
||||
{
|
||||
$actions['optimize'] = self::getAction('optimize', $id);
|
||||
$actions['markCompleted'] = self::getAction('markCompleted', $id);
|
||||
}
|
||||
elseif ($mediaItem->isUserExcluded() && false === $mediaItem->isSomethingOptimized())
|
||||
{
|
||||
$actions['optimize'] = self::getAction('forceOptimize', $id);
|
||||
}
|
||||
|
||||
|
||||
return $actions;
|
||||
}
|
||||
|
||||
public static function getStatusText($mediaItem)
|
||||
{
|
||||
$keyControl = ApiKeyController::getInstance();
|
||||
$quotaControl = QuotaController::getInstance();
|
||||
$optimizeController = new OptimizeController();
|
||||
$settings = \wpSPIO()->settings();
|
||||
|
||||
$text = '';
|
||||
|
||||
if (! $keyControl->keyIsVerified())
|
||||
{
|
||||
$text = __('Invalid API Key. <a href="options-general.php?page=wp-shortpixel-settings">Check your Settings</a>','shortpixel-image-optimiser');
|
||||
}
|
||||
// This basically happens when a NextGen gallery is not added to Custom Media.
|
||||
elseif ($mediaItem->get('id') === 0)
|
||||
{
|
||||
if ($mediaItem->isProcessable(true) === false)
|
||||
{
|
||||
$text = __('Not Processable: ','shortpixel_image_optimiser');
|
||||
$text .= $mediaItem->getProcessableReason();
|
||||
}
|
||||
else {
|
||||
if (\wpSPIO()->env()->has_nextgen && false == $settings->includeNextGen)
|
||||
{
|
||||
$text = __('This image was not found in our database. Enable "Optimize nextgen galleries" in the settings, or add this folder manually. ', 'shortpixel-image-optimiser');
|
||||
}
|
||||
else {
|
||||
$text = __('This image was not found in our database. Refresh folders, or add this gallery', 'shortpixel-image-optimiser');
|
||||
}
|
||||
}
|
||||
}
|
||||
elseif ($mediaItem->isSomethingOptimized())
|
||||
{
|
||||
$text = UiHelper::renderSuccessText($mediaItem);
|
||||
}
|
||||
elseif (false === $mediaItem->isProcessable() )
|
||||
{
|
||||
$text = __('Not Processable: ','shortpixel_image_optimiser');
|
||||
$text .= $mediaItem->getProcessableReason();
|
||||
}
|
||||
elseif (! $mediaItem->exists())
|
||||
{
|
||||
$text = __('File does not exist.','shortpixel-image-optimiser');
|
||||
}
|
||||
elseif ($mediaItem->getMeta('status') < 0)
|
||||
{
|
||||
$text = $mediaItem->getMeta('errorMessage');
|
||||
}
|
||||
elseif( $optimizeController->isItemInQueue($mediaItem) === true)
|
||||
{
|
||||
$text = '<p>' . __('This item is waiting to be processed', 'shortpixel-image-optimiser') . '</p>';
|
||||
$action = self::getAction('cancelOptimize', $mediaItem->get('id'));
|
||||
|
||||
$text .= '<p><a href="javascript:' . $action['function'] . '">' . $action['text'] . '</a></p>';
|
||||
}
|
||||
|
||||
if ($mediaItem->isOptimizePrevented() !== false)
|
||||
{
|
||||
$retry = self::getAction('retry', $mediaItem->get('id'));
|
||||
$unmark = self::getAction('unMarkCompleted', $mediaItem->get('id'));
|
||||
$redo_legacy = false;
|
||||
|
||||
if ($mediaItem->get('type') == 'media')
|
||||
{
|
||||
$was_converted = get_post_meta($mediaItem->get('id'), '_shortpixel_was_converted', true);
|
||||
$updateTs = 1656892800; // July 4th 2022 - 00:00 GMT
|
||||
|
||||
if ($was_converted < $updateTs)
|
||||
{
|
||||
$meta = $mediaItem->getWPMetaData();
|
||||
if (is_array($meta) && isset($meta['ShortPixel']))
|
||||
{
|
||||
$redo_legacy = self::getAction('redo_legacy', $mediaItem->get('id'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$status = $mediaItem->getMeta('status');
|
||||
$text = ''; // reset text
|
||||
|
||||
if (ImageModel::FILE_STATUS_MARKED_DONE == $status)
|
||||
{
|
||||
$text .= "<div class='shortpixel-image-notice'>" . esc_html($mediaItem->getReason('processable'));
|
||||
|
||||
$text .= "<p class='shortpixel-error-reset'>" . sprintf(__('%s Click to unmark as completed %s', 'shortpixel-image-optimiser'), '<a href="javascript:' . $unmark['function'] . '">', '</a>') . '</p>';
|
||||
$text .= '</div>';
|
||||
}
|
||||
else {
|
||||
$text .= "<div class='shortpixel-image-error'>" . esc_html($mediaItem->getReason('processable'));
|
||||
$text .= "<span class='shortpixel-error-reset'>" . sprintf(__('After you have fixed this issue, you can %s click here to retry %s', 'shortpixel-image-optimiser'), '<a href="javascript:' . $retry['function'] . '">', '</a>') . '</span>';
|
||||
$text .= '</div>';
|
||||
}
|
||||
|
||||
|
||||
|
||||
if ($redo_legacy !== false)
|
||||
{
|
||||
$text .= "<div class='shortpixel-image-error'><span class='shortpixel-error-reset'>";
|
||||
|
||||
$text .= sprintf(esc_html__('It seems you have older converted legacy data, which might cause this issue. You can try to %s %s %s . If nothing changes, this is not the cause. ','shortpixel-image-optimiser'), '<a href="javascript:' . $redo_legacy['function'] . '">', $redo_legacy['text'], '</a>');
|
||||
$text .= "</span></div>";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
// Defines all possible actions in the Ui
|
||||
public static function getAction($name, $id, $args = array())
|
||||
{
|
||||
$action = array('function' => '', 'type' => '', 'text' => '', 'display' => '');
|
||||
$keyControl = ApiKeyController::getInstance();
|
||||
|
||||
$compressionType = isset($args['compressionType']) ? $args['compressionType'] : null;
|
||||
|
||||
switch($name)
|
||||
{
|
||||
case 'optimize':
|
||||
$action['function'] = 'window.ShortPixelProcessor.screen.Optimize(' . $id . ')';
|
||||
$action['type'] = 'js';
|
||||
$action['text'] = __('Optimize Now', 'shortpixel-image-optimiser');
|
||||
$action['display'] = 'button';
|
||||
break;
|
||||
case 'forceOptimize':
|
||||
$action['function'] = 'window.ShortPixelProcessor.screen.Optimize(' . $id . ', true)';
|
||||
$action['type'] = 'js';
|
||||
$action['text'] = __('Override exclusions and optimize now', 'shortpixel-image-optimiser');
|
||||
$action['display'] = 'button';
|
||||
break;
|
||||
case 'cancelOptimize':
|
||||
$action['function'] = 'window.ShortPixelProcessor.screen.CancelOptimizeItem(' . $id . ')';
|
||||
$action['type'] = 'js';
|
||||
$action['text'] = __('Cancel item optimization', 'shortpixel-image-optimiser');
|
||||
$action['display'] = 'button';
|
||||
break;
|
||||
case 'markCompleted':
|
||||
$action['function'] = 'window.ShortPixelProcessor.screen.MarkCompleted(' . $id . ')';
|
||||
$action['type'] = 'js';
|
||||
$action['text'] = __('Mark as Completed', 'shortpixel-image-optimiser');
|
||||
$action['display'] = 'button-secondary';
|
||||
$action['layout'] = 'paragraph';
|
||||
$action['title'] = __('This will cause the plugin to skip this image for optimization', 'shortpixel-image-optimiser');
|
||||
break;
|
||||
case 'unMarkCompleted':
|
||||
$action['function'] = 'window.ShortPixelProcessor.screen.UnMarkCompleted(' . $id . ')';
|
||||
$action['type'] = 'js';
|
||||
$action['text'] = __('Click to unmark this item as done', 'shortpixel-image-optimiser');
|
||||
$action['display'] = 'js';
|
||||
|
||||
break;
|
||||
case 'optimizethumbs':
|
||||
$action['function'] = 'window.ShortPixelProcessor.screen.Optimize(' . $id . ');';
|
||||
$action['type'] = 'js';
|
||||
$action['text'] = '';
|
||||
$action['display'] = 'inline';
|
||||
break;
|
||||
|
||||
case 'retry':
|
||||
$action['function'] = 'window.ShortPixelProcessor.screen.Optimize(' . $id . ');';
|
||||
$action['type'] = 'js';
|
||||
$action['text'] = __('Retry', 'shortpixel-image-optimiser') ;
|
||||
$action['display'] = 'button';
|
||||
break;
|
||||
case 'redo_legacy':
|
||||
$action['function'] = 'window.ShortPixelProcessor.screen.RedoLegacy(' . $id . ');';
|
||||
$action['type'] = 'js';
|
||||
$action['text'] = __('Redo Conversion', 'shortpixel-image-optimiser') ;
|
||||
$action['display'] = 'button';
|
||||
break;
|
||||
|
||||
case 'restore':
|
||||
$action['function'] = 'window.ShortPixelProcessor.screen.RestoreItem(' . $id . ');';
|
||||
$action['type'] = 'js';
|
||||
$action['text'] = __('Restore backup','shortpixel-image-optimiser');
|
||||
$action['display'] = 'inline';
|
||||
break;
|
||||
|
||||
case 'compare':
|
||||
$action['function'] = 'ShortPixel.loadComparer(' . $id . ')';
|
||||
$action['type'] = 'js';
|
||||
$action['text'] = __('Compare', 'shortpixel-image-optimiser');
|
||||
$action['display'] = 'inline';
|
||||
break;
|
||||
case 'compare-custom':
|
||||
$action['function'] = 'ShortPixel.loadComparer(' . $id . ',"custom")';
|
||||
$action['type'] = 'js';
|
||||
$action['text'] = __('Compare', 'shortpixel-image-optimiser');
|
||||
$action['display'] = 'inline';
|
||||
break;
|
||||
case 'reoptimize-glossy':
|
||||
$action['function'] = 'window.ShortPixelProcessor.screen.ReOptimize(' . $id . ',' . ImageModel::COMPRESSION_GLOSSY . ')';
|
||||
$action['type'] = 'js';
|
||||
$action['text'] = __('Re-optimize Glossy','shortpixel-image-optimiser') ;
|
||||
$action['display'] = 'inline';
|
||||
break;
|
||||
case 'reoptimize-lossy':
|
||||
$action['function'] = 'window.ShortPixelProcessor.screen.ReOptimize(' . $id . ',' . ImageModel::COMPRESSION_LOSSY . ')';
|
||||
$action['type'] = 'js';
|
||||
$action['text'] = __('Re-optimize Lossy','shortpixel-image-optimiser');
|
||||
$action['display'] = 'inline';
|
||||
break;
|
||||
|
||||
case 'reoptimize-lossless':
|
||||
$action['function'] = 'window.ShortPixelProcessor.screen.ReOptimize(' . $id . ',' . ImageModel::COMPRESSION_LOSSLESS . ')';
|
||||
$action['type'] = 'js';
|
||||
$action['text'] = __('Re-optimize Lossless','shortpixel-image-optimiser');
|
||||
$action['display'] = 'inline';
|
||||
break;
|
||||
case 'reoptimize-smartcrop':
|
||||
$action['function'] = 'window.ShortPixelProcessor.screen.ReOptimize(' . $id . ',' . $compressionType . ',' . ImageModel::ACTION_SMARTCROP . ')';
|
||||
$action['type'] = 'js';
|
||||
$action['text'] = __('Re-optimize with SmartCrop','shortpixel-image-optimiser');
|
||||
$action['display'] = 'inline';
|
||||
break;
|
||||
case 'reoptimize-smartcropless':
|
||||
$action['function'] = 'window.ShortPixelProcessor.screen.ReOptimize(' . $id . ',' . $compressionType . ',' . ImageModel::ACTION_SMARTCROPLESS . ')';
|
||||
$action['type'] = 'js';
|
||||
$action['text'] = __('Re-optimize without SmartCrop','shortpixel-image-optimiser');
|
||||
$action['display'] = 'inline';
|
||||
break;
|
||||
case 'extendquota':
|
||||
$action['function'] = 'https://shortpixel.com/login/'. $keyControl->getKeyForDisplay();
|
||||
$action['type'] = 'button';
|
||||
$action['text'] = __('Extend Quota','shortpixel-image-optimiser');
|
||||
$action['display'] = 'button';
|
||||
break;
|
||||
case 'checkquota':
|
||||
$action['function'] = 'ShortPixel.checkQuota()';
|
||||
$action['type'] = 'js';
|
||||
$action['display'] = 'button';
|
||||
$action['text'] = __('Check Quota','shortpixel-image-optimiser');
|
||||
break;
|
||||
}
|
||||
|
||||
return $action;
|
||||
}
|
||||
|
||||
public static function getConvertErrorReason($error)
|
||||
{
|
||||
switch($error)
|
||||
{
|
||||
case -1: //ERROR_LIBRARY:
|
||||
$reason = __('PNG Library is not present or not working', 'shortpixel-image-optimiser');
|
||||
break;
|
||||
case -2: //ERROR_PATHFAIL:
|
||||
$reason = __('Could not create path', 'shortpixel-image-optimiser');
|
||||
break;
|
||||
case -3: //ERROR_RESULTLARGER:
|
||||
$reason = __('Result file is larger','shortpixel-image-optimiser');
|
||||
break;
|
||||
case -4: // ERROR_WRITEERROR
|
||||
$reason = __('Could not write result file', 'shortpixel-image-optimiser');
|
||||
break;
|
||||
case -5: // ERROR_BACKUPERROR
|
||||
$reason = __('Could not create backup', 'shortpixel-image-optimiser');
|
||||
break;
|
||||
case -6: // ERROR_TRANSPARENT
|
||||
$reason = __('Image is transparent', 'shortpixel-image-optimiser');
|
||||
break;
|
||||
default:
|
||||
$reason = sprintf(__('Unknown error %s', 'shortpixel-image-optimiser'), $error);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
$message = sprintf(__('Not converted: %s ', 'shortpixel-image-optimiser'), $reason);
|
||||
return $message;
|
||||
}
|
||||
|
||||
public static function getKBSearchLink($subject)
|
||||
{
|
||||
return esc_url(self::$knowledge_url . sanitize_text_field($subject));
|
||||
}
|
||||
|
||||
// @param MediaLibraryModel Object $imageItem
|
||||
// @param String $size Preferred size
|
||||
// @param String Preload The preloader tries to guess what the preview might be for a more smooth process. Ignore optimize / backup
|
||||
public static function findBestPreview($imageItem, $size = 800, $preload = false)
|
||||
{
|
||||
$closestObj = $imageItem;
|
||||
|
||||
// set the standard.
|
||||
if ($imageItem->getExtension() == 'pdf') // try not to select non-showable extensions.
|
||||
$bestdiff = 0;
|
||||
else
|
||||
$bestdiff = abs($imageItem->get('width') - $size);
|
||||
|
||||
$thumbnails = $imageItem->get('thumbnails');
|
||||
|
||||
if (! is_array($thumbnails))
|
||||
{
|
||||
return $closestObj; // nothing more to do.
|
||||
}
|
||||
|
||||
foreach($thumbnails as $thumbnail)
|
||||
{
|
||||
if (! $preload && (! $thumbnail->isOptimized() || ! $thumbnail->hasBackup()))
|
||||
continue;
|
||||
|
||||
$diff = abs($thumbnail->get('width') - $size);
|
||||
if ($diff < $bestdiff)
|
||||
{
|
||||
$closestObj = $thumbnail;
|
||||
$bestdiff = $diff;
|
||||
}
|
||||
}
|
||||
|
||||
return $closestObj;
|
||||
}
|
||||
|
||||
public static function formatTS($ts)
|
||||
{
|
||||
//$format = get_option('date_format') .' @ ' . date_i18n(get_option('time_format');
|
||||
if (function_exists('wp_date'))
|
||||
{
|
||||
$date = wp_date(get_option('date_format'), $ts);
|
||||
$date .= ' @ ' . wp_date(get_option('time_format'), $ts);
|
||||
}
|
||||
else
|
||||
{
|
||||
$date = date_i18n(get_option('date_format'), $ts);
|
||||
$date .= ' @ ' . date_i18n(get_option('time_format'), $ts);
|
||||
|
||||
}
|
||||
return $date;
|
||||
}
|
||||
|
||||
public static function formatBytes($bytes, $precision = 2) {
|
||||
$units = array('B', 'KB', 'MB', 'GB', 'TB');
|
||||
|
||||
$bytes = max($bytes, 0);
|
||||
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
|
||||
$pow = min($pow, count($units) - 1);
|
||||
|
||||
$bytes /= pow(1024, $pow);
|
||||
|
||||
return number_format_i18n(round($bytes, $precision), $precision) . ' ' . $units[$pow];
|
||||
}
|
||||
|
||||
public static function formatNumber($number, $precision = 2)
|
||||
{
|
||||
global $wp_locale;
|
||||
$decimalpoint = isset($wp_locale->number_format['decimal_point']) ? $wp_locale->number_format['decimal_point'] : false;
|
||||
$number = number_format_i18n( (float) $number, $precision);
|
||||
|
||||
$hasDecimal = (strpos($number, $decimalpoint) === false) ? false : true;
|
||||
|
||||
// Don't show trailing zeroes if number is a whole unbroken number. -> string comparison because number_format_i18n returns string.
|
||||
if ($decimalpoint !== false && $hasDecimal && substr($number, strpos($number, $decimalpoint) + 1) === '00')
|
||||
{
|
||||
$number = substr($number, 0, strpos($number, $decimalpoint));
|
||||
}
|
||||
// Some locale's have no-breaking-space as thousands separator. This doesn't work well in JS / Cron Shell so replace with space.
|
||||
$number = str_replace(' ', ' ', $number);
|
||||
return $number;
|
||||
}
|
||||
|
||||
public static function formatDate( $date ) {
|
||||
|
||||
if ( '0000-00-00 00:00:00' === $date->format('Y-m-d ') ) {
|
||||
$h_time = '';
|
||||
} else {
|
||||
$time = $date->format('U'); //get_post_time( 'G', true, $post, false );
|
||||
if ( ( abs( $t_diff = time() - $time ) ) < DAY_IN_SECONDS ) {
|
||||
if ( $t_diff < 0 ) {
|
||||
$h_time = sprintf( __( '%s from now' ), human_time_diff( $time ) );
|
||||
} else {
|
||||
$h_time = sprintf( __( '%s ago' ), human_time_diff( $time ) );
|
||||
}
|
||||
} else {
|
||||
$h_time = $date->format( 'Y/m/d' );
|
||||
}
|
||||
}
|
||||
|
||||
return $h_time;
|
||||
}
|
||||
|
||||
protected static function convertImageTypeName($name, $type)
|
||||
{
|
||||
if ($type == 'webp')
|
||||
{
|
||||
$is_double = \wpSPIO()->env()->useDoubleWebpExtension();
|
||||
}
|
||||
if ($type == 'avif')
|
||||
{
|
||||
$is_double = \wpSPIO()->env()->useDoubleAvifExtension();
|
||||
}
|
||||
|
||||
if ($is_double)
|
||||
{
|
||||
return $name . '.' . $type;
|
||||
}
|
||||
else
|
||||
{
|
||||
return substr($name, 0, strrpos($name, '.')) . '.' . $type;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* Strings on settings page that need to be available for both JS and PHP */
|
||||
public static function getSettingsStrings($name = false)
|
||||
{
|
||||
|
||||
$strings = array(
|
||||
);
|
||||
|
||||
$exclusion_types = array(
|
||||
'name' => __('Name', 'shortpixel-image-optimiser'),
|
||||
'path' => __('Path', 'shortpixel-image-optimiser'),
|
||||
'size' => __('Size', 'shortpixel-image-optimiser'),
|
||||
);
|
||||
|
||||
$exclusion_apply = array(
|
||||
'all' => __('All', 'shortpixel-image-optimiser'),
|
||||
'only-thumbs' => __('Only Thumbnails', 'shortpixel-image-optimiser'),
|
||||
'only-custom' => __('Only Custom Media Images', 'shortpixel-image-optimiser'),
|
||||
'selected-thumbs' => __('Selected Images', 'shortpixel-image-optimiser'),
|
||||
);
|
||||
|
||||
$strings['exclusion_types'] = $exclusion_types;
|
||||
$strings['exclusion_apply'] = $exclusion_apply;
|
||||
|
||||
if ($name !== false && isset($strings[$name]))
|
||||
{
|
||||
return $strings[$name];
|
||||
}
|
||||
|
||||
return $strings;
|
||||
}
|
||||
|
||||
|
||||
|
||||
} // class
|
@ -0,0 +1,277 @@
|
||||
<?php
|
||||
namespace ShortPixel\Helper;
|
||||
|
||||
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
|
||||
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
// Our newest Tools class
|
||||
class UtilHelper
|
||||
{
|
||||
|
||||
public static function getPostMetaTable()
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
return $wpdb->prefix . 'shortpixel_postmeta';
|
||||
}
|
||||
|
||||
public static function shortPixelIsPluginActive($plugin) {
|
||||
$activePlugins = apply_filters( 'active_plugins', get_option( 'active_plugins', array()));
|
||||
if ( is_multisite() ) {
|
||||
$activePlugins = array_merge($activePlugins, get_site_option( 'active_sitewide_plugins'));
|
||||
}
|
||||
return in_array( $plugin, $activePlugins);
|
||||
}
|
||||
|
||||
static public function timestampToDB($timestamp)
|
||||
{
|
||||
return date("Y-m-d H:i:s", $timestamp);
|
||||
}
|
||||
|
||||
static public function DBtoTimestamp($date)
|
||||
{
|
||||
return strtotime($date);
|
||||
}
|
||||
|
||||
public static function getWordPressImageSizes()
|
||||
{
|
||||
global $_wp_additional_image_sizes;
|
||||
|
||||
$sizes_names = get_intermediate_image_sizes();
|
||||
$sizes = array();
|
||||
foreach ( $sizes_names as $size ) {
|
||||
$sizes[ $size ][ 'width' ] = intval( get_option( "{$size}_size_w" ) );
|
||||
$sizes[ $size ][ 'height' ] = intval( get_option( "{$size}_size_h" ) );
|
||||
$sizes[ $size ][ 'crop' ] = get_option( "{$size}_crop" ) ? get_option( "{$size}_crop" ) : false;
|
||||
$sizes[ $size ][ 'nice-name'] = ucfirst($size);
|
||||
}
|
||||
if(function_exists('wp_get_additional_image_sizes')) {
|
||||
$sizes = array_merge($sizes, wp_get_additional_image_sizes());
|
||||
} elseif(is_array($_wp_additional_image_sizes)) {
|
||||
$sizes = array_merge($sizes, $_wp_additional_image_sizes);
|
||||
}
|
||||
|
||||
$sizes = apply_filters('shortpixel/settings/image_sizes', $sizes);
|
||||
return $sizes;
|
||||
}
|
||||
|
||||
// wp_normalize_path doesn't work for windows installs in some situations, so we can use it, but we still want some of the functions.
|
||||
public static function spNormalizePath($path)
|
||||
{
|
||||
$path = preg_replace( '|(?<=.)/+|', '/', $path );
|
||||
return $path;
|
||||
}
|
||||
|
||||
// Copy of private https://developer.wordpress.org/reference/functions/_wp_relative_upload_path/
|
||||
public static function getRelativeUploadPath($path)
|
||||
{
|
||||
$new_path = $path;
|
||||
$uploads = wp_get_upload_dir();
|
||||
if ( 0 === strpos( $new_path, $uploads['basedir'] ) ) {
|
||||
$new_path = str_replace( $uploads['basedir'], '', $new_path );
|
||||
$new_path = ltrim( $new_path, '/' );
|
||||
}
|
||||
return $new_path;
|
||||
}
|
||||
|
||||
public static function getExclusions($args = array())
|
||||
{
|
||||
$defaults = array(
|
||||
'filter' => false,
|
||||
'thumbname' => null,
|
||||
'is_thumbnail' => false,
|
||||
'is_custom' => false,
|
||||
);
|
||||
|
||||
$args = wp_parse_args($args, $defaults);
|
||||
|
||||
$patterns = \wpSPIO()->settings()->excludePatterns;
|
||||
$matches = array();
|
||||
|
||||
if (false === is_array($patterns))
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
foreach($patterns as $index => $pattern)
|
||||
{
|
||||
if (! isset($pattern['apply']))
|
||||
{
|
||||
$patterns[$index]['apply'] = 'all';
|
||||
}
|
||||
|
||||
if (true === $args['filter'])
|
||||
{
|
||||
if (true === self::matchExclusion($patterns[$index], $args))
|
||||
{
|
||||
$matches[] = $pattern;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (true === $args['filter'])
|
||||
{
|
||||
return $matches;
|
||||
}
|
||||
else
|
||||
return $patterns;
|
||||
}
|
||||
|
||||
protected static function matchExclusion($pattern, $options)
|
||||
{
|
||||
$apply = $pattern['apply'];
|
||||
$thumblist = isset($pattern['thumblist']) ? $pattern['thumblist'] : array();
|
||||
$bool = false;
|
||||
|
||||
if ($apply === 'all')
|
||||
{
|
||||
$bool = true;
|
||||
}
|
||||
elseif ($apply == 'only-thumbs' && true === $options['is_thumbnail'])
|
||||
{
|
||||
$bool = true;
|
||||
}
|
||||
elseif ($apply == 'only-custom' && true === $options['is_custom'])
|
||||
{
|
||||
$bool = true;
|
||||
}
|
||||
elseif (count($thumblist) > 0 && ! is_null($options['thumbname']))
|
||||
{
|
||||
$thumbname = $options['thumbname'];
|
||||
if (in_array($thumbname, $thumblist))
|
||||
{
|
||||
$bool = true;
|
||||
}
|
||||
}
|
||||
return $bool;
|
||||
}
|
||||
|
||||
|
||||
public static function alterHtaccess($webp = false, $avif = false)
|
||||
{
|
||||
// [BS] Backward compat. 11/03/2019 - remove possible settings from root .htaccess
|
||||
/* Plugin init is before loading these admin scripts. So it can happen misc.php is not yet loaded */
|
||||
if (! function_exists('insert_with_markers'))
|
||||
{
|
||||
Log::addWarn('AlterHtaccess Called before WP init');
|
||||
return;
|
||||
//require_once( ABSPATH . 'wp-admin/includes/misc.php' );
|
||||
}
|
||||
$upload_dir = wp_upload_dir();
|
||||
$upload_base = trailingslashit($upload_dir['basedir']);
|
||||
|
||||
if (false === $webp && false === $avif ) {
|
||||
insert_with_markers( get_home_path() . '.htaccess', 'ShortPixelWebp', '');
|
||||
|
||||
// Only empty these tags if the file actually exist, they are created by SPIO.
|
||||
if (file_exists($upload_base . '.htaccess'))
|
||||
{
|
||||
insert_with_markers( $upload_base . '.htaccess', 'ShortPixelWebp', '');
|
||||
}
|
||||
|
||||
|
||||
if (file_exists(trailingslashit(WP_CONTENT_DIR) . '.htaccess'))
|
||||
{
|
||||
insert_with_markers( trailingslashit(WP_CONTENT_DIR) . '.htaccess', 'ShortPixelWebp', '');
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
$avif_rules = '
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
##### Directives for delivering AVIF files, if they exist #####
|
||||
# Does the browser support avif?
|
||||
RewriteCond %{HTTP_ACCEPT} image/avif
|
||||
# AND is the request a jpg or png? (also grab the basepath %1 to match in the next rule)
|
||||
RewriteCond %{REQUEST_URI} ^(.+)\.(?:jpe?g|png|gif)$
|
||||
# AND does a .avif image exist?
|
||||
RewriteCond %{DOCUMENT_ROOT}/%1.avif -f
|
||||
# THEN send the avif image and set the env var avif
|
||||
RewriteRule (.+)\.(?:jpe?g|png)$ $1.avif [NC,T=image/avif,E=avif,L]
|
||||
|
||||
# Does the browser support avif?
|
||||
RewriteCond %{HTTP_ACCEPT} image/avif
|
||||
# AND is the request a jpg or png? (also grab the basepath %1 to match in the next rule)
|
||||
RewriteCond %{REQUEST_URI} ^(.+)\.(?:jpe?g|png|gif)$
|
||||
# AND does a .jpg.avif image exist?
|
||||
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI}.avif -f
|
||||
# THEN send the avif image and set the env var avif
|
||||
RewriteRule ^(.+)$ $1.avif [NC,T=image/avif,E=avif,L]
|
||||
|
||||
</IfModule>
|
||||
<IfModule mod_headers.c>
|
||||
# If REDIRECT_avif env var exists, append Accept to the Vary header
|
||||
Header append Vary Accept env=REDIRECT_avif
|
||||
</IfModule>
|
||||
<IfModule mod_mime.c>
|
||||
AddType image/avif .avif
|
||||
</IfModule>
|
||||
';
|
||||
|
||||
$webp_rules = '
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
##### TRY FIRST the file appended with .webp (ex. test.jpg.webp) #####
|
||||
# Is the browser Chrome?
|
||||
RewriteCond %{HTTP_USER_AGENT} Chrome [OR]
|
||||
# OR Is request from Page Speed
|
||||
RewriteCond %{HTTP_USER_AGENT} "Google Page Speed Insights" [OR]
|
||||
# OR does this browser explicitly support webp
|
||||
RewriteCond %{HTTP_ACCEPT} image/webp
|
||||
# AND NOT MS EDGE 42/17 - doesnt work.
|
||||
RewriteCond %{HTTP_USER_AGENT} !Edge/17
|
||||
# AND is the request a jpg, png or gif?
|
||||
RewriteCond %{REQUEST_URI} ^(.+)\.(?:jpe?g|png|gif)$
|
||||
# AND does a .ext.webp image exist?
|
||||
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI}.webp -f
|
||||
# THEN send the webp image and set the env var webp
|
||||
RewriteRule ^(.+)$ $1.webp [NC,T=image/webp,E=webp,L]
|
||||
##### IF NOT, try the file with replaced extension (test.webp) #####
|
||||
RewriteCond %{HTTP_USER_AGENT} Chrome [OR]
|
||||
RewriteCond %{HTTP_USER_AGENT} "Google Page Speed Insights" [OR]
|
||||
RewriteCond %{HTTP_ACCEPT} image/webp
|
||||
RewriteCond %{HTTP_USER_AGENT} !Edge/17
|
||||
# AND is the request a jpg, png or gif? (also grab the basepath %1 to match in the next rule)
|
||||
RewriteCond %{REQUEST_URI} ^(.+)\.(?:jpe?g|png|gif)$
|
||||
# AND does a .webp image exist?
|
||||
RewriteCond %{DOCUMENT_ROOT}/%1.webp -f
|
||||
# THEN send the webp image and set the env var webp
|
||||
RewriteRule (.+)\.(?:jpe?g|png|gif)$ $1.webp [NC,T=image/webp,E=webp,L]
|
||||
</IfModule>
|
||||
<IfModule mod_headers.c>
|
||||
# If REDIRECT_webp env var exists, append Accept to the Vary header
|
||||
Header append Vary Accept env=REDIRECT_webp
|
||||
</IfModule>
|
||||
<IfModule mod_mime.c>
|
||||
AddType image/webp .webp
|
||||
</IfModule>
|
||||
' ;
|
||||
|
||||
$rules = '';
|
||||
// if ($avif)
|
||||
$rules .= $avif_rules;
|
||||
// if ($webp)
|
||||
$rules .= $webp_rules;
|
||||
|
||||
insert_with_markers( get_home_path() . '.htaccess', 'ShortPixelWebp', $rules);
|
||||
|
||||
/** In uploads and on, it needs Inherit. Otherwise things such as the 404 error page will not be loaded properly
|
||||
* since the WP rewrite will not be active at that point (overruled) **/
|
||||
$rules = str_replace('RewriteEngine On', 'RewriteEngine On' . PHP_EOL . 'RewriteOptions Inherit', $rules);
|
||||
|
||||
// Can shortcircuit (return false) the creation of subdirectory Htaccess files if this causes issues and is not needed.
|
||||
$bool = apply_filters('shortpixel/install/write_deep_htaccess', true);
|
||||
|
||||
if (true === $bool)
|
||||
{
|
||||
insert_with_markers( $upload_base . '.htaccess', 'ShortPixelWebp', $rules);
|
||||
insert_with_markers( trailingslashit(WP_CONTENT_DIR) . '.htaccess', 'ShortPixelWebp', $rules);
|
||||
}
|
||||
}
|
||||
}
|
||||
} // class
|
153
wp-content/plugins/shortpixel-image-optimiser/class/Model.php
Normal file
153
wp-content/plugins/shortpixel-image-optimiser/class/Model.php
Normal file
@ -0,0 +1,153 @@
|
||||
<?php
|
||||
namespace ShortPixel;
|
||||
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
abstract class Model
|
||||
{
|
||||
protected $model = array();
|
||||
|
||||
public function getData()
|
||||
{
|
||||
$data = array();
|
||||
foreach($this->model as $item => $options)
|
||||
{
|
||||
|
||||
$data[$item] = $this->{$item};
|
||||
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function getModel()
|
||||
{
|
||||
return array_keys($this->model); // only the variable names are public.
|
||||
}
|
||||
|
||||
protected function sanitize($name, $value)
|
||||
{
|
||||
if (! isset($this->model[$name]))
|
||||
return null;
|
||||
|
||||
// if no sanitize method is set, default to strictest string.
|
||||
$sanitize = isset($this->model[$name]['s']) ? $this->model[$name]['s'] : 'string';
|
||||
switch($sanitize)
|
||||
{
|
||||
case "string":
|
||||
$value = $this->sanitizeString($value);
|
||||
break;
|
||||
case "int":
|
||||
$value = $this->sanitizeInteger($value);
|
||||
break;
|
||||
case "boolean":
|
||||
$value = $this->sanitizeBoolean($value);
|
||||
break;
|
||||
case 'array':
|
||||
case 'Array':
|
||||
$value = $this->sanitizeArray($value);
|
||||
break;
|
||||
case 'exception': // for exceptional situations. The data will not be sanitized! Need to do this elsewhere.
|
||||
return $value;
|
||||
break;
|
||||
case 'skip': // skips should not be in any save candidate and not be sanitized.
|
||||
return null;
|
||||
break;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/** Sanitize the passed post data against the model attribute formats.
|
||||
*
|
||||
* @param Array $post The Post data
|
||||
* @param boolean $missing If fields are missing, include them empty in the output
|
||||
* @return Array Sanitized Post Data
|
||||
*/
|
||||
public function getSanitizedData($post, $missing = true)
|
||||
{
|
||||
$postData = array();
|
||||
foreach($post as $name => $value)
|
||||
{
|
||||
$name = sanitize_text_field($name);
|
||||
$value = $this->sanitize($name, $value);
|
||||
if ($value !== null)
|
||||
$postData[$name] = $value;
|
||||
else {
|
||||
Log::addWarn("Provided field $name not part of model " . get_class() );
|
||||
}
|
||||
}
|
||||
|
||||
if ($missing)
|
||||
{
|
||||
$model_fields = $this->getModel();
|
||||
$post_fields = array_keys($postData);
|
||||
|
||||
$missing_fields = array_diff($model_fields, $post_fields);
|
||||
foreach($missing_fields as $index => $field_name)
|
||||
{
|
||||
$field_name = sanitize_text_field($field_name);
|
||||
$type = $this->getType($field_name);
|
||||
if ($type === 'boolean')
|
||||
{
|
||||
$postData[$field_name] = 0;
|
||||
}
|
||||
elseif ($type !== false && $type !== 'skip')
|
||||
{
|
||||
$postData[$field_name] = '';
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return $postData;
|
||||
}
|
||||
|
||||
|
||||
public function getType($name)
|
||||
{
|
||||
if (! isset($this->model[$name]))
|
||||
{
|
||||
return null;
|
||||
Log::addWarn("Provided field $name not part of model " . get_class() );
|
||||
}
|
||||
|
||||
|
||||
$type = isset($this->model[$name]['s']) ? $this->model[$name]['s'] : false;
|
||||
return $type;
|
||||
}
|
||||
|
||||
public function sanitizeString($string)
|
||||
{
|
||||
return (string) sanitize_text_field($string);
|
||||
}
|
||||
public function sanitizeInteger($int)
|
||||
{
|
||||
return intval($int);
|
||||
}
|
||||
public function sanitizeBoolean($bool)
|
||||
{
|
||||
return ($bool) ? true : false;
|
||||
}
|
||||
|
||||
public function sanitizeArray($array)
|
||||
{
|
||||
if (! is_array($array))
|
||||
{
|
||||
Log::addWarn('Field is of type Array, but Array not provided');
|
||||
return null;
|
||||
}
|
||||
$new_array = array();
|
||||
foreach($array as $key => $value)
|
||||
{
|
||||
$newkey = $this->sanitizeString($key);
|
||||
$newval = $this->sanitizeString($value);
|
||||
$new_array[$newkey] = $newval ;
|
||||
}
|
||||
|
||||
return $new_array;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,140 @@
|
||||
<?php
|
||||
namespace ShortPixel\Model;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
|
||||
use ShortPixel\Controller\QuotaController as QuotaController;
|
||||
|
||||
// Central place for user / access checking, roles etc.
|
||||
class AccessModel
|
||||
{
|
||||
|
||||
// Instance of class
|
||||
private static $instance;
|
||||
|
||||
// Array of known SPIO Capabilities mapped to WordPress variants
|
||||
private $caps;
|
||||
|
||||
// int . The current user id
|
||||
private $current_user_id;
|
||||
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->setDefaultPermissions();
|
||||
}
|
||||
|
||||
protected function setDefaultPermissions()
|
||||
{
|
||||
|
||||
$spioCaps = array(
|
||||
'notices' => 'activate_plugins', // used in AdminNoticesController
|
||||
'quota-warning' => 'manage_options', // used in AdminController
|
||||
'image_all' => 'edit_others_posts',
|
||||
'image_user' => 'edit_post',
|
||||
'custom_all' => 'edit_others_posts',
|
||||
'is_admin_user' => 'manage_options',
|
||||
'actions' => array(),
|
||||
);
|
||||
|
||||
$spioCaps = apply_filters('shortpixel/init/permissions', $spioCaps);
|
||||
// $this->cap_actions = bla.
|
||||
$this->caps = $spioCaps;
|
||||
|
||||
}
|
||||
|
||||
public static function getInstance()
|
||||
{
|
||||
if (is_null(self::$instance))
|
||||
{
|
||||
self::$instance = new AccessModel();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/** Check for allowing a notice
|
||||
* @param $notice Object of type notice.
|
||||
*/
|
||||
public function noticeIsAllowed($notice)
|
||||
{
|
||||
$cap = $this->getCap('notices');
|
||||
return $this->user()->has_cap($cap);
|
||||
}
|
||||
|
||||
/*
|
||||
@param SPIO capability to check again the user WordPress permissions.
|
||||
*/
|
||||
public function userIsAllowed($cap)
|
||||
{
|
||||
$cap = $this->getCap($cap);
|
||||
return $this->user()->has_cap($cap);
|
||||
}
|
||||
|
||||
public function imageIsEditable($mediaItem)
|
||||
{
|
||||
$type = $mediaItem->get('type');
|
||||
if ($type == 'custom' )
|
||||
{
|
||||
return $this->user()->has_cap($this->getCap('custom_all'), $mediaItem->get('id'));
|
||||
}
|
||||
elseif ($type == 'media') // media
|
||||
{
|
||||
if ($this->user()->has_cap($this->getCap('image_all'), $mediaItem->get('id')) || $this->user()->has_cap($this->getCap('image_user'), $mediaItem->get('id')) )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function isFeatureAvailable($name)
|
||||
{
|
||||
$available = true;
|
||||
|
||||
switch($name)
|
||||
{
|
||||
case 'avif':
|
||||
$quotaControl = QuotaController::getInstance();
|
||||
$quota = $quotaControl->getQuota();
|
||||
|
||||
if (property_exists($quota, 'unlimited') && $quota->unlimited === true)
|
||||
{
|
||||
$available = false;
|
||||
}
|
||||
|
||||
break;
|
||||
case 'webp':
|
||||
default:
|
||||
|
||||
break;
|
||||
}
|
||||
return $available;
|
||||
}
|
||||
|
||||
|
||||
protected function user()
|
||||
{
|
||||
return wp_get_current_user();
|
||||
}
|
||||
|
||||
/** Find the needed capability
|
||||
*
|
||||
* This translates a SPIO capability into the associated cap that is registered within WordPress.
|
||||
*
|
||||
* @param $cap The required Capability
|
||||
* @param $default The default value if not found. This is defaults to an admin cap to prevent access leaking.
|
||||
*/
|
||||
protected function getCap($cap, $default = 'manage_options')
|
||||
{
|
||||
if (isset($this->caps[$cap]))
|
||||
return $this->caps[$cap];
|
||||
else
|
||||
return $default;
|
||||
}
|
||||
|
||||
|
||||
} // CLASS
|
@ -0,0 +1,155 @@
|
||||
<?php
|
||||
namespace ShortPixel\Model;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
|
||||
use ShortPixel\Notices\NoticeController as Notice;
|
||||
use ShortPixel\Notices\NoticeController as Notices;
|
||||
|
||||
|
||||
abstract class AdminNoticeModel
|
||||
{
|
||||
protected $key; // abstract
|
||||
protected $notice;
|
||||
|
||||
protected $errorLevel = 'normal';
|
||||
protected $suppress_delay = YEAR_IN_SECONDS;
|
||||
protected $callback;
|
||||
|
||||
protected $include_screens = array();
|
||||
protected $exclude_screens = array();
|
||||
|
||||
protected $data;
|
||||
|
||||
// No stuff loading here, low init
|
||||
public function __construct()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
// The main init, ty.
|
||||
public function load()
|
||||
{
|
||||
$noticeController = Notices::getInstance();
|
||||
$notice = $noticeController->getNoticeByID($this->key);
|
||||
|
||||
if (is_object($notice))
|
||||
{
|
||||
$this->notice = $notice;
|
||||
}
|
||||
|
||||
if (is_object($notice) && $notice->isDismissed())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (is_null($this->notice) && $this->checkTrigger() === true)
|
||||
{
|
||||
$this->add();
|
||||
}
|
||||
elseif ( is_object($this->notice) && $this->checkReset() === true)
|
||||
{
|
||||
$this->reset();
|
||||
}
|
||||
}
|
||||
|
||||
public function getKey()
|
||||
{
|
||||
return $this->key;
|
||||
}
|
||||
|
||||
public function reset($key = null)
|
||||
{
|
||||
$key = (is_null($key)) ? $this->key : $key;
|
||||
Notices::removeNoticeByID($key);
|
||||
}
|
||||
|
||||
protected function checkReset()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// For when trigger condition is not applicable.
|
||||
public function addManual($args = array())
|
||||
{
|
||||
foreach($args as $key => $val)
|
||||
{
|
||||
$this->addData($key, $val);
|
||||
}
|
||||
$this->add();
|
||||
}
|
||||
|
||||
public function getNoticeObj()
|
||||
{
|
||||
return $this->notice; // can be null!
|
||||
}
|
||||
|
||||
// Proxy for noticeModel dismissed
|
||||
public function isDismissed()
|
||||
{
|
||||
$notice = $this->getNoticeObj();
|
||||
if (is_null($notice) || $notice->isDismissed() === false)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
protected function add()
|
||||
{
|
||||
|
||||
switch ($this->errorLevel)
|
||||
{
|
||||
case 'warning':
|
||||
$notice = Notices::addWarning($this->getMessage());
|
||||
break;
|
||||
case 'normal';
|
||||
default:
|
||||
$notice = Notices::addNormal($this->getMessage());
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
/// Todo implement include / exclude screens here.
|
||||
if (count($this->exclude_screens) > 0)
|
||||
{
|
||||
$notice->limitScreens('exclude', $this->exclude_screens);
|
||||
}
|
||||
|
||||
if (count($this->include_screens) > 0)
|
||||
{
|
||||
$notice->limitScreens('include', $this->include_screens);
|
||||
}
|
||||
|
||||
if (! is_null($this->callback))
|
||||
Notices::makePersistent($notice, $this->key, $this->suppress_delay, $this->callback);
|
||||
else
|
||||
Notices::makePersistent($notice, $this->key, $this->suppress_delay);
|
||||
|
||||
$this->notice = $notice;
|
||||
}
|
||||
|
||||
protected function addData($name, $value)
|
||||
{
|
||||
$this->data[$name] = $value;
|
||||
}
|
||||
|
||||
protected function getData($name)
|
||||
{
|
||||
if (isset($this->data[$name]))
|
||||
{
|
||||
return $this->data[$name];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ?? abstract proteced removeCondition();
|
||||
|
||||
abstract protected function checkTrigger();
|
||||
abstract protected function getMessage();
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
namespace ShortPixel\Model\AdminNotices;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
class ApiNotice extends \ShortPixel\Model\AdminNoticeModel
|
||||
{
|
||||
protected $key = 'MSG_NO_APIKEY';
|
||||
|
||||
public function load()
|
||||
{
|
||||
$activationDate = \wpSPIO()->settings()->activationDate;
|
||||
if (! $activationDate)
|
||||
{
|
||||
$activationDate = time();
|
||||
\wpSPIO()->settings()->activationDate = $activationDate;
|
||||
}
|
||||
|
||||
parent::load();
|
||||
}
|
||||
|
||||
protected function checkTrigger()
|
||||
{
|
||||
if (\wpSPIO()->settings()->verifiedKey)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// If not key is verified.
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function getMessage()
|
||||
{
|
||||
$message = "<p>" . __('To start the optimization process, you need to validate your API key on the '
|
||||
. '<a href="options-general.php?page=wp-shortpixel-settings">ShortPixel Settings</a> page in your WordPress admin.','shortpixel-image-optimiser') . "
|
||||
</p>
|
||||
<p>" . __('If you do not have an API key yet, just fill out the form and a key will be created.','shortpixel-image-optimiser') . "</p>";
|
||||
|
||||
return $message;
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
namespace ShortPixel\Model\AdminNotices;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
use ShortPixel\Controller\AdminNoticesController as AdminNoticesController;
|
||||
|
||||
class ApiNoticeRepeat extends \ShortPixel\Model\AdminNoticeModel
|
||||
{
|
||||
protected $key = 'MSG_NO_APIKEY_REPEAT';
|
||||
protected $errorLevel = 'warning';
|
||||
|
||||
protected function checkTrigger()
|
||||
{
|
||||
if (\wpSPIO()->settings()->verifiedKey)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Is set by general ApiNotice. If not set, don't bother with the repeat.
|
||||
$activationDate = \wpSPIO()->settings()->activationDate;
|
||||
if (! $activationDate)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$controller = AdminNoticesController::getInstance();
|
||||
|
||||
$firstNotice = $controller->getNoticeByKey('MSG_NO_APIKEY');
|
||||
|
||||
// Check if first notice is there, and not dismissed, then don't repeat.
|
||||
if ($firstNotice->isDismissed() === false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// After 6 hours
|
||||
if (time() < $activationDate + (6 * HOUR_IN_SECONDS))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// If not key is verified and first one is dismissed, and not this one.
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function getMessage()
|
||||
{
|
||||
$message = __("Action required! Please <a href='https://shortpixel.com/wp-apikey' target='_blank'>get your API key</a> to activate your ShortPixel plugin.",'shortpixel-image-optimiser');
|
||||
|
||||
return $message;
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
namespace ShortPixel\Model\AdminNotices;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
use ShortPixel\Controller\AdminNoticesController as AdminNoticesController;
|
||||
|
||||
class ApiNoticeRepeatLong extends \ShortPixel\Model\AdminNoticeModel
|
||||
{
|
||||
protected $key = 'MSG_NO_APIKEY_REPEAT_LONG';
|
||||
protected $errorLevel = 'warning';
|
||||
|
||||
protected function checkTrigger()
|
||||
{
|
||||
if (\wpSPIO()->settings()->verifiedKey)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Is set by general ApiNotice. If not set, don't bother with the repeat.
|
||||
$activationDate = \wpSPIO()->settings()->activationDate;
|
||||
if (! $activationDate)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$controller = AdminNoticesController::getInstance();
|
||||
|
||||
// Check the original
|
||||
$firstNotice = $controller->getNoticeByKey('MSG_NO_APIKEY');
|
||||
if ($firstNotice->isDismissed() === false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check the Repeat.
|
||||
$secondNotice = $controller->getNoticeByKey('MSG_NO_APIKEY_REPEAT');
|
||||
if ($secondNotice->isDismissed() === false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// After 3 days.
|
||||
if (time() < $activationDate + (3 * DAY_IN_SECONDS))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// If not key is verified and first one is dismissed, and not this one.
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function getMessage()
|
||||
{
|
||||
$message = __("Your image gallery is not optimized. It takes 2 minutes to <a href='https://shortpixel.com/wp-apikey' target='_blank'>get your API key</a> and activate your ShortPixel plugin.",'shortpixel-image-optimiser') . "<BR><BR>";
|
||||
|
||||
return $message;
|
||||
}
|
||||
}
|
@ -0,0 +1,118 @@
|
||||
<?php
|
||||
namespace ShortPixel\Model\AdminNotices;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
use \ShortPixel\Controller\CacheController as CacheController;
|
||||
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
|
||||
|
||||
|
||||
class AvifNotice extends \ShortPixel\Model\AdminNoticeModel
|
||||
{
|
||||
protected $key = 'MSG_AVIF_ERROR';
|
||||
protected $errorLevel = 'error';
|
||||
|
||||
protected $error_message;
|
||||
protected $error_detail;
|
||||
|
||||
// Remove this.
|
||||
public function __construct()
|
||||
{
|
||||
$this->callback = array($this, 'function_crash');
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/*
|
||||
public function function_crash()
|
||||
{
|
||||
echo 'Yall';
|
||||
return false;
|
||||
}
|
||||
*/
|
||||
protected function checkTrigger()
|
||||
{
|
||||
// No Automatic Trigger.
|
||||
return false;
|
||||
}
|
||||
|
||||
public function check()
|
||||
{
|
||||
$cache = new CacheController();
|
||||
if (apply_filters('shortpixel/avifcheck/override', false) === true)
|
||||
{ return; }
|
||||
|
||||
|
||||
if ($cache->getItem('avif_server_check')->exists() === false)
|
||||
{
|
||||
$url = \WPSPIO()->plugin_url('res/img/test.avif');
|
||||
$headers = get_headers($url);
|
||||
$is_error = true;
|
||||
|
||||
$this->addData('headers', $headers);
|
||||
// Defaults.
|
||||
$this->error_message = __('AVIF server test failed. Your server may not be configured to display AVIF files correctly. Serving AVIF might cause your images not to load. Check your images, disable the AVIF option, or update your web server configuration.', 'shortpixel-image-optimiser');
|
||||
$this->error_detail = __('The request did not return valid HTTP headers. Check if the plugin is allowed to access ' . $url, 'shortpixel-image-optimiser');
|
||||
|
||||
$contentType = null;
|
||||
$response = $headers[0];
|
||||
|
||||
if (is_array($headers) )
|
||||
{
|
||||
foreach($headers as $index => $header)
|
||||
{
|
||||
if ( strpos(strtolower($header), 'content-type') !== false )
|
||||
{
|
||||
// This is another header that can interrupt.
|
||||
if (strpos(strtolower($header), 'x-content-type-options') === false)
|
||||
{
|
||||
$contentType = $header;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// http not ok, redirect etc. Shouldn't happen.
|
||||
if (is_null($response) || strpos($response, '200') === false)
|
||||
{
|
||||
$this->error_detail = sprintf(__('AVIF check could not be completed because the plugin could not retrieve %s %s %s. %s Please check the security/firewall settings and try again', 'shortpixel-image-optimiser'), '<a href="' . $url . '">', $url, '</a>', '<br>');
|
||||
}
|
||||
elseif(is_null($contentType) || strpos($contentType, 'avif') === false)
|
||||
{
|
||||
$this->error_detail = sprintf(__('The required Content-type header for AVIF files was not found. Please check this with your hosting and/or CDN provider. For more details on how to fix this issue, %s see this article %s', 'shortpixel_image_optimiser'), '<a href="https://shortpixel.com/blog/avif-mime-type-delivery-apache-nginx/" target="_blank"> ', '</a>');
|
||||
}
|
||||
else
|
||||
{
|
||||
$is_error = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($is_error)
|
||||
{
|
||||
if (is_null($this->notice) || $this->notice->isDismissed() === false)
|
||||
{
|
||||
$this->addManual();
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->reset();
|
||||
|
||||
$item = $cache->getItem('avif_server_check');
|
||||
$item->setValue(time());
|
||||
$item->setExpires(MONTH_IN_SECONDS);
|
||||
$cache->storeItemObject($item );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected function getMessage()
|
||||
{
|
||||
$headers = $this->getData('headers');
|
||||
$message = '<h4>' . $this->error_message . '</h4><p>' . $this->error_detail . '</p><p class="small">' . __('Returned headers for:<br>', 'shortpixel-image-optimiser') . print_r($headers, true) . '</p>';
|
||||
return $message;
|
||||
}
|
||||
}
|
@ -0,0 +1,219 @@
|
||||
<?php
|
||||
namespace ShortPixel\Model\AdminNotices;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
class CompatNotice extends \ShortPixel\Model\AdminNoticeModel
|
||||
{
|
||||
protected $key = 'MSG_COMPAT';
|
||||
protected $errorLevel = 'warning';
|
||||
|
||||
protected function checkTrigger()
|
||||
{
|
||||
$conflictPlugins = $this->getConflictingPlugins();
|
||||
if (count($conflictPlugins) > 0)
|
||||
{
|
||||
$this->addData('conflicts', $conflictPlugins);
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected function getMessage()
|
||||
{
|
||||
$conflicts = $this->getData('conflicts');
|
||||
if (! is_array($conflicts))
|
||||
$conflicts = array();
|
||||
|
||||
$message = __("The following plugins are not compatible with ShortPixel and may cause unexpected results: ",'shortpixel-image-optimiser');
|
||||
$message .= '<ul class="sp-conflict-plugins">';
|
||||
foreach($conflicts as $plugin) {
|
||||
//ShortPixelVDD($plugin);
|
||||
$action = $plugin['action'];
|
||||
$link = ( $action == 'Deactivate' )
|
||||
? wp_nonce_url( admin_url( 'admin-post.php?action=shortpixel_deactivate_conflict_plugin&plugin=' . urlencode( $plugin['path'] ) ), 'sp_deactivate_plugin_nonce' )
|
||||
: $plugin['href'];
|
||||
$message .= '<li class="sp-conflict-plugins-list"><strong>' . $plugin['name'] . '</strong>';
|
||||
$message .= '<a href="' . $link . '" class="button button-primary">' . $action . '</a>';
|
||||
|
||||
if($plugin['details']) $message .= '<br>';
|
||||
if($plugin['details']) $message .= '<span>' . $plugin['details'] . '</span>';
|
||||
}
|
||||
$message .= "</ul>";
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
protected function getConflictingPlugins() {
|
||||
$settings = \wpSPIO()->settings();
|
||||
|
||||
$conflictPlugins = array(
|
||||
'WP Smush - Image Optimization'
|
||||
=> array(
|
||||
'action'=>'Deactivate',
|
||||
'data'=>'wp-smushit/wp-smush.php',
|
||||
'page'=>'wp-smush-bulk'
|
||||
),
|
||||
'Imagify Image Optimizer'
|
||||
=> array(
|
||||
'action'=>'Deactivate',
|
||||
'data'=>'imagify/imagify.php',
|
||||
'page'=>'imagify'
|
||||
),
|
||||
'Compress JPEG & PNG images (TinyPNG)'
|
||||
=> array(
|
||||
'action'=>'Deactivate',
|
||||
'data'=>'tiny-compress-images/tiny-compress-images.php',
|
||||
'page'=>'tinify'
|
||||
),
|
||||
'Kraken.io Image Optimizer'
|
||||
=> array(
|
||||
'action'=>'Deactivate',
|
||||
'data'=>'kraken-image-optimizer/kraken.php',
|
||||
'page'=>'wp-krakenio'
|
||||
),
|
||||
'Optimus - WordPress Image Optimizer'
|
||||
=> array(
|
||||
'action'=>'Deactivate',
|
||||
'data'=>'optimus/optimus.php',
|
||||
'page'=>'optimus'
|
||||
),
|
||||
'Phoenix Media Rename' => array(
|
||||
'action' => 'Deactivate',
|
||||
'data' => 'phoenix-media-rename/phoenix-media-rename.php',
|
||||
),
|
||||
'EWWW Image Optimizer'
|
||||
=> array(
|
||||
'action'=>'Deactivate',
|
||||
'data'=>'ewww-image-optimizer/ewww-image-optimizer.php',
|
||||
'page'=>'ewww-image-optimizer%2F'
|
||||
),
|
||||
'EWWW Image Optimizer Cloud'
|
||||
=> array(
|
||||
'action'=>'Deactivate',
|
||||
'data'=>'ewww-image-optimizer-cloud/ewww-image-optimizer-cloud.php',
|
||||
'page'=>'ewww-image-optimizer-cloud%2F'
|
||||
),
|
||||
'ImageRecycle pdf & image compression'
|
||||
=> array(
|
||||
'action'=>'Deactivate',
|
||||
'data'=>'imagerecycle-pdf-image-compression/wp-image-recycle.php',
|
||||
'page'=>'option-image-recycle'
|
||||
),
|
||||
'CheetahO Image Optimizer'
|
||||
=> array(
|
||||
'action'=>'Deactivate',
|
||||
'data'=>'cheetaho-image-optimizer/cheetaho.php',
|
||||
'page'=>'cheetaho'
|
||||
),
|
||||
'Zara 4 Image Compression'
|
||||
=> array(
|
||||
'action'=>'Deactivate',
|
||||
'data'=>'zara-4/zara-4.php',
|
||||
'page'=>'zara-4'
|
||||
),
|
||||
'CW Image Optimizer'
|
||||
=> array(
|
||||
'action'=>'Deactivate',
|
||||
'data'=>'cw-image-optimizer/cw-image-optimizer.php',
|
||||
'page'=>'cw-image-optimizer'
|
||||
),
|
||||
'Simple Image Sizes'
|
||||
=> array(
|
||||
'action'=>'Deactivate',
|
||||
'data'=>'simple-image-sizes/simple_image_sizes.php'
|
||||
),
|
||||
'Regenerate Thumbnails and Delete Unused'
|
||||
=> array(
|
||||
'action' => 'Deactivate',
|
||||
'data' => 'regenerate-thumbnails-and-delete-unused/regenerate_wpregenerate.php',
|
||||
),
|
||||
'Swift Performance'
|
||||
=> array(
|
||||
'action' => 'Deactivate',
|
||||
'data' => 'swift-performance/performance.php',
|
||||
),
|
||||
'Swift AI'
|
||||
=> array(
|
||||
'action' => 'Deactivate',
|
||||
'data' => 'swift-ai/main.php',
|
||||
),
|
||||
'Swift Performance Lite'
|
||||
=> array(
|
||||
'action' => 'Deactivate',
|
||||
'data' => 'swift-performance-lite/performance.php',
|
||||
),
|
||||
//DEACTIVATED TEMPORARILY - it seems that the customers get scared.
|
||||
/* 'Jetpack by WordPress.com - The Speed up image load times Option'
|
||||
=> array(
|
||||
'action'=>'Change Setting',
|
||||
'data'=>'jetpack/jetpack.php',
|
||||
'href'=>'admin.php?page=jetpack#/settings'
|
||||
)
|
||||
*/
|
||||
);
|
||||
if($settings->processThumbnails) {
|
||||
$details = __('Details: recreating image files may require re-optimization of the resulting thumbnails, even if they were previously optimized. Please use <a href="https://wordpress.org/plugins/regenerate-thumbnails-advanced/" target="_blank">reGenerate Thumbnails Advanced</a> instead.','shortpixel-image-optimiser');
|
||||
|
||||
$conflictPlugins = array_merge($conflictPlugins, array(
|
||||
'Regenerate Thumbnails'
|
||||
=> array(
|
||||
'action'=>'Deactivate',
|
||||
'data'=>'regenerate-thumbnails/regenerate-thumbnails.php',
|
||||
'page'=>'regenerate-thumbnails',
|
||||
'details' => $details
|
||||
),
|
||||
'Force Regenerate Thumbnails'
|
||||
=> array(
|
||||
'action'=>'Deactivate',
|
||||
'data'=>'force-regenerate-thumbnails/force-regenerate-thumbnails.php',
|
||||
'page'=>'force-regenerate-thumbnails',
|
||||
'details' => $details
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
$found = array();
|
||||
foreach($conflictPlugins as $name => $path) {
|
||||
$action = ( isset($path['action']) ) ? $path['action'] : null;
|
||||
$data = ( isset($path['data']) ) ? $path['data'] : null;
|
||||
$href = ( isset($path['href']) ) ? $path['href'] : null;
|
||||
$page = ( isset($path['page']) ) ? $path['page'] : null;
|
||||
$details = ( isset($path['details']) ) ? $path['details'] : null;
|
||||
if(is_plugin_active($data)) {
|
||||
|
||||
// Local checks for things. If too much this needs some other impl.
|
||||
if( $data == 'jetpack/jetpack.php' ){
|
||||
$jetPackPhoton = get_option('jetpack_active_modules') ? in_array('photon', get_option('jetpack_active_modules')) : false;
|
||||
if( !$jetPackPhoton ){ continue; }
|
||||
}
|
||||
|
||||
if ($data == 'swift-performance/performance.php' || $data == 'swift-ai/main.php')
|
||||
{
|
||||
if (false === $this->checkSwiftActive())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$found[] = array( 'name' => $name, 'action'=> $action, 'path' => $data, 'href' => $href , 'page' => $page, 'details' => $details);
|
||||
}
|
||||
}
|
||||
return $found;
|
||||
}
|
||||
|
||||
private function checkSwiftActive()
|
||||
{
|
||||
if ( function_exists('swift3_check_option') && true == swift3_check_option('optimize-images', 'on'))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
namespace ShortPixel\Model\AdminNotices;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
class LegacyNotice extends \ShortPixel\Model\AdminNoticeModel
|
||||
{
|
||||
protected $key = 'MSG_CONVERT_LEGACY';
|
||||
|
||||
protected function checkTrigger()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function getMessage()
|
||||
{
|
||||
$message = '<p><strong>' . __('ShortPixel has found items in the media library with an outdated optimization format!', 'shortpixel-image-optimiser') . '</strong></p>';
|
||||
|
||||
$message .= '<p>' . __('Prior to version 5.0, a different format was used to store ShortPixel optimization information. ShortPixel automatically migrates the media library items to the new format when they are opened. %s Please check if your images contain the optimization information after migration. %s Read more %s', 'shortpixel-image-optimiser') . '</p>';
|
||||
|
||||
$message .= '<p>' . __('It is recommended to migrate all items to the modern format by clicking the button below.', 'shortpixel-image-optimser') . '</p>';
|
||||
$message .= '<p><a href="%s" class="button button-primary">%s</a></p>';
|
||||
|
||||
$read_link = esc_url('https://shortpixel.com/knowledge-base/article/539-spio-5-tells-me-to-convert-legacy-data-what-is-this');
|
||||
$action_link = esc_url(admin_url('upload.php?page=wp-short-pixel-bulk&panel=bulk-migrate'));
|
||||
$action_name = __('Migrate optimization data', 'shortpixel-image-optimiser');
|
||||
|
||||
$message = sprintf($message, '<br>', '<a href="' . $read_link . '" target="_blank">', '</a>', $action_link, $action_name);
|
||||
|
||||
return $message;
|
||||
}
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
<?php
|
||||
namespace ShortPixel\Model\AdminNotices;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
|
||||
|
||||
class ListviewNotice extends \ShortPixel\Model\AdminNoticeModel
|
||||
{
|
||||
protected $key = 'MSG_LISTVIEW_ACTIVE';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->include_screens[] = 'upload';
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/*public function load()
|
||||
{
|
||||
parent::load();
|
||||
// Reset this notice even when dismissed when condition changed.
|
||||
if ($this->isDismissed() && $this->checkReset() === true)
|
||||
{
|
||||
$this->reset();
|
||||
}
|
||||
} */
|
||||
|
||||
protected function checkTrigger()
|
||||
{
|
||||
// Don't check for this, when not on this screen.
|
||||
$screen_id = \wpSPIO()->env()->screen_id;
|
||||
if ($screen_id !== 'upload')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! function_exists('wp_get_current_user') )
|
||||
{
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
$viewMode = get_user_option('media_library_mode', get_current_user_id() );
|
||||
|
||||
if ($viewMode === "" || strlen($viewMode) == 0)
|
||||
{
|
||||
// If nothing is set, set it for them.
|
||||
update_user_option( get_current_user_id(), 'media_library_mode', 'list' );
|
||||
return false;
|
||||
}
|
||||
elseif ($viewMode !== "list")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (is_object($this->getNoticeObj()))
|
||||
$this->reset();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function checkReset()
|
||||
{
|
||||
if (! function_exists('wp_get_current_user') )
|
||||
{
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
$current_user = wp_get_current_user();
|
||||
$currentUserID = $current_user->ID;
|
||||
$viewMode = get_user_meta($currentUserID, "wp_media_library_mode", true);
|
||||
|
||||
if ($viewMode == 'list')
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
protected function getMessage()
|
||||
{
|
||||
|
||||
$message = sprintf(__('Now you can see ShortPixel Image Optimizer\'s actions and optimization data in Grid view too! Click on any image below and you can see the ShortPixel actions and menus in the popup that opens. However, the list view provides a better experience. Click now to %sswitch to the list view%s. ', 'shortpixel-image-optimiser'), '<a href="' . admin_url('upload.php?mode=list') . '">','</a>');
|
||||
|
||||
return $message;
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
namespace ShortPixel\Model\AdminNotices;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
class NewExclusionFormat extends \ShortPixel\Model\AdminNoticeModel
|
||||
{
|
||||
|
||||
protected $key = 'MSG_EXCLUSION_WARNING';
|
||||
|
||||
|
||||
protected function checkTrigger()
|
||||
{
|
||||
$patterns = \wpSPIO()->settings()->excludePatterns;
|
||||
|
||||
if (! is_array($patterns))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach($patterns as $index => $pattern)
|
||||
{
|
||||
if (! isset($pattern['apply']))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function getMessage()
|
||||
{
|
||||
$message = "<p>" . __('As of version 5.5.0, ShortPixel Image Optimiser also checks thumbnails for exclusions. This can change which images are optimized and which are excluded. Please check your exclusion rules on the '
|
||||
. '<a href="options-general.php?page=wp-shortpixel-settings&part=adv-settings">ShortPixel Settings</a> page.','shortpixel-image-optimiser') . "
|
||||
</p>";
|
||||
|
||||
return $message;
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
namespace ShortPixel\Model\AdminNotices;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
class NextgenNotice extends \ShortPixel\Model\AdminNoticeModel
|
||||
{
|
||||
protected $key = 'MSG_INTEGRATION_NGGALLERY';
|
||||
|
||||
protected function checkTrigger()
|
||||
{
|
||||
|
||||
$settings = \wpSPIO()->settings();
|
||||
|
||||
if (! $settings->verifiedKey)
|
||||
{
|
||||
return false; // no key, no integrations.
|
||||
}
|
||||
|
||||
if (\wpSPIO()->env()->has_nextgen && ! $settings->includeNextGen)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function getMessage()
|
||||
{
|
||||
$url = esc_url(admin_url('options-general.php?page=wp-shortpixel-settings&part=adv-settings'));
|
||||
$message = sprintf(__('You seem to be using NextGen Gallery. You can optimize your galleries with ShortPixel, but this is not currently enabled. To enable it, %sgo to settings and enable%s it!', 'shortpixel_image_optimiser'), '<a href="' . $url . '">', '</a>');
|
||||
|
||||
return $message;
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,104 @@
|
||||
<?php
|
||||
namespace ShortPixel\Model\AdminNotices;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
use ShortPixel\Controller\StatsController as StatsController;
|
||||
use ShortPixel\Controller\AdminNoticesController as AdminNoticesController;
|
||||
use ShortPixel\Controller\QuotaController as QuotaController;
|
||||
|
||||
class QuotaNoticeMonth extends \ShortPixel\Model\AdminNoticeModel
|
||||
{
|
||||
protected $key = 'MSG_UPGRADE_MONTH';
|
||||
|
||||
public function load()
|
||||
{
|
||||
$this->callback = array(AdminNoticesController::getInstance(), 'proposeUpgradePopup');
|
||||
parent::load();
|
||||
}
|
||||
|
||||
protected function checkTrigger()
|
||||
{
|
||||
$quotaController = QuotaController::getInstance();
|
||||
|
||||
if ($quotaController->hasQuota() === false)
|
||||
return false;
|
||||
|
||||
$quotaData = $quotaController->getQuota();
|
||||
|
||||
if ($this->monthlyUpgradeNeeded($quotaData) === false)
|
||||
return false;
|
||||
|
||||
$this->addData('average', $this->getMonthAverage());
|
||||
$this->addData('month_total', $quotaData->monthly->total);
|
||||
$this->addData('onetime_remaining', $quotaData->onetime->remaining);
|
||||
|
||||
}
|
||||
|
||||
protected function getMessage()
|
||||
{
|
||||
$quotaController = QuotaController::getInstance();
|
||||
|
||||
$quotaData = $quotaController->getQuota();
|
||||
$average = $this->getMonthAverage(); // $this->getData('average');
|
||||
$month_total = $quotaData->monthly->total;// $this->getData('month_total');
|
||||
$onetime_remaining = $quotaData->onetime->remaining; //$this->getData('onetime_remaining'); */
|
||||
|
||||
$message = '<p>' . sprintf(__("You add an average of <strong>%d images and thumbnails</strong> to your Media Library every month and you have <strong>a plan of %d images/month (and %d one-time images)</strong>.%s"
|
||||
. " You may need to upgrade your plan to have all your images optimized.", 'shortpixel-image-optimiser'), $average, $month_total, $onetime_remaining, '<br>') . '</p>';
|
||||
|
||||
$message .= ' <button class="button button-primary" id="shortpixel-upgrade-advice" onclick="ShortPixel.proposeUpgrade()" style="margin-right:10px;"><strong>' . __('Show me the best available options', 'shortpixel-image-optimiser') . '</strong></button>';
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
protected function CheckUpgradeNeeded($quotaData)
|
||||
{
|
||||
if (isset($quotaData->monthly->total) && !$quotaData->unlimited)
|
||||
{
|
||||
$monthAvg = $this->getMonthAvg($quotaData);
|
||||
// +20 I suspect to not trigger on very low values of monthly use(?)
|
||||
$threshold = $quotaData->monthly->total + ($quotaData->onetime->remaining / 6 ) +20;
|
||||
|
||||
if ($monthAvg > $threshold)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function getMonthAverage() {
|
||||
$stats = StatsController::getInstance();
|
||||
|
||||
// Count how many months have some optimized images.
|
||||
for($i = 4, $count = 0; $i>=1; $i--) {
|
||||
if($count == 0 && $stats->find('period', 'months', $i) == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
$count++;
|
||||
|
||||
}
|
||||
// Sum last 4 months, and divide by number of active months to get number of avg per active month.
|
||||
return ($stats->find('period', 'months', 1) + $stats->find('period', 'months', 2) + $stats->find('period', 'months', 3) + $stats->find('period', 'months', 4) / max(1,$count));
|
||||
}
|
||||
|
||||
protected function monthlyUpgradeNeeded($quotaData)
|
||||
{
|
||||
if (isset($quotaData->monthly->total))
|
||||
{
|
||||
$monthAvg = $this->getMonthAverage($quotaData);
|
||||
// +20 I suspect to not trigger on very low values of monthly use(?)
|
||||
$threshold = $quotaData->monthly->total + ($quotaData->onetime->remaining / 6 ) +20;
|
||||
|
||||
if ($monthAvg > $threshold)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} // class
|
@ -0,0 +1,111 @@
|
||||
<?php
|
||||
namespace ShortPixel\Model\AdminNotices;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
use ShortPixel\Controller\StatsController as StatsController;
|
||||
use ShortPixel\Controller\ApiKeyController as ApiKeyController;
|
||||
use ShortPixel\Controller\AdminNoticesController as AdminNoticesController;
|
||||
use ShortPixel\Controller\QuotaController as QuotaController;
|
||||
|
||||
|
||||
class QuotaNoticeReached extends \ShortPixel\Model\AdminNoticeModel
|
||||
{
|
||||
protected $key = 'MSG_QUOTA_REACHED';
|
||||
protected $errorLevel = 'error';
|
||||
|
||||
|
||||
public function load()
|
||||
{
|
||||
$this->callback = array(AdminNoticesController::getInstance(), 'proposeUpgradePopup');
|
||||
parent::load();
|
||||
}
|
||||
|
||||
protected function checkTrigger()
|
||||
{
|
||||
$quotaController = QuotaController::getInstance();
|
||||
|
||||
if ($quotaController->hasQuota() === true)
|
||||
return false;
|
||||
|
||||
// $quotaData = $quotaController->getQuota();
|
||||
|
||||
$this->reset('MSG_UPGRADE_MONTH');
|
||||
$this->reset('MSG_UPGRADE_BULK');
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
protected function getMessage()
|
||||
{
|
||||
$statsControl = StatsController::getInstance();
|
||||
$averageCompression = $statsControl->getAverageCompression();
|
||||
$quotaController = QuotaController::getInstance();
|
||||
|
||||
$keyControl = ApiKeyController::getInstance();
|
||||
|
||||
//$keyModel->loadKey();
|
||||
|
||||
$login_url = 'https://shortpixel.com/login/';
|
||||
$friend_url = $login_url;
|
||||
|
||||
if ($keyControl->getKeyForDisplay())
|
||||
{
|
||||
$login_url .= $keyControl->getKeyForDisplay() . '/spio-unlimited';
|
||||
$friend_url = $login_url . 'tell-a-friend';
|
||||
}
|
||||
|
||||
$message = '<div class="sp-quota-exceeded-alert" id="short-pixel-notice-exceed">';
|
||||
|
||||
if($averageCompression) {
|
||||
|
||||
$message .= '<div style="float:right;">
|
||||
<div class="bulk-progress-indicator" style="height: 110px">
|
||||
<div style="margin-bottom:5px">' . __('Average image<br>reduction until now:','shortpixel-image-optimiser') . '</div>
|
||||
<div id="sp-avg-optimization"><input type="text" id="sp-avg-optimization-dial" value="' . round($averageCompression) . '" class="dial percentDial" data-dialsize="60"></div>
|
||||
<script>
|
||||
jQuery(function() {
|
||||
if (ShortPixel)
|
||||
{
|
||||
ShortPixel.percentDial("#sp-avg-optimization-dial", 60);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</div>';
|
||||
|
||||
}
|
||||
|
||||
$message .= '<h3>' . __('Quota Exceeded','shortpixel-image-optimiser') . '</h3>';
|
||||
|
||||
$quota = $quotaController->getQuota();
|
||||
|
||||
$creditsUsed = number_format($quota->monthly->consumed + $quota->onetime->consumed);
|
||||
$totalOptimized = $statsControl->find('total', 'images');
|
||||
$totalImagesToOptimize = number_format($statsControl->totalImagesToOptimize());
|
||||
|
||||
$message .= '<p>' . sprintf(__('The plugin has optimized <strong>%s images</strong> and has been stopped because it has reached the available quota limit.','shortpixel-image-optimiser'),
|
||||
$creditsUsed);
|
||||
|
||||
if($totalImagesToOptimize > 0) {
|
||||
|
||||
$message .= sprintf(__('<strong> %s images and thumbnails</strong> have not been optimized by ShortPixel yet.','shortpixel-image-optimiser'), $totalImagesToOptimize );
|
||||
}
|
||||
|
||||
$message .= sprintf('</p>
|
||||
<div>
|
||||
<button class="button button-primary" type="button" id="shortpixel-upgrade-advice" onclick="ShortPixel.proposeUpgrade()" style="margin-right:10px;"><strong>' . __('Show me the best available options', 'shortpixel-image-optimiser') . '</strong></button>
|
||||
<a class="button button-primary" href="%s"
|
||||
title="' . __('Go to My Account and choose a plan','shortpixel-image-optimiser') . '" target="_blank" style="margin-right:10px;">
|
||||
<strong>' . __('Upgrade','shortpixel-image-optimiser') . '</strong>
|
||||
</a>
|
||||
<button type="button" name="checkQuota" class="button" onclick="ShortPixel.checkQuota()">'. __('Confirm new credits','shortpixel-image-optimiser') . '</button>
|
||||
</div>', $login_url);
|
||||
|
||||
$message .= '</div>'; /// closing div
|
||||
return $message;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
namespace ShortPixel\Model\AdminNotices;
|
||||
|
||||
class SmartcropNotice extends \ShortPixel\Model\AdminNoticeModel
|
||||
{
|
||||
protected $key = 'MSG_FEATURE_SMARTCROP';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->exclude_screens[] = 'settings_page_wp-shortpixel-settings';
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function checkTrigger()
|
||||
{
|
||||
|
||||
$settings = \wpSPIO()->settings();
|
||||
|
||||
if (! $settings->verifiedKey)
|
||||
{
|
||||
return false; // no key, no integrations.
|
||||
}
|
||||
|
||||
if (! $settings->useSmartcrop)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function checkReset()
|
||||
{
|
||||
$settings = \wpSPIO()->settings();
|
||||
if ($settings->useSmartcrop == true)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function getMessage()
|
||||
{
|
||||
$link = 'https://shortpixel.com/knowledge-base/article/182-what-is-smart-cropping';
|
||||
$link2 = 'https://shortpixel.com/blog/how-to-smart-crop-wordpress-images/#how-to-crop-wordpress-images-automatically-smart-solution';
|
||||
$link3 = esc_url(admin_url('options-general.php?page=wp-shortpixel-settings'));
|
||||
|
||||
$message = sprintf(__('%s With ShortPixel you can now %ssmartly crop%s thumbnails on your website. This is especially useful for eCommerce websites %s(read more)%s. %s %s Enable the option on the %sShortPixel Settings%s page. %s', 'shortpixel-image-optimiser'),
|
||||
'<p>' ,
|
||||
'<a href="' . $link . '" target="_blank">', '</a>',
|
||||
'<a href="' . $link2 . '" target="_blank">', '</a>',
|
||||
'</p>', '<p>',
|
||||
'<a href="' . $link3 . '" >', '</a>',
|
||||
'</p>'
|
||||
);
|
||||
return $message;
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
namespace ShortPixel\Model\AdminNotices;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
class UnlistedNotice extends \ShortPixel\Model\AdminNoticeModel
|
||||
{
|
||||
|
||||
protected $key = 'MSG_UNLISTED_FOUND';
|
||||
|
||||
protected function checkTrigger()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// @todo This message is not properly stringF'ed.
|
||||
protected function getMessage()
|
||||
{
|
||||
$settings = \wpSPIO()->settings();
|
||||
|
||||
//$unlisted = isset($settings->currentStats['foundUnlistedThumbs']) ? $settings->currentStats['foundUnlistedThumbs'] : null;
|
||||
$unlisted_id = $this->getData('id');
|
||||
$unlisted_name = $this->getData('name');
|
||||
$unlistedFiles = (is_array($this->getData('filelist'))) ? $this->getData('filelist') : array();
|
||||
|
||||
$admin_url = esc_url(admin_url('options-general.php?page=wp-shortpixel-settings&part=adv-settings'));
|
||||
|
||||
|
||||
$message = __("<p>ShortPixel has found thumbnails that are not registered in the metadata, but are present alongside the other thumbnails. These thumbnails could be created and needed by a plugin or the theme. Let ShortPixel optimize them too?</p>", 'shortpixel-image-optimiser');
|
||||
$message .= '<p>' . __("For example, the image", 'shortpixel-image-optimiser') . '
|
||||
<a href="post.php?post=' . $unlisted_id . '&action=edit" target="_blank">
|
||||
' . $unlisted_name . '
|
||||
</a> also has these thumbnails that are not listed in the metadata: ' . (implode(', ', $unlistedFiles)) . '
|
||||
</p>';
|
||||
|
||||
$message .= '<p>' . sprintf(__('You can enable optimizing %s Unlisted Images %s in the %s settings %s', 'shortpixel-image-optimiser'), '<b>', '</b>', '<a href="'. $admin_url . '">','</a>') . '</p>';
|
||||
|
||||
return $message;
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,311 @@
|
||||
<?php
|
||||
namespace ShortPixel\Model;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
|
||||
use ShortPixel\Notices\NoticeController as Notice;
|
||||
|
||||
use ShortPixel\Controller\AdminNoticesController as AdminNoticesController;
|
||||
use ShortPixel\Controller\QuotaController as QuotaController;
|
||||
|
||||
class ApiKeyModel extends \ShortPixel\Model
|
||||
{
|
||||
|
||||
// variables
|
||||
protected $apiKey;
|
||||
protected $apiKeyTried; // stop retrying the same key over and over if not valid.
|
||||
protected $verifiedKey;
|
||||
protected $redirectedSettings;
|
||||
|
||||
// states
|
||||
// key is verified is set by checkKey *after* checks and validation
|
||||
protected $key_is_verified = false; // this state doesn't have to be the same as the verifiedKey field in DB.
|
||||
protected $key_is_empty = false;
|
||||
protected $key_is_constant = false;
|
||||
protected $key_is_hidden = false;
|
||||
|
||||
protected static $notified = array();
|
||||
|
||||
protected $model = array(
|
||||
'apiKey' => array('s' => 'string',
|
||||
'key' => 'wp-short-pixel-apiKey',
|
||||
),
|
||||
'apiKeyTried' => array('s' => 'string',
|
||||
'key' => 'wp-short-pixel-apiKeyTried'
|
||||
),
|
||||
'verifiedKey' => array('s' => 'boolean',
|
||||
'key' => 'wp-short-pixel-verifiedKey',
|
||||
),
|
||||
'redirectedSettings' => array('s' => 'int',
|
||||
'key' => 'wp-short-pixel-redirected-settings',
|
||||
),
|
||||
);
|
||||
|
||||
public $shortPixel;
|
||||
|
||||
/** Constructor. Check for constants, load the key */
|
||||
public function __construct()
|
||||
{
|
||||
$this->key_is_constant = (defined("SHORTPIXEL_API_KEY")) ? true : false;
|
||||
$this->key_is_hidden = (defined("SHORTPIXEL_HIDE_API_KEY")) ? SHORTPIXEL_HIDE_API_KEY : false;
|
||||
|
||||
}
|
||||
|
||||
/** Load the key from storage. This can be a constant, or the database. Check if key is valid.
|
||||
*
|
||||
*/
|
||||
public function loadKey()
|
||||
{
|
||||
$this->apiKey = get_option($this->model['apiKey']['key'], false);
|
||||
$this->verifiedKey = get_option($this->model['verifiedKey']['key'], false);
|
||||
$this->redirectedSettings = get_option($this->model['redirectedSettings']['key'], false);
|
||||
$this->apiKeyTried = get_option($this->model['apiKeyTried']['key'], false);
|
||||
|
||||
if ($this->key_is_constant)
|
||||
{
|
||||
$key = SHORTPIXEL_API_KEY;
|
||||
}
|
||||
else
|
||||
{
|
||||
$key = $this->apiKey;
|
||||
}
|
||||
|
||||
$valid = $this->checkKey($key);
|
||||
|
||||
return $valid;
|
||||
}
|
||||
|
||||
protected function update()
|
||||
{
|
||||
update_option($this->model['apiKey']['key'], trim($this->apiKey));
|
||||
update_option($this->model['verifiedKey']['key'], $this->verifiedKey);
|
||||
update_option($this->model['redirectedSettings']['key'], $this->redirectedSettings);
|
||||
update_option($this->model['apiKeyTried']['key'], $this->apiKeyTried);
|
||||
|
||||
}
|
||||
|
||||
/** Resets the last APIkey that was attempted with validation
|
||||
*
|
||||
* The last apikey tried is saved to prevent server and notice spamming when using a constant key, or a wrong key in the database without updating.
|
||||
*/
|
||||
public function resetTried()
|
||||
{
|
||||
if (is_null($this->apiKeyTried))
|
||||
{
|
||||
return; // if already null, no need for additional activity
|
||||
}
|
||||
$this->apiKeyTried = null;
|
||||
$this->update();
|
||||
Log::addDebug('Reset Tried', $this->apiKeyTried);
|
||||
}
|
||||
|
||||
/** Checks the API key to see if we have a validated situation
|
||||
* @param $key String The 20-character ShortPixel API Key or empty string
|
||||
* @return boolean Returns a boolean indicating valid key or not
|
||||
*
|
||||
* An Api key can be removed from the system by passing an empty string when the key is not hidden.
|
||||
* If the key has changed from stored key, the function will pass a validation request to the server
|
||||
* Failing to key a 20char string, or passing an empty key will result in notices.
|
||||
*/
|
||||
public function checkKey($key)
|
||||
{
|
||||
|
||||
$valid = false;
|
||||
|
||||
if (strlen($key) == 0)
|
||||
{
|
||||
// first-timers, redirect to nokey screen
|
||||
$this->checkRedirect(); // this should be a one-time thing.
|
||||
if($this->key_is_hidden) // hidden empty keys shouldn't be checked
|
||||
{
|
||||
$this->key_is_verified = $this->verifiedKey;
|
||||
return $this->key_is_verified;
|
||||
}
|
||||
elseif ($key != $this->apiKey)
|
||||
{
|
||||
Notice::addWarning(__('Your API Key has been removed', 'shortpixel-image-optimiser'));
|
||||
$this->clearApiKey(); // notice and remove;
|
||||
return false;
|
||||
}
|
||||
$valid = false;
|
||||
|
||||
}
|
||||
elseif (strlen($key) <> 20 && $key != $this->apiKeyTried)
|
||||
{
|
||||
$this->NoticeApiKeyLength($key);
|
||||
Log::addDebug('Key Wrong Length');
|
||||
$valid = $this->verifiedKey; // if we already had a verified key, and a wrong new one is giving keep status.
|
||||
}
|
||||
elseif( ($key != $this->apiKey || ! $this->verifiedKey) && $key != $this->apiKeyTried)
|
||||
{
|
||||
Log::addDebug('Validate Key' . $key);
|
||||
$valid = $this->validateKey($key);
|
||||
}
|
||||
elseif($key == $this->apiKey) // all is fine!
|
||||
{
|
||||
$valid = $this->verifiedKey;
|
||||
}
|
||||
|
||||
// if key is not valid on load, means not valid at all
|
||||
if (! $valid)
|
||||
{
|
||||
$this->verifiedKey = false;
|
||||
$this->key_is_verified = false;
|
||||
$this->apiKeyTried = $key;
|
||||
$this->update();
|
||||
}
|
||||
else {
|
||||
$this->key_is_verified = true;
|
||||
}
|
||||
|
||||
return $this->key_is_verified; // first time this is set! *after* this function
|
||||
}
|
||||
|
||||
public function is_verified()
|
||||
{
|
||||
return $this->key_is_verified;
|
||||
}
|
||||
|
||||
public function is_constant()
|
||||
{
|
||||
return $this->key_is_constant;
|
||||
}
|
||||
|
||||
public function is_hidden()
|
||||
{
|
||||
return $this->key_is_hidden;
|
||||
}
|
||||
|
||||
public function getKey()
|
||||
{
|
||||
return $this->apiKey;
|
||||
}
|
||||
|
||||
public function uninstall()
|
||||
{
|
||||
$this->clearApiKey();
|
||||
}
|
||||
|
||||
protected function clearApiKey()
|
||||
{
|
||||
$this->key_is_empty = true;
|
||||
$this->apiKey = '';
|
||||
$this->verifiedKey = false;
|
||||
$this->apiKeyTried = '';
|
||||
$this->key_is_verified = false;
|
||||
|
||||
AdminNoticesController::resetAPINotices();
|
||||
AdminNoticesController::resetQuotaNotices();
|
||||
AdminNoticesController::resetIntegrationNotices();
|
||||
|
||||
// Remove them all
|
||||
delete_option($this->model['apiKey']['key']);
|
||||
delete_option($this->model['verifiedKey']['key']);
|
||||
delete_option($this->model['redirectedSettings']['key']);
|
||||
delete_option($this->model['apiKeyTried']['key']);
|
||||
|
||||
// $this->update();
|
||||
|
||||
}
|
||||
|
||||
protected function validateKey($key)
|
||||
{
|
||||
Log::addDebug('Validating Key ' . $key);
|
||||
// first, save Auth to satisfy getquotainformation
|
||||
|
||||
$quotaData = $this->remoteValidate($key);
|
||||
|
||||
$checked_key = ($quotaData['APIKeyValid']) ? true : false;
|
||||
|
||||
|
||||
if (! $checked_key)
|
||||
{
|
||||
Log::addError('Key is not validated', $quotaData['Message']);
|
||||
Notice::addError(sprintf(__('Error during verifying API key: %s','shortpixel-image-optimiser'), $quotaData['Message'] ));
|
||||
}
|
||||
elseif ($checked_key) {
|
||||
$this->apiKey = $key;
|
||||
$this->verifiedKey = $checked_key;
|
||||
$this->processNewKey($quotaData);
|
||||
$this->update();
|
||||
}
|
||||
return $this->verifiedKey;
|
||||
}
|
||||
|
||||
|
||||
/** Process some things when key has been added. This is from original wp-short-pixel.php */
|
||||
protected function processNewKey($quotaData)
|
||||
{
|
||||
//display notification
|
||||
$urlParts = explode("/", get_site_url());
|
||||
if( $quotaData['DomainCheck'] == 'NOT Accessible'){
|
||||
$notice = array("status" => "warn", "msg" => __("API Key is valid but your site is not accessible from our servers. Please make sure that your server is accessible from the Internet before using the API or otherwise we won't be able to optimize them.",'shortpixel-image-optimiser'));
|
||||
Notice::addWarning($notice);
|
||||
} else {
|
||||
if ( function_exists("is_multisite") && is_multisite() && !defined("SHORTPIXEL_API_KEY"))
|
||||
$notice = __("Great, your API Key is valid! <br>You seem to be running a multisite, please note that API Key can also be configured in wp-config.php like this:",'shortpixel-image-optimiser')
|
||||
. "<BR> <b>define('SHORTPIXEL_API_KEY', '". $this->apiKey ."');</b>";
|
||||
else
|
||||
$notice = __('Great, your API Key is valid. Please take a few moments to review the plugin settings before starting to optimize your images.','shortpixel-image-optimiser');
|
||||
|
||||
Notice::addSuccess($notice);
|
||||
}
|
||||
|
||||
//test that the "uploads" have the right rights and also we can create the backup dir for ShortPixel
|
||||
if ( \wpSPIO()->filesystem()->checkBackupFolder() === false)
|
||||
{
|
||||
$notice = sprintf(__("There is something preventing us to create a new folder for backing up your original files.<BR>Please make sure that folder <b>%s</b> has the necessary write and read rights.",'shortpixel-image-optimiser'), WP_CONTENT_DIR . '/' . SHORTPIXEL_UPLOADS_NAME );
|
||||
Notice::addError($notice);
|
||||
}
|
||||
|
||||
AdminNoticesController::resetAPINotices();
|
||||
}
|
||||
|
||||
|
||||
protected function NoticeApiKeyLength($key)
|
||||
{
|
||||
// repress double warning.
|
||||
if (isset(self::$notified['apilength']) && self::$notified['apilength'])
|
||||
return;
|
||||
|
||||
$KeyLength = strlen($key);
|
||||
$notice = sprintf(__("The key you provided has %s characters. The API key should have 20 characters, letters and numbers only.",'shortpixel-image-optimiser'), $KeyLength)
|
||||
. "<BR> <b>"
|
||||
. __('Please check that the API key is the same as the one you received in your confirmation email.','shortpixel-image-optimiser')
|
||||
. "</b><BR> "
|
||||
. __('If this problem persists, please contact us at ','shortpixel-image-optimiser')
|
||||
. "<a href='mailto:help@shortpixel.com?Subject=API Key issues' target='_top'>help@shortpixel.com</a>"
|
||||
. __(' or ','shortpixel-image-optimiser')
|
||||
. "<a href='https://shortpixel.com/contact' target='_blank'>" . __('here','shortpixel-image-optimiser') . "</a>.";
|
||||
self::$notified['apilength'] = true;
|
||||
Notice::addError($notice);
|
||||
}
|
||||
|
||||
// Does remote Validation of key. In due time should be replaced with something more lean.
|
||||
private function remoteValidate($key)
|
||||
{
|
||||
$qControl = QuotaController::getInstance();
|
||||
$quotaData = $qControl->remoteValidateKey($key);
|
||||
|
||||
return $quotaData;
|
||||
|
||||
|
||||
}
|
||||
|
||||
protected function checkRedirect()
|
||||
{
|
||||
|
||||
if(! \wpSPIO()->env()->is_ajaxcall && !$this->redirectedSettings && !$this->verifiedKey && (!function_exists("is_multisite") || ! is_multisite())) {
|
||||
$this->redirectedSettings = 1;
|
||||
$this->update();
|
||||
wp_safe_redirect(admin_url("options-general.php?page=wp-shortpixel-settings"));
|
||||
exit();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
<?php
|
||||
namespace ShortPixel\Model;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
|
||||
|
||||
|
||||
/* Model for storing cached data
|
||||
*
|
||||
* Use this in conjunction with cache controller, don't call it stand-alone.
|
||||
*/
|
||||
class CacheModel
|
||||
{
|
||||
|
||||
protected $name;
|
||||
protected $value;
|
||||
protected $expires = HOUR_IN_SECONDS; // This is the expires, when saved without SetExpires! This value is not a representation of any expire time when loading something cache!
|
||||
protected $exists = false;
|
||||
|
||||
|
||||
public function __construct($name)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->load();
|
||||
}
|
||||
|
||||
/** Set the expiration of this item. In seconds
|
||||
* @param $time Expiration in Seconds
|
||||
*/
|
||||
public function setExpires($time)
|
||||
{
|
||||
$this->expires = $time;
|
||||
}
|
||||
|
||||
public function setValue($value)
|
||||
{
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
public function exists()
|
||||
{
|
||||
return $this->exists;
|
||||
}
|
||||
|
||||
public function getValue()
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
if ($this->expires <= 0)
|
||||
{
|
||||
return; // don't save transients without expiration
|
||||
}
|
||||
$this->exists = set_transient($this->name, $this->value, $this->expires);
|
||||
|
||||
}
|
||||
|
||||
public function delete()
|
||||
{
|
||||
delete_transient($this->name);
|
||||
$this->exists = false;
|
||||
}
|
||||
|
||||
protected function load()
|
||||
{
|
||||
$item = get_transient($this->name);
|
||||
if ($item !== false)
|
||||
{
|
||||
$this->value = $item;
|
||||
$this->exists = true;
|
||||
$this->checkExpiration($this->name);
|
||||
}
|
||||
}
|
||||
|
||||
/** It has been shown that sometimes the expire of the transient is lost, creating a persistent transient. This can be harmful, especially in the case of bulk-secret which can create a situation were no client will optimize due to the hanging transient. */
|
||||
private function checkExpiration($name)
|
||||
{
|
||||
$option = get_option('_transient_timeout_' . $name);
|
||||
|
||||
if (false !== $option && is_numeric($option))
|
||||
{
|
||||
return true; // ok
|
||||
}
|
||||
else {
|
||||
|
||||
// Via object cache the expire info can't be retrieved. Customer is on it's own with this.
|
||||
if (wp_using_ext_object_cache())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->value = '';
|
||||
$this->delete();
|
||||
Log::addError('Found hanging transient with no expiration! ' . $name, $option);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,253 @@
|
||||
<?php
|
||||
namespace ShortPixel\Model\Converter;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
|
||||
|
||||
use ShortPixel\Helper\UtilHelper as UtilHelper;
|
||||
|
||||
class ApiConverter extends MediaLibraryConverter
|
||||
{
|
||||
|
||||
const CONVERTABLE_EXTENSIONS = array( 'heic');
|
||||
|
||||
protected $requestAPIthumbnails = true;
|
||||
|
||||
|
||||
public function isConvertable()
|
||||
{
|
||||
$fs = \wpSPIO()->filesystem();
|
||||
$extension = $this->imageModel->getExtension();
|
||||
|
||||
// If extension is in list of allowed Api Converts.
|
||||
if (in_array($extension, static::CONVERTABLE_EXTENSIONS) && $extension !== 'png')
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// If file has not been converted in terms of file, but has a placeholder - process ongoing, so continue;
|
||||
if (false === $this->imageModel->getMeta()->convertMeta()->isConverted() && true === $this->imageModel->getMeta()->convertMeta()->hasPlaceHolder())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// File has been converted, not converting again.
|
||||
if (true === $this->imageModel->getMeta()->convertMeta()->isConverted())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Create placeholder here.
|
||||
public function convert($args = array())
|
||||
{
|
||||
$defaults = array(
|
||||
'runReplacer' => true, // The replacer doesn't need running when the file is just uploaded and doing in handle upload hook.
|
||||
);
|
||||
|
||||
$args = wp_parse_args($args, $defaults);
|
||||
|
||||
$this->setupReplacer();
|
||||
|
||||
$fs = \wpSPIO()->filesystem();
|
||||
|
||||
$placeholderFile = $fs->getFile(\wpSPIO()->plugin_path('res/img/fileformat-heic-placeholder.jpg'));
|
||||
|
||||
// Convert runs when putting imageModel to queue format in the Queue classs. This could run without optimization (before) taking place and when accidentally running it more than once results in duplicate files / backups (img-1, img-2 etc). Check placeholder and baseName to prevent this. Assume already done when it has it .
|
||||
if ($this->imageModel->getMeta()->convertMeta()->hasPlaceHolder() && $this->imageModel->getMeta()->convertMeta()->getReplacementImageBase() !== false)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// @todo Check replacementpath here. Rename main file - and backup - if numeration is needed.
|
||||
// @todo Also placeholder probably needs to be done each time to block current job in progress.
|
||||
$replacementPath = $this->getReplacementPath();
|
||||
if (false === $replacementPath)
|
||||
{
|
||||
Log::addWarn('ApiConverter replacement path failed');
|
||||
$this->imageModel->getMeta()->convertMeta()->setError(self::ERROR_PATHFAIL);
|
||||
|
||||
return false; // @todo Add ResponseController something here.
|
||||
}
|
||||
|
||||
$replaceFile = $fs->getFile($replacementPath);
|
||||
// If filebase (filename without extension) is not the same, this indicates that a double is there and it's enumerated. Move backup accordingly.
|
||||
|
||||
$destinationFile = $fs->getFile($replacementPath);
|
||||
$copyok = $placeholderFile->copy($destinationFile);
|
||||
|
||||
if ($copyok)
|
||||
{
|
||||
$this->imageModel->getMeta()->convertMeta()->setFileFormat('heic');
|
||||
$this->imageModel->getMeta()->convertMeta()->setPlaceHolder(true);
|
||||
$this->imageModel->getMeta()->convertMeta()->setReplacementImageBase($destinationFile->getFileBase());
|
||||
$this->imageModel->saveMeta();
|
||||
|
||||
|
||||
// @todo Wip . Moved from handleConverted.
|
||||
// Backup basically. Do this first.
|
||||
$conversion_args = array('replacementPath' => $replacementPath);
|
||||
$prepared = $this->imageModel->conversionPrepare($conversion_args);
|
||||
if (false === $prepared)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->setTarget($destinationFile);
|
||||
// $params = array('success' => true, 'generate_metadata' => false);
|
||||
// $this->updateMetaData($params);
|
||||
|
||||
$fs->flushImage($this->imageModel);
|
||||
|
||||
if (true === $args['runReplacer'])
|
||||
{
|
||||
$result = $this->replacer->replace();
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
Log::addError('Failed to copy placeholder');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Restore from original file. Search and replace everything else to death.
|
||||
public function restore()
|
||||
{
|
||||
/*$params = array('restore' => true);
|
||||
$fs = \wpSPIO()->filesystem();
|
||||
|
||||
$this->setupReplacer();
|
||||
|
||||
$newExtension = $this->imageModel->getMeta()->convertMeta()->getFileFormat();
|
||||
|
||||
$oldFileName = $this->imageModel->getFileName(); // Old File Name, Still .jpg
|
||||
$newFileName = $this->imageModel->getFileBase() . '.' . $newExtension;
|
||||
|
||||
if ($this->imageModel->isScaled())
|
||||
{
|
||||
$oldFileName = $this->imageModel->getOriginalFile()->getFileName();
|
||||
$newFileName = $this->imageModel->getOriginalFile()->getFileBase() . '.' . $newExtension;
|
||||
}
|
||||
|
||||
$fsNewFile = $fs->getFile($this->imageModel->getFileDir() . $newFileName);
|
||||
|
||||
$this->newFile = $fsNewFile;
|
||||
$this->setTarget($fsNewFile);
|
||||
|
||||
$this->updateMetaData($params);
|
||||
// $result = $this->replacer->replace();
|
||||
|
||||
$fs->flushImageCache(); */
|
||||
}
|
||||
|
||||
public function getCheckSum()
|
||||
{
|
||||
return 1; // done or not.
|
||||
}
|
||||
|
||||
public function handleConverted($optimizeData)
|
||||
{
|
||||
$this->setupReplacer();
|
||||
$fs = \wpSPIO()->filesystem();
|
||||
|
||||
|
||||
/* $replacementPath = $this->getReplacementPath();
|
||||
if (false === $replacementPath)
|
||||
{
|
||||
Log::addWarn('ApiConverter replacement path failed');
|
||||
$this->imageModel->getMeta()->convertMeta()->setError(self::ERROR_PATHFAIL);
|
||||
|
||||
return false; // @todo Add ResponseController something here.
|
||||
}
|
||||
|
||||
$replacementFile = $fs->getFile($replacementPath);
|
||||
*/
|
||||
$replacementBase = $this->imageModel->getMeta()->convertMeta()->getReplacementImageBase();
|
||||
if (false === $replacementBase)
|
||||
{
|
||||
$replacementPath = $this->getReplacementPath();
|
||||
$replacementFile = $fs->getFile($replacementPath);
|
||||
}
|
||||
else {
|
||||
$replacementPath = $replacementBase . '.jpg';
|
||||
$replacementFile = $fs->getFile($this->imageModel->getFileDir() . $replacementPath);
|
||||
}
|
||||
|
||||
// If -sigh- file has a placeholder, then do something with that.
|
||||
if (true === $this->imageModel->getMeta()->convertMeta()->hasPlaceHolder())
|
||||
{
|
||||
$this->imageModel->getMeta()->convertMeta()->setPlaceHolder(false);
|
||||
// $this->imageModel->getMeta()->convertMeta()->setReplacementImageBase(false);
|
||||
|
||||
// $attach_id = $this->imageModel->get('id');
|
||||
|
||||
// ReplacementFile as source should not point to the placeholder file
|
||||
$this->source_url = $fs->pathToUrl($replacementFile);
|
||||
$this->replacer->setSource($this->source_url);
|
||||
|
||||
$replacementFile->delete();
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (isset($optimizeData['files']) && isset($optimizeData['data']))
|
||||
{
|
||||
$files = $optimizeData['files'];
|
||||
$data = $optimizeData['data'];
|
||||
}
|
||||
else {
|
||||
Log::addError('Something went wrong with handleOptimized', $optimizeData);
|
||||
return false;
|
||||
}
|
||||
|
||||
$mainImageKey = $this->imageModel->get('mainImageKey');
|
||||
$mainFile = (isset($files) && isset($files[$mainImageKey])) ? $files[$mainImageKey] : false;
|
||||
|
||||
if (false === $mainFile)
|
||||
{
|
||||
Log::addError('MainFile not set during success Api Conversion');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! isset($mainFile['image']) || ! isset($mainFile['image']['file']))
|
||||
{
|
||||
Log::addError('Optimizer didn\'t return file', $mainFile);
|
||||
return false;
|
||||
}
|
||||
|
||||
$tempFile = $fs->getFile($mainFile['image']['file']);
|
||||
|
||||
$res = $tempFile->copy($replacementFile);
|
||||
|
||||
if (true === $res)
|
||||
{
|
||||
$this->newFile = $replacementFile;
|
||||
$tempFile->delete();
|
||||
|
||||
$params = array('success' => true);
|
||||
$this->updateMetaData($params);
|
||||
|
||||
$result = true;
|
||||
|
||||
// if (true === $args['runReplacer'])
|
||||
// {
|
||||
$result = $this->replacer->replace();
|
||||
// }
|
||||
|
||||
// Conversion done, but backup results.
|
||||
$this->imageModel->conversionSuccess(array('omit_backup' => false));
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // class
|
@ -0,0 +1,172 @@
|
||||
<?php
|
||||
namespace ShortPixel\Model\Converter;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
use ShortPixel\Replacer\Replacer as Replacer;
|
||||
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
|
||||
use ShortPixel\Model\File\DirectoryModel as DirectoryModel;
|
||||
use ShortPixel\Model\File\FileModel as FileModel;
|
||||
use ShortPixel\Controller\ResponseController as ResponseController;
|
||||
|
||||
/* ShortPixel Image Optimiser Converters. Unified interface for handling conversion between file types */
|
||||
abstract class Converter
|
||||
{
|
||||
const CONVERTABLE_EXTENSIONS = array('png', 'heic');
|
||||
|
||||
const ERROR_LIBRARY = -1; /// PNG Library error
|
||||
const ERROR_PATHFAIL = -2; // Couldn't create replacement path
|
||||
const ERROR_RESULTLARGER = -3; // Converted file is bigger than the original
|
||||
const ERROR_WRITEERROR = -4; // Result file could not be written
|
||||
const ERROR_BACKUPERROR = -5; // Backup didn't work.
|
||||
const ERROR_TRANSPARENT = -6; // Transparency when it is not allowed.
|
||||
|
||||
protected $imageModel; // The current ImageModel from SPIO
|
||||
|
||||
// Method specific
|
||||
abstract public function convert($args = array());
|
||||
abstract public function isConvertable();
|
||||
abstract public function restore();
|
||||
abstract public function getCheckSum();
|
||||
|
||||
// Media Library specific
|
||||
abstract protected function updateMetaData($params);
|
||||
abstract public function getUpdatedMeta();
|
||||
abstract protected function setupReplacer();
|
||||
abstract protected function setTarget($file);
|
||||
|
||||
public function __construct($imageModel)
|
||||
{
|
||||
$this->imageModel = $imageModel;
|
||||
$this->imageModel->getMeta()->convertMeta()->setFileFormat($imageModel->getExtension());
|
||||
}
|
||||
|
||||
public function isConverterFor($extension)
|
||||
{
|
||||
if ($extension === $this->imageModel->getMeta()->convertMeta()->getFileFormat())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ForConversion: Return empty if file can't be converted or is already converrted
|
||||
public static function getConverter($imageModel, $forConversion = false)
|
||||
{
|
||||
$extension = $imageModel->getExtension();
|
||||
|
||||
$converter = false;
|
||||
|
||||
$converter = self::getConverterByExt($extension, $imageModel);
|
||||
|
||||
// No Support (yet)
|
||||
if ($imageModel->get('type') == 'custom')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Second option for conversion is image who have been placeholdered.
|
||||
if (true === $imageModel->getMeta()->convertMeta()->hasPlaceHolder() && false === $imageModel->getMeta()->convertMeta()->isConverted() && ! is_null($imageModel->getMeta()->convertMeta()->getFileFormat()))
|
||||
{
|
||||
$converter = self::getConverterByExt($imageModel->getMeta()->convertMeta()->getFileFormat(), $imageModel);
|
||||
}
|
||||
|
||||
if (true === $forConversion) // don't check more.
|
||||
{
|
||||
return $converter;
|
||||
}
|
||||
|
||||
if (false === $converter)
|
||||
{
|
||||
if ($imageModel->getMeta()->convertMeta()->isConverted() && ! is_null($imageModel->getMeta()->convertMeta()->getFileFormat()) )
|
||||
{
|
||||
$converter = self::getConverterByExt($imageModel->getMeta()->convertMeta()->getFileFormat(), $imageModel);
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return $converter;
|
||||
}
|
||||
|
||||
/** Own function to get a unique filename since the WordPress wp_unique_filename seems to not function properly w/ thumbnails */
|
||||
protected function unique_file(DirectoryModel $dir, FileModel $file, $number = 0)
|
||||
{
|
||||
if (! $file->exists())
|
||||
return $file;
|
||||
|
||||
if ($file->is_virtual())
|
||||
{
|
||||
return $file;
|
||||
}
|
||||
|
||||
$number = 0;
|
||||
$fs = \wpSPIO()->filesystem();
|
||||
|
||||
$base = $file->getFileBase();
|
||||
$ext = $file->getExtension();
|
||||
|
||||
while($file->exists())
|
||||
{
|
||||
$number++;
|
||||
$numberbase = $base . '-' . $number;
|
||||
Log::addDebug('check for unique file -- ' . $dir->getPath() . $numberbase . '.' . $ext);
|
||||
$file = $fs->getFile($dir->getPath() . $numberbase . '.' . $ext);
|
||||
}
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
protected function getReplacementPath()
|
||||
{
|
||||
$fs = \wpSPIO()->filesystem();
|
||||
|
||||
$filename = $this->imageModel->getFileName();
|
||||
$newFileName = $this->imageModel->getFileBase() . '.jpg'; // convert extension to .jpg
|
||||
|
||||
$fsNewFile = $fs->getFile($this->imageModel->getFileDir() . $newFileName);
|
||||
|
||||
$uniqueFile = $this->unique_file( $this->imageModel->getFileDir(), $fsNewFile);
|
||||
$newPath = $uniqueFile->getFullPath(); //(string) $fsFile->getFileDir() . $uniquepath;
|
||||
|
||||
if (! $this->imageModel->getFileDir()->is_writable())
|
||||
{
|
||||
Log::addWarn('Replacement path for PNG not writable ' . $this->imageModel->getFileDir()->getPath());
|
||||
$msg = __('Replacement path for PNG not writable', 'shortpixel-image-optimiser');
|
||||
ResponseController::addData($this->imageModel->get('id'), 'message', $msg);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->setTarget($uniqueFile);
|
||||
|
||||
return $newPath;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static function getConverterByExt($ext, $imageModel)
|
||||
{
|
||||
$converter = false;
|
||||
switch($ext)
|
||||
{
|
||||
case 'png':
|
||||
$converter = new PNGConverter($imageModel);
|
||||
break;
|
||||
case 'heic':
|
||||
$converter = new ApiConverter($imageModel);
|
||||
break;
|
||||
|
||||
}
|
||||
return $converter;
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,173 @@
|
||||
<?php
|
||||
namespace ShortPixel\Model\Converter;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
use ShortPixel\Replacer\Replacer as Replacer;
|
||||
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
|
||||
|
||||
/* Abstract base to use for image converters. Handles media library related functions ( replacing ) */
|
||||
abstract class MediaLibraryConverter extends Converter
|
||||
{
|
||||
protected $source_url;
|
||||
protected $replacer; // Replacer class Object.
|
||||
protected $newFile; // The newFile Object.
|
||||
|
||||
public function getUpdatedMeta()
|
||||
{
|
||||
$id = $this->imageModel->get('id');
|
||||
$meta = wp_get_attachment_metadata($id); // reset the metadata because we are on the hook.
|
||||
return $meta;
|
||||
}
|
||||
|
||||
protected function setupReplacer()
|
||||
{
|
||||
$this->replacer = new Replacer();
|
||||
$fs = \wpSPIO()->filesystem();
|
||||
|
||||
$url = $fs->pathToUrl($this->imageModel);
|
||||
|
||||
if ($this->imageModel->isScaled()) // @todo Test these assumptions
|
||||
{
|
||||
$url = $fs->pathToUrl($this->imageModel->getOriginalFile());
|
||||
}
|
||||
|
||||
$this->source_url = $url;
|
||||
$this->replacer->setSource($url);
|
||||
|
||||
$this->replacer->setSourceMeta($this->imageModel->getWPMetaData());
|
||||
|
||||
}
|
||||
|
||||
protected function setTarget($newFile)
|
||||
{
|
||||
$fs = \wpSPIO()->filesystem();
|
||||
$this->newFile = $newFile; // set target newFile.
|
||||
|
||||
$url = $fs->pathToUrl($this->imageModel);
|
||||
$newUrl = str_replace($this->imageModel->getFileName(), $newFile->getFileName(), $url);
|
||||
|
||||
$this->replacer->setTarget($newUrl);
|
||||
}
|
||||
|
||||
protected function updateMetaData($params)
|
||||
{
|
||||
$defaults = array(
|
||||
'success' => false,
|
||||
'restore' => false,
|
||||
'generate_metadata' => true,
|
||||
);
|
||||
|
||||
$params = wp_parse_args($params, $defaults);
|
||||
|
||||
$newFile = $this->newFile;
|
||||
$fullPath = $newFile->getFullPath();
|
||||
|
||||
if (! is_object($newFile))
|
||||
{
|
||||
Log::addError('Update metadata failed. NewFile not properly set', $newFile);
|
||||
return false;
|
||||
}
|
||||
|
||||
$attach_id = $this->imageModel->get('id');
|
||||
|
||||
$WPMLduplicates = $this->imageModel->getWPMLDuplicates();
|
||||
|
||||
$attachment = get_post($attach_id);
|
||||
|
||||
$guid = $attachment->guid;
|
||||
|
||||
// This action prevents images from being regenerated on the thumbnail hook.
|
||||
do_action('shortpixel-thumbnails-before-regenerate', $attach_id );
|
||||
do_action('shortpixel/converter/prevent-offload', $attach_id);
|
||||
|
||||
// Update attached_file
|
||||
$bool = update_attached_file($attach_id, $newFile->getFullPath() );
|
||||
if (false === $bool)
|
||||
return false;
|
||||
|
||||
|
||||
// Update post mime on attachment
|
||||
if (isset($params['success']) && true === $params['success'])
|
||||
{
|
||||
$newExt = $this->imageModel->getMeta()->convertMeta()->getFileFormat();
|
||||
$newGuid = str_replace($guid, $newExt, 'jpg'); // This probable doesn't work bcause doesn't update Guid with this function.
|
||||
$post_ar = array('ID' => $attach_id, 'post_mime_type' => 'image/jpeg', 'guid' => $newGuid);
|
||||
}
|
||||
elseif ( isset($params['restore']) && true === $params['restore'] )
|
||||
{
|
||||
$oldExt = $this->imageModel->getMeta()->convertMeta()->getFileFormat();
|
||||
$newGuid = str_replace($guid, 'jpg', $oldExt);
|
||||
$post_ar = array('ID' => $attach_id, 'post_mime_type' => 'image/png', 'guid' => $newGuid);
|
||||
}
|
||||
|
||||
$result = wp_update_post($post_ar);
|
||||
|
||||
if ($result === 0 || is_wp_error($result))
|
||||
{
|
||||
Log::addError('Issue updating WP Post converter - ' . $attach_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
$metadata = wp_get_attachment_metadata($attach_id);
|
||||
|
||||
if (true === $params['generate_metadata'])
|
||||
{
|
||||
$attachment = get_post( $attach_id );
|
||||
|
||||
$new_metadata = wp_generate_attachment_metadata($attach_id, $newFile->getFullPath());
|
||||
|
||||
}
|
||||
else {
|
||||
$new_metadata = array();
|
||||
}
|
||||
|
||||
// Metadata might not be array when add_attachment is calling this hook via AdminController ( PNG2JPG)
|
||||
if (is_array($metadata))
|
||||
{
|
||||
// Original Image in the new situation can not be there. Don't preserve it.
|
||||
if (isset($metadata['original_image']) && ! isset($new_metadata['original_image']) )
|
||||
{
|
||||
unset($metadata['original_image']);
|
||||
}
|
||||
|
||||
$new_metadata = array_merge($metadata, $new_metadata); // merge to preserve other custom metadata
|
||||
|
||||
}
|
||||
|
||||
if (isset($params['success']) && true === $params['success'])
|
||||
{
|
||||
do_action('shortpixel/converter/prevent-offload-off', $attach_id);
|
||||
}
|
||||
|
||||
if (is_array($new_metadata) && count($new_metadata) > 0)
|
||||
{
|
||||
$bool = wp_update_attachment_metadata($attach_id, $new_metadata);
|
||||
}
|
||||
|
||||
// Restore -sigh- fires off a later signal, because on the succesHandler in MediaLIbraryModel it may copy back backups.
|
||||
if (isset($params['restore']) && true === $params['restore'])
|
||||
{
|
||||
do_action('shortpixel/converter/prevent-offload-off', $attach_id);
|
||||
}
|
||||
|
||||
if (is_array($WPMLduplicates) && count($WPMLduplicates) > 0)
|
||||
{
|
||||
foreach ($WPMLduplicates as $duplicate_id)
|
||||
{
|
||||
update_attached_file($duplicate_id, $newFile->getFullPath() );
|
||||
wp_update_attachment_metadata($duplicate_id, $new_metadata);
|
||||
|
||||
$post_ar["ID"] = $duplicate_id;
|
||||
wp_update_post($post_ar);
|
||||
}
|
||||
}
|
||||
|
||||
$this->replacer->setTargetMeta($new_metadata);
|
||||
|
||||
}
|
||||
|
||||
|
||||
} // class
|
@ -0,0 +1,427 @@
|
||||
<?php
|
||||
namespace ShortPixel\Model\Converter;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
|
||||
use ShortPixel\Model\Image\ImageModel as ImageModel;
|
||||
use ShortPixel\Model\File\DirectoryModel as DirectoryModel;
|
||||
use ShortPixel\Model\File\FileModel as FileModel;
|
||||
use ShortPixel\Notices\NoticeController as Notices;
|
||||
|
||||
use ShortPixel\Controller\ResponseController as ResponseController;
|
||||
|
||||
use ShortPixel\Helper\DownloadHelper as DownloadHelper;
|
||||
|
||||
|
||||
class PNGConverter extends MediaLibraryConverter
|
||||
{
|
||||
protected $instance;
|
||||
|
||||
protected $current_image; // The current PHP image resource in memory
|
||||
protected $virtual_filesize;
|
||||
protected $replacer; // Replacer class Object.
|
||||
|
||||
protected $converterActive = false;
|
||||
protected $forceConvertTransparent = false;
|
||||
|
||||
protected $lastError;
|
||||
|
||||
protected $settingCheckSum;
|
||||
|
||||
|
||||
public function __construct($imageModel)
|
||||
{
|
||||
parent::__construct($imageModel);
|
||||
|
||||
$settings = \wpSPIO()->settings();
|
||||
$env = \wpSPIO()->env();
|
||||
|
||||
|
||||
$this->converterActive = (intval($settings->png2jpg) > 0) ? true : false;
|
||||
|
||||
if ($env->is_gd_installed === false)
|
||||
{
|
||||
$this->converterActive = false;
|
||||
$this->lastError = __('GD library is not active on this installation. Can\'t convert images to PNG', 'shortpixel-image-optimiser');
|
||||
}
|
||||
|
||||
$this->forceConvertTransparent = ($settings->png2jpg == 2) ? true : false;
|
||||
|
||||
// If conversion is tried, but failed somehow, it will never try it again, even after changing settings. This should prevent that switch.
|
||||
$this->settingCheckSum = intval($settings->png2jpg) + intval($settings->backupImages);
|
||||
|
||||
|
||||
}
|
||||
|
||||
public function isConvertable()
|
||||
{
|
||||
$imageModel = $this->imageModel;
|
||||
|
||||
// Settings
|
||||
if ($this->converterActive === false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extension
|
||||
if ($imageModel->getExtension() !== 'png') // not a png ext. fail silently.
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Existence
|
||||
if (! $imageModel->exists())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (true === $imageModel->getMeta()->convertMeta()->isConverted() || true === $this->hasTried($imageModel->getMeta()->convertMeta()->didTry()) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function hasTried($checksum)
|
||||
{
|
||||
if ( intval($checksum) === $this->getCheckSum())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function convert($args = array())
|
||||
{
|
||||
if (! $this->isConvertable($this->imageModel))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$fs = \wpSPIO()->filesystem();
|
||||
|
||||
$defaults = array(
|
||||
'runReplacer' => true, // The replacer doesn't need running when the file is just uploaded and doing in handle upload hook.
|
||||
);
|
||||
|
||||
$conversionArgs = array('checksum' => $this->getCheckSum());
|
||||
|
||||
$this->setupReplacer();
|
||||
$this->raiseMemoryLimit();
|
||||
|
||||
$replacementPath = $this->getReplacementPath();
|
||||
if (false === $replacementPath)
|
||||
{
|
||||
Log::addWarn('ApiConverter replacement path failed');
|
||||
$this->imageModel->getMeta()->convertMeta()->setError(self::ERROR_PATHFAIL);
|
||||
|
||||
return false; // @todo Add ResponseController something here.
|
||||
}
|
||||
|
||||
$replaceFile = $fs->getFile($replacementPath);
|
||||
Log::addDebug('Image replacement base : ' . $replaceFile->getFileBase());
|
||||
$this->imageModel->getMeta()->convertMeta()->setReplacementImageBase($replaceFile->getFileBase());
|
||||
|
||||
$prepared = $this->imageModel->conversionPrepare($conversionArgs);
|
||||
if (false === $prepared)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$args = wp_parse_args($args, $defaults);
|
||||
|
||||
if ($this->forceConvertTransparent === false && $this->isTransparent())
|
||||
{
|
||||
$this->imageModel->getMeta()->convertMeta()->setError(self::ERROR_TRANSPARENT);
|
||||
$this->imageModel->conversionFailed($conversionArgs);
|
||||
return false;
|
||||
}
|
||||
|
||||
Log::addDebug('Starting PNG conversion of #' . $this->imageModel->get('id'));
|
||||
$bool = $this->run();
|
||||
|
||||
if (true === $bool)
|
||||
{
|
||||
$params = array('success' => true);
|
||||
$this->updateMetaData($params);
|
||||
|
||||
$result = true;
|
||||
if (true === $args['runReplacer'])
|
||||
{
|
||||
$result = $this->replacer->replace();
|
||||
}
|
||||
|
||||
if (is_array($result))
|
||||
{
|
||||
foreach($result as $error)
|
||||
Notices::addError($error);
|
||||
}
|
||||
|
||||
|
||||
$this->imageModel->conversionSuccess($conversionArgs);
|
||||
|
||||
// new hook.
|
||||
do_action('shortpixel/image/convertpng2jpg_success', $this->imageModel);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->imageModel->conversionFailed($conversionArgs);
|
||||
|
||||
//legacy. Note at this point metadata has not been updated.
|
||||
do_action('shortpixel/image/convertpng2jpg_after', $this->imageModel, $args);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getCheckSum()
|
||||
{
|
||||
return intval($this->settingCheckSum);
|
||||
}
|
||||
|
||||
|
||||
protected function run()
|
||||
{
|
||||
do_action('shortpixel/image/convertpng2jpg_before', $this->imageModel);
|
||||
|
||||
$img = $this->getPNGImage();
|
||||
$fs = \wpSPIO()->filesystem();
|
||||
|
||||
$width = $this->imageModel->get('width');
|
||||
$height = $this->imageModel->get('height');
|
||||
|
||||
// If imageModel doesn't have proper width / height set. This can happen with remote files.
|
||||
if (! is_int($width) && ! $width > 0)
|
||||
{
|
||||
$width = imagesx($img);
|
||||
}
|
||||
if (! is_int($height) && ! $height > 0)
|
||||
{
|
||||
$height = imagesy($img);
|
||||
}
|
||||
|
||||
Log::addDebug("PNG2JPG doConvert width $width height $height", memory_get_usage());
|
||||
$bg = imagecreatetruecolor($width, $height);
|
||||
|
||||
if(false === $bg || false === $img)
|
||||
{
|
||||
Log::addError('ImageCreateTrueColor failed');
|
||||
if (false === $bg)
|
||||
{
|
||||
$msg = __('Creating an TrueColor Image failed - Possible library error', 'shortpixel-image-optimiser');
|
||||
}
|
||||
elseif (false === $img)
|
||||
{
|
||||
$msg = __('Image source failed - Check if source image is PNG and library is working', 'shortpixel-image-optimiser');
|
||||
}
|
||||
|
||||
$this->imageModel->getMeta()->convertMeta()->setError(self::ERROR_LIBRARY);
|
||||
ResponseController::addData($this->imageModel->get('id'), 'message', $msg);
|
||||
return false;
|
||||
}
|
||||
|
||||
imagefill($bg, 0, 0, imagecolorallocate($bg, 255, 255, 255));
|
||||
imagealphablending($bg, 1);
|
||||
imagecopy($bg, $img, 0, 0, 0, 0, $width, $height);
|
||||
|
||||
// $fsFile = $fs->getFile($image); // the original png file
|
||||
// @todo Add ResponseController support to here and getReplacementPath.
|
||||
$replacementPath = $this->getReplacementPath();
|
||||
if (false === $replacementPath)
|
||||
{
|
||||
Log::addWarn('Png2Jpg replacement path failed');
|
||||
$this->imageModel->getMeta()->convertMeta()->setError(self::ERROR_PATHFAIL);
|
||||
|
||||
return false; // @todo Add ResponseController something here.
|
||||
}
|
||||
|
||||
// check old filename, replace with uniqued filename.
|
||||
|
||||
/** Quality is set to 90 and not using WP defaults (or filter) for good reason. Lower settings very quickly degrade the libraries output quality. Better to leave this hardcoded at 90 and let the ShortPixel API handle the optimization **/
|
||||
if ($bool = imagejpeg($bg, $replacementPath, 90)) {
|
||||
Log::addDebug("PNG2JPG doConvert created JPEG at $replacementPath");
|
||||
$newSize = filesize($replacementPath); // This might invoke wrapper but ok
|
||||
|
||||
if (! is_null($this->virtual_filesize))
|
||||
{
|
||||
$origSize = $this->virtual_filesize;
|
||||
}
|
||||
else {
|
||||
$origSize = $this->imageModel->getFileSize();
|
||||
}
|
||||
|
||||
// Reload the file we just wrote.
|
||||
$newFile = $fs->getFile($replacementPath);
|
||||
|
||||
|
||||
if($newSize > $origSize * 0.95 || $newSize == 0) {
|
||||
//if the image is not 5% smaller, don't bother.
|
||||
//if the size is 0, a conversion (or disk write) problem happened, go on with the PNG
|
||||
Log::addDebug("PNG2JPG converted image is larger ($newSize vs. $origSize), keeping the PNG");
|
||||
$msg = __('Converted file is larger. Keeping original file', 'shortpixel-image-optimiser');
|
||||
ResponseController::addData($this->imageModel->get('id'), 'message', $msg);
|
||||
$newFile->delete();
|
||||
$this->imageModel->getMeta()->convertMeta()->setError(self::ERROR_RESULTLARGER);
|
||||
|
||||
return false;
|
||||
}
|
||||
elseif (! $newFile->exists())
|
||||
{
|
||||
Log::addWarn('PNG imagejpeg file not written!', $uniqueFile->getFileName() );
|
||||
$msg = __('Error - PNG file not written', 'shortpixel-image-optimiser');
|
||||
ResponseController::addData($this->imageModel->get('id'), 'message', $msg);
|
||||
$this->imageModel->getMeta()->convertMeta()->setError(self::ERROR_WRITEERROR);
|
||||
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
$this->newFile = $newFile;
|
||||
}
|
||||
|
||||
|
||||
Log::addDebug('PNG2jPG Converted');
|
||||
}
|
||||
|
||||
$fs->flushImageCache();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function restore()
|
||||
{
|
||||
$params = array(
|
||||
'restore' => true,
|
||||
);
|
||||
$fs = \wpSPIO()->filesystem();
|
||||
|
||||
$this->setupReplacer();
|
||||
|
||||
$oldFileName = $this->imageModel->getFileName(); // Old File Name, Still .jpg
|
||||
$newFileName = $this->imageModel->getFileBase() . '.png';
|
||||
|
||||
if ($this->imageModel->isScaled())
|
||||
{
|
||||
$oldFileName = $this->imageModel->getOriginalFile()->getFileName();
|
||||
$newFileName = $this->imageModel->getOriginalFile()->getFileBase() . '.png';
|
||||
}
|
||||
|
||||
$fsNewFile = $fs->getFile($this->imageModel->getFileDir() . $newFileName);
|
||||
|
||||
$this->newFile = $fsNewFile;
|
||||
$this->setTarget($fsNewFile);
|
||||
|
||||
$this->updateMetaData($params);
|
||||
$result = $this->replacer->replace();
|
||||
|
||||
$fs->flushImageCache();
|
||||
|
||||
}
|
||||
|
||||
protected function isTransparent() {
|
||||
$isTransparent = false;
|
||||
$transparent_pixel = $bg = false;
|
||||
|
||||
$imagePath = $this->imageModel->getFullPath();
|
||||
|
||||
// Check for transparency at the bit path.
|
||||
if(ord(file_get_contents($imagePath, false, null, 25, 1)) & 4) {
|
||||
Log::addDebug("PNG2JPG: 25th byte has third bit 1 - transparency");
|
||||
$isTransparent = true;
|
||||
// return true;
|
||||
} else {
|
||||
|
||||
$contents = file_get_contents($imagePath);
|
||||
if (stripos($contents, 'PLTE') !== false && stripos($contents, 'tRNS') !== false) {
|
||||
$isTransparent = true;
|
||||
}
|
||||
if (false === $isTransparent) {
|
||||
|
||||
$width = $this->imageModel->get('width');
|
||||
$height = $this->imageModel->get('height');
|
||||
Log::addDebug("PNG2JPG Image width: " . $width . " height: " . $height . " aprox. size: " . round($width*$height*5/1024/1024) . "M memory limit: " . ini_get('memory_limit') . " USED: " . memory_get_usage());
|
||||
|
||||
$image = $this->getPNGImage();
|
||||
if (false === $image)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Log::addDebug("PNG2JPG width $width height $height. Now checking pixels.");
|
||||
//run through pixels until transparent pixel is found:
|
||||
for ($i = 0; $i < $width; $i++) {
|
||||
for ($j = 0; $j < $height; $j++) {
|
||||
$rgba = imagecolorat($image, $i, $j);
|
||||
if (($rgba & 0x7F000000) >> 24) {
|
||||
$isTransparent = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} // non-transparant.
|
||||
|
||||
Log::addDebug("PNG2JPG is " . (false === $isTransparent ? " not" : "") . " transparent");
|
||||
|
||||
return $isTransparent;
|
||||
|
||||
}
|
||||
|
||||
/** Try to load resource and an PNG via library */
|
||||
protected function getPNGImage()
|
||||
{
|
||||
if (is_object($this->current_image))
|
||||
{
|
||||
return $this->current_image;
|
||||
}
|
||||
|
||||
if ($this->imageModel->isScaled())
|
||||
{
|
||||
$imagePath = $this->imageModel->getOriginalFile()->getFullPath();
|
||||
}
|
||||
else {
|
||||
$imagePath = $this->imageModel->getFullPath();
|
||||
}
|
||||
|
||||
if (true === $this->imageModel->is_virtual())
|
||||
{
|
||||
$downloadHelper = DownloadHelper::getInstance();
|
||||
Log::addDebug('PNG converter: Item is remote, attempting to download');
|
||||
|
||||
$tempFile = $downloadHelper->downloadFile($this->imageModel->getURL());
|
||||
if (is_object($tempFile))
|
||||
{
|
||||
$imagePath = $tempFile->getFullPath();
|
||||
$this->virtual_filesize = $tempFile->getFileSize();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$image = @imagecreatefrompng($imagePath);
|
||||
if (! $image)
|
||||
{
|
||||
$this->current_image = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->current_image = $image;
|
||||
}
|
||||
|
||||
return $this->current_image;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Try to increase limits when doing heavy processing
|
||||
private function raiseMemoryLimit()
|
||||
{
|
||||
if(function_exists('wp_raise_memory_limit')) {
|
||||
wp_raise_memory_limit( 'image' );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} // class
|
@ -0,0 +1,328 @@
|
||||
<?php
|
||||
namespace ShortPixel\Model;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
|
||||
|
||||
/** Loads a few environment variables handy to have nearby
|
||||
*
|
||||
* Notice - This is meant to be loaded via the plugin class. Easy access with wpSPIO()->getEnv().
|
||||
*/
|
||||
class EnvironmentModel extends \ShortPixel\Model
|
||||
{
|
||||
// Server and PHP
|
||||
public $is_nginx;
|
||||
public $is_apache;
|
||||
public $is_gd_installed;
|
||||
public $is_curl_installed;
|
||||
private $disabled_functions = array();
|
||||
|
||||
// MultiSite
|
||||
public $is_multisite;
|
||||
public $is_mainsite;
|
||||
|
||||
// Integrations
|
||||
public $has_nextgen;
|
||||
|
||||
// WordPress
|
||||
public $is_front = false;
|
||||
public $is_admin = false;
|
||||
public $is_ajaxcall = false;
|
||||
|
||||
private $screen_is_set = false;
|
||||
public $is_screen_to_use = false; // where shortpixel optimizer loads
|
||||
public $is_our_screen = false; // where shortpixel hooks in more complicated functions.
|
||||
public $is_gutenberg_editor = false;
|
||||
public $is_bulk_page = false; // ShortPixel bulk screen.
|
||||
public $screen_id = false;
|
||||
|
||||
// Debug flag
|
||||
public $is_debug = false;
|
||||
// Is the plugin configured to automatically optimize on upload hook?
|
||||
public $is_autoprocess = false;
|
||||
|
||||
protected static $instance;
|
||||
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->setServer();
|
||||
$this->setWordPress();
|
||||
add_action('plugins_loaded', array($this, 'setIntegrations') ); // not set on construct.
|
||||
add_action('current_screen', array($this, 'setScreen') ); // Not set on construct
|
||||
}
|
||||
|
||||
public static function getInstance()
|
||||
{
|
||||
if (is_null(self::$instance))
|
||||
self::$instance = new EnvironmentModel();
|
||||
|
||||
/*if (! self::$instance->screen_is_set)
|
||||
self::$instance->setScreen(); */
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/** Check ENV is a specific function is allowed. Use this with functions that might be turned off on configurations
|
||||
* @param $function String The name of the function being tested
|
||||
* Note: In future this function can be extended with other function edge cases.
|
||||
*/
|
||||
public function is_function_usable($function)
|
||||
{
|
||||
if (count($this->disabled_functions) == 0)
|
||||
{
|
||||
$disabled = ini_get('disable_functions');
|
||||
$this->disabled_functions = explode(',', $disabled);
|
||||
}
|
||||
|
||||
if (isset($this->disabled_functions[$function]))
|
||||
return false;
|
||||
|
||||
if (function_exists($function))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function checkPHPVersion($needed)
|
||||
{
|
||||
|
||||
if (version_compare(PHP_VERSION, $needed) >= 0 )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function plugin_active($name)
|
||||
{
|
||||
switch($name)
|
||||
{
|
||||
case 'wpml':
|
||||
$plugin = 'sitepress-multilingual-cms/sitepress.php';
|
||||
break;
|
||||
case 'polylang':
|
||||
$plugin = 'polylang/polylang.php';
|
||||
break;
|
||||
case 'spai':
|
||||
$plugin = 'shortpixel-adaptive-images/short-pixel-ai.php';
|
||||
break;
|
||||
case 's3-offload':
|
||||
$plugin = 'amazon-s3-and-cloudfront/wordpress-s3.php';
|
||||
break;
|
||||
case 'woocommerce':
|
||||
$plugin = 'woocommerce/woocommerce.php';
|
||||
break;
|
||||
default:
|
||||
$plugin = 'none';
|
||||
break;
|
||||
}
|
||||
|
||||
if (!function_exists('is_plugin_active')) {
|
||||
include_once(ABSPATH . 'wp-admin/includes/plugin.php');
|
||||
}
|
||||
|
||||
return \is_plugin_active($plugin);
|
||||
}
|
||||
|
||||
//https://www.php.net/manual/en/function.sys-getloadavg.php
|
||||
public function getSystemLoad()
|
||||
{
|
||||
$load = sys_getloadavg();
|
||||
|
||||
}
|
||||
|
||||
/* https://github.com/WordPress/WordPress/blob/master/wp-includes/class-wp-image-editor-imagick.php */
|
||||
public function hasImagick()
|
||||
{
|
||||
$editor = wp_get_image_editor(\wpSPIO()->plugin_path('res/img/test.jpg'));
|
||||
$className = get_class($editor);
|
||||
|
||||
if ($className == 'WP_Image_Editor_Imagick')
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
public function hasOffload()
|
||||
{
|
||||
$off = \ShortPixel\External\Offload\Offloader::getInstance();
|
||||
$name = $off->getOffloadName();
|
||||
if (is_null($name))
|
||||
return false;
|
||||
else
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getOffloadName()
|
||||
{
|
||||
$off = \ShortPixel\External\Offload\Offloader::getInstance();
|
||||
$name = $off->getOffloadName();
|
||||
return $name;
|
||||
}
|
||||
|
||||
public function useVirtualHeavyFunctions()
|
||||
{
|
||||
$bool = apply_filters('shortpixel/file/virtual/heavy_features', true);
|
||||
return $bool;
|
||||
}
|
||||
|
||||
private function setServer()
|
||||
{
|
||||
$this->is_nginx = ! empty($_SERVER["SERVER_SOFTWARE"]) && strpos(strtolower(wp_unslash($_SERVER["SERVER_SOFTWARE"])), 'nginx') !== false ? true : false;
|
||||
$this->is_apache = ! empty($_SERVER["SERVER_SOFTWARE"]) && strpos(strtolower(wp_unslash($_SERVER["SERVER_SOFTWARE"])), 'apache') !== false ? true : false;
|
||||
$this->is_gd_installed = function_exists('imagecreatefrompng') && function_exists('imagejpeg');
|
||||
$this->is_curl_installed = function_exists('curl_init');
|
||||
}
|
||||
|
||||
|
||||
private function setWordPress()
|
||||
{
|
||||
$this->is_multisite = (function_exists("is_multisite") && is_multisite()) ? true : false;
|
||||
$this->is_mainsite = (function_exists('is_main_site') && true === is_main_site()) ? true : false;
|
||||
|
||||
$this->determineFrontBack();
|
||||
|
||||
if (wp_doing_ajax())
|
||||
{
|
||||
$this->is_ajaxcall = true;
|
||||
}
|
||||
|
||||
$this->is_debug = Log::debugIsActive();
|
||||
|
||||
if (\wpSPIO()->settings()->autoMediaLibrary == 1)
|
||||
$this->is_autoprocess = true;
|
||||
|
||||
}
|
||||
|
||||
// check if this request is front or back.
|
||||
protected function determineFrontBack()
|
||||
{
|
||||
if ( is_admin() || wp_doing_ajax() )
|
||||
$this->is_admin = true;
|
||||
else
|
||||
$this->is_front = true;
|
||||
|
||||
}
|
||||
|
||||
public function setScreen($screen)
|
||||
{
|
||||
// WordPress pages where we'll be active on.
|
||||
// https://codex.wordpress.org/Plugin_API/Admin_Screen_Reference
|
||||
$use_screens = array(
|
||||
'edit-post_tag', // edit tags
|
||||
'upload', // media library
|
||||
'attachment', // edit media
|
||||
'post', // post screen
|
||||
'page', // page editor
|
||||
'edit-post', // edit post
|
||||
'new-post', // new post
|
||||
'edit-page', // all pages
|
||||
'media', // add new item screen
|
||||
);
|
||||
$use_screens = apply_filters('shortpixel/init/optimize_on_screens', $use_screens);
|
||||
|
||||
$this->screen_id = $screen->id;
|
||||
if(is_array($use_screens) && in_array($screen->id, $use_screens)) {
|
||||
$this->is_screen_to_use = true;
|
||||
}
|
||||
|
||||
// Our pages.
|
||||
$pages = \wpSPIO()->get_admin_pages();
|
||||
// the main WP pages where SPIO hooks a lot of functions into, our operating area.
|
||||
$wp_pages = array('upload', 'attachment');
|
||||
$pages = array_merge($pages, $wp_pages);
|
||||
|
||||
/* pages can be null in certain cases i.e. plugin activation.
|
||||
* treat those cases as improper screen set.
|
||||
*/
|
||||
if (is_null($pages))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( in_array($screen->id, $pages))
|
||||
{
|
||||
$this->is_screen_to_use = true;
|
||||
$this->is_our_screen = true;
|
||||
|
||||
// Strpos instead of full screen id, because the first page (media_page) is not reliable and can change.
|
||||
if ( strpos($screen->id, 'wp-short-pixel-bulk') !== false)
|
||||
$this->is_bulk_page = true;
|
||||
}
|
||||
elseif (is_object($screen) && method_exists( $screen, 'is_block_editor' ) && $screen->is_block_editor() ) {
|
||||
$this->is_screen_to_use = true;
|
||||
$this->is_gutenberg_editor = true;
|
||||
}
|
||||
|
||||
$this->screen_is_set = true;
|
||||
}
|
||||
|
||||
public function setIntegrations()
|
||||
{
|
||||
$ng = \ShortPixel\NextGenController::getInstance();
|
||||
$this->has_nextgen = $ng->has_nextgen();
|
||||
}
|
||||
|
||||
//set default move as "list". only set once, it won't try to set the default mode again.
|
||||
public function setDefaultViewModeList()
|
||||
{
|
||||
$settings = \wpSPIO()->settings();
|
||||
if( $settings->mediaLibraryViewMode == false)
|
||||
{
|
||||
$settings->mediaLibraryViewMode = 1;
|
||||
$currentUserID = false;
|
||||
if ( function_exists('wp_get_current_user') ) {
|
||||
$current_user = wp_get_current_user();
|
||||
$currentUserID = $current_user->ID;
|
||||
update_user_meta($currentUserID, "wp_media_library_mode", "list");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function getRelativePluginSlug()
|
||||
{
|
||||
$dir = SHORTPIXEL_PLUGIN_DIR;
|
||||
$file = SHORTPIXEL_PLUGIN_FILE;
|
||||
|
||||
$fs = \wpSPIO()->filesystem();
|
||||
|
||||
$plugins_dir = $fs->getDirectory($dir)->getParent();
|
||||
|
||||
$slug = str_replace($plugins_dir->getPath(), '', $file);
|
||||
|
||||
return $slug;
|
||||
}
|
||||
|
||||
public function useDoubleWebpExtension()
|
||||
{
|
||||
if (defined('SHORTPIXEL_USE_DOUBLE_WEBP_EXTENSION') && SHORTPIXEL_USE_DOUBLE_WEBP_EXTENSION)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function useDoubleAvifExtension()
|
||||
{
|
||||
if (defined('SHORTPIXEL_USE_DOUBLE_AVIF_EXTENSION') && SHORTPIXEL_USE_DOUBLE_AVIF_EXTENSION)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function useTrustedMode()
|
||||
{
|
||||
if (defined('SHORTPIXEL_TRUSTED_MODE') && true === SHORTPIXEL_TRUSTED_MODE)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,557 @@
|
||||
<?php
|
||||
namespace ShortPixel\Model\File;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
|
||||
|
||||
use ShortPixel\Helper\UtilHelper as UtilHelper;
|
||||
|
||||
/* Model for Directories
|
||||
*
|
||||
* For all low-level operations on directories
|
||||
* Private model of FileSystemController. Please get your directories via there.
|
||||
*
|
||||
*/
|
||||
|
||||
class DirectoryModel extends \ShortPixel\Model
|
||||
{
|
||||
// Directory info
|
||||
protected $path;
|
||||
protected $name;
|
||||
|
||||
// Directory status
|
||||
protected $exists = null;
|
||||
protected $is_writable = null;
|
||||
protected $is_readable = null;
|
||||
protected $is_virtual = null;
|
||||
|
||||
protected $fields = array();
|
||||
|
||||
protected $new_directory_permission = 0755;
|
||||
|
||||
public static $TRUSTED_MODE = false;
|
||||
|
||||
|
||||
/** Creates a directory model object. DirectoryModel directories don't need to exist on FileSystem
|
||||
*
|
||||
* When a filepath is given, it will remove the file part.
|
||||
* @param $path String The Path
|
||||
*/
|
||||
public function __construct($path)
|
||||
{
|
||||
$fs = \wpSPIO()->filesystem();
|
||||
|
||||
// Test if path actually has someting, otherwise just bail.
|
||||
if (strlen(trim($path)) == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($fs->pathIsUrl($path))
|
||||
{
|
||||
$pathinfo = pathinfo($path);
|
||||
if (isset($pathinfo['extension'])) // check if this is a file, remove the file information.
|
||||
{
|
||||
$path = $pathinfo['dirname'];
|
||||
}
|
||||
|
||||
$this->is_virtual = true;
|
||||
$this->is_readable = true; // assume
|
||||
$this->exists = true;
|
||||
}
|
||||
|
||||
// When in trusted mode prevent filesystem checks as much as possible.
|
||||
if (true === self::$TRUSTED_MODE)
|
||||
{
|
||||
$this->exists = true;
|
||||
$this->is_writable = true;
|
||||
$this->is_readable = true;
|
||||
}
|
||||
|
||||
// On virtual situation this would remove the slashes on :// , causing issues with offload et al.
|
||||
if (false === $this->is_virtual)
|
||||
$path = UtilHelper::spNormalizePath($path);
|
||||
|
||||
if (! $this->is_virtual() && ! is_dir($path) ) // path is wrong, *or* simply doesn't exist.
|
||||
{
|
||||
/* Test for file input.
|
||||
* If pathinfo is fed a fullpath, it rips of last entry without setting extension, don't further trust.
|
||||
* If it's a file extension is set, then trust.
|
||||
*/
|
||||
$pathinfo = pathinfo($path);
|
||||
|
||||
|
||||
if (isset($pathinfo['extension']))
|
||||
{
|
||||
$path = $pathinfo['dirname'];
|
||||
}
|
||||
elseif (is_file($path))
|
||||
$path = dirname($path);
|
||||
}
|
||||
|
||||
if (! $this->is_virtual() && ! is_dir($path))
|
||||
{
|
||||
/* Check if realpath improves things. We support non-existing paths, which realpath fails on, so only apply on result.
|
||||
Moved realpath to check after main pathinfo is set. Reason is that symlinked directories which don't include the WordPress upload dir will start to fail in file_model on processpath ( doesn't see it as a wp path, starts to try relative path). Not sure if realpath should be used anyhow in this model /BS
|
||||
*/
|
||||
$testpath = realpath($path);
|
||||
if ($testpath)
|
||||
$path = $testpath;
|
||||
}
|
||||
|
||||
$this->path = trailingslashit($path);
|
||||
|
||||
// Basename doesn't work properly on non-latin ( cyrillic, greek etc ) directory names, returning the parent path instead.
|
||||
$dir = new \SplFileInfo($path);
|
||||
//basename($this->path);
|
||||
$this->name = $dir->getFileName();
|
||||
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return (string) $this->path;
|
||||
}
|
||||
|
||||
/** Returns path *with* trailing slash
|
||||
*
|
||||
* @return String Path with trailing slash
|
||||
*/
|
||||
public function getPath()
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
public function getModified()
|
||||
{
|
||||
return filemtime($this->path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get basename of the directory. Without path
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function exists()
|
||||
{
|
||||
if (is_null($this->exists))
|
||||
{
|
||||
$this->exists = file_exists($this->path) && is_dir($this->path);
|
||||
}
|
||||
return $this->exists;
|
||||
}
|
||||
|
||||
public function is_writable()
|
||||
{
|
||||
if (is_null($this->is_writable))
|
||||
{
|
||||
$this->is_writable = is_writable($this->path);
|
||||
}
|
||||
return $this->is_writable;
|
||||
}
|
||||
|
||||
|
||||
public function is_readable()
|
||||
{
|
||||
if (is_null($this->is_readable))
|
||||
{
|
||||
$this->is_readable = is_readable($this->path);
|
||||
}
|
||||
|
||||
return $this->is_readable;
|
||||
}
|
||||
|
||||
public function is_virtual()
|
||||
{
|
||||
return $this->is_virtual;
|
||||
}
|
||||
/** Try to obtain the path, minus the installation directory.
|
||||
* @return Mixed False if this didn't work, Path as string without basedir if it did. With trailing slash, without starting slash.
|
||||
*/
|
||||
public function getRelativePath()
|
||||
{
|
||||
// not used anywhere in directory.
|
||||
// $upload_dir = wp_upload_dir(null, false);
|
||||
|
||||
$install_dir = get_home_path();
|
||||
if($install_dir == '/') {
|
||||
$install_dir = \wpSPIO()->filesystem()->getWPAbsPath();
|
||||
}
|
||||
|
||||
$install_dir = trailingslashit($install_dir);
|
||||
|
||||
$path = $this->getPath();
|
||||
// try to build relativePath without first slash.
|
||||
$relativePath = str_replace($install_dir, '', $path);
|
||||
|
||||
if (is_dir( $install_dir . $relativePath) === false)
|
||||
{
|
||||
$test_path = $this->reverseConstructPath($path, $install_dir);
|
||||
if ($test_path !== false)
|
||||
{
|
||||
$relativePath = $test_path;
|
||||
}
|
||||
else {
|
||||
if($test_path = $this->constructUsualDirectories($path))
|
||||
{
|
||||
$relativePath = $test_path;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// If relativePath has less amount of characters, changes are this worked.
|
||||
if (strlen($path) > strlen($relativePath))
|
||||
{
|
||||
return ltrim(trailingslashit($relativePath), '/');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private function reverseConstructPath($path, $install_path)
|
||||
{
|
||||
// Array value to reset index
|
||||
$pathar = array_values(array_filter(explode('/', $path)));
|
||||
$parts = array();
|
||||
|
||||
if (is_array($pathar))
|
||||
{
|
||||
// Reverse loop the structure until solid ground is found.
|
||||
for ($i = (count($pathar)); $i > 0; $i--)
|
||||
{
|
||||
$parts[] = $pathar[$i - 1];
|
||||
$testpath = implode('/', array_reverse($parts));
|
||||
// if the whole thing exists
|
||||
if (is_dir($install_path . $testpath) === true)
|
||||
{
|
||||
return $testpath;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* Last Resort function to just reduce path to various known WorPress paths. */
|
||||
private function constructUsualDirectories($path)
|
||||
{
|
||||
$pathar = array_values(array_filter(explode('/', $path))); // array value to reset index
|
||||
$testpath = false;
|
||||
if ( ($key = array_search('wp-content', $pathar)) !== false)
|
||||
{
|
||||
$testpath = implode('/', array_slice($pathar, $key));
|
||||
}
|
||||
elseif ( ($key = array_search('uploads', $pathar)) !== false)
|
||||
{
|
||||
$testpath = implode('/', array_slice($pathar, $key));
|
||||
}
|
||||
|
||||
return $testpath;
|
||||
}
|
||||
|
||||
/** Checks the directory into working order
|
||||
* Tries to create directory if it doesn't exist
|
||||
* Tries to fix file permission if writable is needed
|
||||
* @param $check_writable Boolean Directory should be writable
|
||||
*/
|
||||
public function check($check_writable = false)
|
||||
{
|
||||
$permission = $this->getPermissionRecursive();
|
||||
|
||||
if ($permission === false) // if something wrong, return to default.
|
||||
{
|
||||
$permission = $this->new_directory_permission;
|
||||
}
|
||||
|
||||
if (! $this->exists())
|
||||
{
|
||||
|
||||
Log::addInfo('Directory does not exists. Try to create recursive ' . $this->path . ' with ' . $permission);
|
||||
|
||||
|
||||
$result = @mkdir($this->path, $permission , true);
|
||||
chmod ($this->path, $permission );
|
||||
|
||||
if (! $result)
|
||||
{
|
||||
$error = error_get_last();
|
||||
Log::addWarn('MkDir failed: ' . $error['message'], array($error));
|
||||
}
|
||||
// reset.
|
||||
$this->exists = null;
|
||||
$this->is_readable = null;
|
||||
$this->is_writable = null;
|
||||
|
||||
}
|
||||
if ($this->exists() && $check_writable && ! $this->is_writable())
|
||||
{
|
||||
chmod($this->path, $permission);
|
||||
if (! $this->is_writable()) // perhaps parent permission is no good.
|
||||
{
|
||||
chmod($this->path, $this->new_directory_permission);
|
||||
}
|
||||
}
|
||||
|
||||
if (! $this->exists())
|
||||
{
|
||||
Log::addInfo('Directory does not exist :' . $this->path);
|
||||
return false;
|
||||
}
|
||||
if ($check_writable && !$this->is_writable())
|
||||
{
|
||||
Log::addInfo('Directory not writable :' . $this->path);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getPermissionRecursive()
|
||||
{
|
||||
$parent = $this->getParent();
|
||||
if (! $parent->exists())
|
||||
{
|
||||
return $parent->getPermissionRecursive();
|
||||
}
|
||||
else
|
||||
{
|
||||
return $parent->getPermissions();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* Get files from directory
|
||||
* @returns Array|boolean Returns false if something wrong w/ directory, otherwise a files array of FileModel Object.
|
||||
*/
|
||||
public function getFiles($args = array())
|
||||
{
|
||||
|
||||
$defaults = array(
|
||||
'date_newer' => null,
|
||||
'exclude_files' => null,
|
||||
'include_files' => null,
|
||||
);
|
||||
$args = wp_parse_args($args, $defaults);
|
||||
|
||||
// if all filters are set to null, so point in checking those.
|
||||
$has_filters = (count(array_filter($args)) > 0) ? true : false;
|
||||
|
||||
if ( ! $this->exists() || ! $this->is_readable() )
|
||||
return false;
|
||||
|
||||
$fileArray = array();
|
||||
|
||||
if ($handle = opendir($this->path)) {
|
||||
while (false !== ($entry = readdir($handle))) {
|
||||
if ( ($entry != "." && $entry != "..") && ! is_dir($this->path . $entry) ) {
|
||||
|
||||
|
||||
$fileObj = new FileModel($this->path . $entry);
|
||||
if ($has_filters)
|
||||
{
|
||||
if ($this->fileFilter($fileObj,$args) === false)
|
||||
{
|
||||
$fileObj = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (! is_null($fileObj))
|
||||
$fileArray[] = $fileObj;
|
||||
}
|
||||
}
|
||||
closedir($handle);
|
||||
}
|
||||
|
||||
/*
|
||||
if ($has_filters)
|
||||
{
|
||||
$fileArray = array_filter($fileArray, function ($file) use ($args) {
|
||||
return $this->fileFilter($file, $args);
|
||||
} );
|
||||
} */
|
||||
return $fileArray;
|
||||
}
|
||||
|
||||
// @return boolean true if it should be kept in array, false if not.
|
||||
private function fileFilter(FileModel $file, $args)
|
||||
{
|
||||
$filter = true;
|
||||
|
||||
if (! is_null($args['include_files']))
|
||||
{
|
||||
foreach($args['include_files'] as $inc)
|
||||
{
|
||||
// If any in included is true, filter is good for us.
|
||||
$filter = false;
|
||||
if (strpos( strtolower($file->getRawFullPath()), strtolower($inc) ) !== false)
|
||||
{
|
||||
$filter = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (! is_null($args['date_newer']))
|
||||
{
|
||||
$modified = $file->getModified();
|
||||
if ($modified < $args['date_newer'] )
|
||||
$filter = false;
|
||||
}
|
||||
if (! is_null($args['exclude_files']))
|
||||
{
|
||||
foreach($args['exclude_files'] as $ex)
|
||||
{
|
||||
if (strpos( strtolower($file->getRawFullPath()), strtolower($ex) ) !== false)
|
||||
$filter = false;
|
||||
}
|
||||
}
|
||||
|
||||
return $filter;
|
||||
}
|
||||
|
||||
/** Get subdirectories from directory
|
||||
* * @returns Array|boolean Returns false if something wrong w/ directory, otherwise a files array of DirectoryModel Object.
|
||||
*/
|
||||
public function getSubDirectories()
|
||||
{
|
||||
|
||||
if (! $this->exists() || ! $this->is_readable())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$dirIt = new \DirectoryIterator($this->path);
|
||||
$dirArray = array();
|
||||
foreach ($dirIt as $fileInfo)
|
||||
{ // IsDot must go first here, or there is possiblity to run into openbasedir restrictions.
|
||||
if (! $fileInfo->isDot() && $fileInfo->isDir() && $fileInfo->isReadable())
|
||||
{
|
||||
if ('ShortPixel\Model\File\DirectoryOtherMediaModel' == get_called_class())
|
||||
{
|
||||
$dir = new DirectoryOtherMediaModel($fileInfo->getRealPath());
|
||||
}
|
||||
else
|
||||
{
|
||||
$dir = new DirectoryModel($fileInfo->getRealPath());
|
||||
}
|
||||
|
||||
if ($dir->exists())
|
||||
$dirArray[] = $dir;
|
||||
}
|
||||
|
||||
}
|
||||
return $dirArray;
|
||||
}
|
||||
|
||||
/** Check if this dir is a subfolder
|
||||
* @param DirectoryModel The directoryObject that is tested as the parent */
|
||||
public function isSubFolderOf(DirectoryModel $dir)
|
||||
{
|
||||
// the same path, is not a subdir of.
|
||||
if ($this->getPath() === $dir->getPath())
|
||||
return false;
|
||||
|
||||
// the main path must be followed from the beginning to be a subfolder.
|
||||
if (strpos($this->getPath(), $dir->getPath() ) === 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//** Note, use sparingly, recursive function
|
||||
public function getFolderSize()
|
||||
{
|
||||
// \wpSPIO()->filesystem()->getFilesRecursive($this)
|
||||
$size = 0;
|
||||
$files = $this->getFiles();
|
||||
|
||||
// GetFiles can return Boolean false on missing directory.
|
||||
if (! is_array($files))
|
||||
{
|
||||
return $size;
|
||||
}
|
||||
|
||||
foreach($files as $fileObj)
|
||||
{
|
||||
$size += $fileObj->getFileSize();
|
||||
}
|
||||
unset($files); //attempt at performance.
|
||||
|
||||
$subdirs = $this->getSubDirectories();
|
||||
|
||||
foreach($subdirs as $subdir)
|
||||
{
|
||||
$size += $subdir->getFolderSize();
|
||||
}
|
||||
|
||||
return $size;
|
||||
}
|
||||
|
||||
/** Get this paths parent */
|
||||
public function getParent()
|
||||
{
|
||||
$path = $this->getPath();
|
||||
$parentPath = dirname($path);
|
||||
|
||||
$parentDir = new DirectoryModel($parentPath);
|
||||
|
||||
return $parentDir;
|
||||
}
|
||||
|
||||
public function getPermissions()
|
||||
{
|
||||
if (! $this->exists())
|
||||
{
|
||||
Log::addWarning('Directory not existing (fileperms): '. $this->getPath() );
|
||||
return false;
|
||||
}
|
||||
$perms = fileperms($this->getPath());
|
||||
|
||||
if ($perms !== false)
|
||||
{
|
||||
return $perms;
|
||||
}
|
||||
else
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
public function delete()
|
||||
{
|
||||
return rmdir($this->getPath());
|
||||
}
|
||||
|
||||
/** This will try to remove the whole structure. Use with care.
|
||||
* This is mostly used to clear the backups.
|
||||
*/
|
||||
public function recursiveDelete()
|
||||
{
|
||||
if (! $this->exists() || ! $this->is_writable())
|
||||
return false;
|
||||
|
||||
// This is a security measure to prevent unintended wipes.
|
||||
$wpdir = \wpSPIO()->filesystem()->getWPUploadBase();
|
||||
if (! $this->isSubFolderOf($wpdir))
|
||||
return false;
|
||||
|
||||
$files = $this->getFiles();
|
||||
$subdirs = $this->getSubDirectories();
|
||||
|
||||
foreach($files as $file)
|
||||
$file->delete();
|
||||
|
||||
foreach($subdirs as $subdir)
|
||||
$subdir->recursiveDelete();
|
||||
|
||||
$this->delete();
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,637 @@
|
||||
<?php
|
||||
namespace ShortPixel\Model\File;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
|
||||
use ShortPixel\Notices\NoticeController as Notice;
|
||||
|
||||
use \ShortPixel\Model\File\DirectoryModel as DirectoryModel;
|
||||
use \ShortPixel\Model\Image\ImageModel as ImageModel;
|
||||
|
||||
use ShortPixel\Controller\OptimizeController as OptimizeController;
|
||||
use ShortPixel\Controller\OtherMediaController as OtherMediaController;
|
||||
|
||||
// extends DirectoryModel. Handles ShortPixel_meta database table
|
||||
// Replacing main parts of shortpixel-folder
|
||||
class DirectoryOtherMediaModel extends DirectoryModel
|
||||
{
|
||||
|
||||
protected $id = -1; // if -1, this might not exist yet in Dbase. Null is not used, because that messes with isset
|
||||
|
||||
protected $name;
|
||||
protected $status = 0;
|
||||
protected $fileCount = 0; // inherent onreliable statistic in dbase. When insert / batch insert the folder count could not be updated, only on refreshFolder which is a relative heavy function to use on every file upload. Totals are better gotten from a stat-query, on request.
|
||||
protected $updated = 0;
|
||||
protected $created = 0;
|
||||
protected $checked = 0;
|
||||
protected $path_md5;
|
||||
|
||||
protected $is_nextgen = false;
|
||||
protected $in_db = false;
|
||||
protected $is_removed = false;
|
||||
|
||||
protected $last_message;
|
||||
|
||||
//protected $stats;
|
||||
|
||||
protected static $stats;
|
||||
|
||||
const DIRECTORY_STATUS_REMOVED = -1;
|
||||
const DIRECTORY_STATUS_NORMAL = 0;
|
||||
const DIRECTORY_STATUS_NEXTGEN = 1;
|
||||
|
||||
/** Path or Folder Object, from SpMetaDao
|
||||
*
|
||||
*/
|
||||
public function __construct($path)
|
||||
{
|
||||
|
||||
if (is_object($path)) // Load directly via Database object, this saves a query.
|
||||
{
|
||||
$folder = $path;
|
||||
$path = $folder->path;
|
||||
|
||||
parent::__construct($path);
|
||||
$this->loadFolder($folder);
|
||||
}
|
||||
else
|
||||
{
|
||||
parent::__construct($path);
|
||||
$this->loadFolderbyPath($path);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function get($name)
|
||||
{
|
||||
if (property_exists($this, $name))
|
||||
return $this->$name;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function set($name, $value)
|
||||
{
|
||||
if (property_exists($this, $name))
|
||||
{
|
||||
$this->$name = $value;
|
||||
return true;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function getAllStats()
|
||||
{
|
||||
if (is_null(self::$stats))
|
||||
{
|
||||
global $wpdb;
|
||||
$sql = 'SELECT SUM(CASE WHEN status = 2 OR status = -11 THEN 1 ELSE 0 END) optimized, SUM(CASE WHEN status = 0 THEN 1 ELSE 0 END) waiting, count(*) total, folder_id FROM ' . $wpdb->prefix . 'shortpixel_meta GROUP BY folder_id';
|
||||
|
||||
$result = $wpdb->get_results($sql, ARRAY_A);
|
||||
|
||||
$stats = array();
|
||||
foreach($result as $rawdata)
|
||||
{
|
||||
$folder_id = $rawdata['folder_id'];
|
||||
|
||||
$data = array(
|
||||
'optimized' => (int) $rawdata['optimized'],
|
||||
'waiting' => (int) $rawdata['waiting'],
|
||||
'total' => (int) $rawdata['total'],
|
||||
);
|
||||
$stats[$folder_id] = $data;
|
||||
}
|
||||
|
||||
self::$stats = $stats;
|
||||
}
|
||||
|
||||
return self::$stats;
|
||||
}
|
||||
|
||||
public function getStats()
|
||||
{
|
||||
$stats = self::getAllStats(); // Querying all stats is more efficient than one-by-one
|
||||
|
||||
if (isset($stats[$this->id]))
|
||||
{
|
||||
return $stats[$this->id];
|
||||
}
|
||||
else {
|
||||
global $wpdb;
|
||||
$sql = "SELECT SUM(CASE WHEN status = 2 OR status = -11 THEN 1 ELSE 0 END) optimized, "
|
||||
. "SUM(CASE WHEN status = 0 THEN 1 ELSE 0 END) waiting, count(*) total "
|
||||
. "FROM " . $wpdb->prefix . "shortpixel_meta "
|
||||
. "WHERE folder_id = %d";
|
||||
$sql = $wpdb->prepare($sql, $this->id);
|
||||
$res = $wpdb->get_row($sql, ARRAY_A);
|
||||
|
||||
if (is_array($res))
|
||||
{
|
||||
$result = array(
|
||||
'optimized' => (int) $res['optimized'],
|
||||
'waiting' => (int) $res['waiting'],
|
||||
'total' => (int) $res['total'],
|
||||
|
||||
);
|
||||
return $result;
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
// Simple Update
|
||||
global $wpdb;
|
||||
$data = array(
|
||||
// 'id' => $this->id,
|
||||
'status' => $this->status,
|
||||
'file_count' => $this->fileCount,
|
||||
'ts_updated' => $this->timestampToDB($this->updated),
|
||||
'ts_checked' => $this->timestampToDB($this->checked),
|
||||
'name' => $this->name,
|
||||
'path' => $this->getPath(),
|
||||
);
|
||||
$format = array('%d', '%d', '%s', '%s', '%s', '%s');
|
||||
$table = $wpdb->prefix . 'shortpixel_folders';
|
||||
|
||||
$is_new = false;
|
||||
$result = false;
|
||||
|
||||
if ($this->in_db) // Update
|
||||
{
|
||||
$result = $wpdb->update($table, $data, array('id' => $this->id), $format);
|
||||
}
|
||||
else // Add new
|
||||
{
|
||||
// Fallback. In case some other process adds it. This happens with Nextgen.
|
||||
if (true === $this->loadFolderByPath($this->getPath()))
|
||||
{
|
||||
$result = $wpdb->update($table, $data, array('id' => $this->id), $format);
|
||||
}
|
||||
else
|
||||
{
|
||||
$data['ts_created'] = $this->timestampToDB(time());
|
||||
$this->id = $wpdb->insert($table, $data);
|
||||
if ($this->id !== false)
|
||||
{
|
||||
$is_new = true;
|
||||
$result = $this->id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// reloading because action can create a new DB-entry, which will not be reflected (in id )
|
||||
if ($is_new)
|
||||
{
|
||||
$this->loadFolderByPath($this->getPath());
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function delete()
|
||||
{
|
||||
$id = $this->id;
|
||||
if (! $this->in_db)
|
||||
{
|
||||
Log::addError('Trying to remove Folder without being in the database (in_db false) ' . $id, $this->getPath());
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$otherMedia = OtherMediaController::getInstance();
|
||||
|
||||
// Remove all files from this folder that are not optimized.
|
||||
$sql = "DELETE FROM " . $otherMedia->getMetaTable() . ' WHERE status <> 2 and folder_id = %d';
|
||||
$sql = $wpdb->prepare($sql, $this->id);
|
||||
$wpdb->query($sql);
|
||||
|
||||
// Check if there are any images left.
|
||||
$sql = 'SELECT count(id) FROM ' . $otherMedia->getMetaTable() . ' WHERE folder_id = %d';
|
||||
$sql = $wpdb->prepare($sql, $this->id);
|
||||
$numImages = $wpdb->get_var($sql);
|
||||
|
||||
if ($numImages > 0)
|
||||
{
|
||||
$sql = 'UPDATE ' . $otherMedia->getFolderTable() . ' SET status = -1 where id = %d';
|
||||
$sql = $wpdb->prepare($sql, $this->id);
|
||||
$result = $wpdb->query($sql);
|
||||
}
|
||||
else
|
||||
{
|
||||
$sql = 'DELETE FROM ' . $otherMedia->getFolderTable() . ' where id = %d';
|
||||
$sql = $wpdb->prepare($sql, $this->id);
|
||||
$result = $wpdb->query($sql);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function isRemoved()
|
||||
{
|
||||
if ($this->is_removed)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Updates the updated variable on folder to indicating when the last file change was made
|
||||
* @return boolean True if file were changed since last update, false if not
|
||||
*/
|
||||
public function updateFileContentChange()
|
||||
{
|
||||
if (! $this->exists() )
|
||||
return false;
|
||||
|
||||
$old_time = $this->updated;
|
||||
|
||||
$time = $this->recurseLastChangeFile();
|
||||
$this->updated = $time;
|
||||
$this->save();
|
||||
|
||||
if ($old_time !== $time)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** Crawls the folder and check for files that are newer than param time, or folder updated
|
||||
* Note - last update timestamp is not updated here, needs to be done separately.
|
||||
*/
|
||||
public function refreshFolder($force = false)
|
||||
{
|
||||
if ($force === false)
|
||||
{
|
||||
$time = $this->updated;
|
||||
}
|
||||
else
|
||||
{
|
||||
$time = 0; //force refresh of the whole.
|
||||
}
|
||||
|
||||
$stats = $this->getStats();
|
||||
$total_before = $stats['total'];
|
||||
|
||||
if (! $this->checkDirectory(true))
|
||||
{
|
||||
Log::addWarn('Refreshing directory, something wrong in checkDirectory ' . $this->getPath());
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->id <= 0)
|
||||
{
|
||||
Log::addWarn('FolderObj from database is not there, while folder seems ok ' . $this->getPath() );
|
||||
return false;
|
||||
}
|
||||
elseif (! $this->exists())
|
||||
{
|
||||
$message = sprintf(__('Folder %s does not exist! ', 'shortpixel-image-optimiser'), $this->getPath());
|
||||
$this->last_message = $message;
|
||||
Notice::addError( $message );
|
||||
return false;
|
||||
}
|
||||
elseif (! $this->is_writable())
|
||||
{
|
||||
$message = sprintf(__('Folder %s is not writeable. Please check permissions and try again.','shortpixel-image-optimiser'),$this->getPath());
|
||||
$this->last_message = $message;
|
||||
Notice::addWarning( $message );
|
||||
return false;
|
||||
}
|
||||
|
||||
$fs = \wpSPIO()->filesystem();
|
||||
$filter = ($time > 0) ? array('date_newer' => $time) : array();
|
||||
$filter['exclude_files'] = array('.webp', '.avif');
|
||||
$filter['include_files'] = ImageModel::PROCESSABLE_EXTENSIONS;
|
||||
|
||||
$files = $fs->getFilesRecursive($this, $filter);
|
||||
|
||||
\wpSPIO()->settings()->hasCustomFolders = time(); // note, check this against bulk when removing. Custom Media Bulk depends on having a setting.
|
||||
|
||||
$result = $this->addImages($files);
|
||||
|
||||
// Reset stat.
|
||||
unset(self::$stats[$this->id]);
|
||||
|
||||
$stats = $this->getStats();
|
||||
$this->fileCount = $stats['total'];
|
||||
|
||||
$this->checked = time();
|
||||
$this->save();
|
||||
|
||||
$stats['new'] = $stats['total'] - $total_before;
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
|
||||
/** Check if a directory is allowed. Directory can't be media library, outside of root, or already existing in the database
|
||||
* @param $silent If not allowed, don't generate notices.
|
||||
*
|
||||
*/
|
||||
public function checkDirectory($silent = false)
|
||||
{
|
||||
$fs = \wpSPIO()->filesystem();
|
||||
$rootDir = $fs->getWPFileBase();
|
||||
$backupDir = $fs->getDirectory(SHORTPIXEL_BACKUP_FOLDER);
|
||||
$otherMediaController = OtherMediaController::getInstance();
|
||||
|
||||
if (! $this->exists())
|
||||
{
|
||||
$message = __('Could not be added, directory not found: ' . $path ,'shortpixel-image-optimiser');
|
||||
$this->last_message = $message;
|
||||
|
||||
if (false === $silent)
|
||||
{
|
||||
Notice::addError($message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
elseif (! $this->isSubFolderOf($rootDir) && $this->getPath() != $rootDir->getPath() )
|
||||
{
|
||||
$message = sprintf(__('The %s folder cannot be processed as it\'s not inside the root path of your website (%s).','shortpixel-image-optimiser'),$this->getPath(), $rootDir->getPath());
|
||||
$this->last_message = $message;
|
||||
|
||||
if (false === $silent)
|
||||
{
|
||||
Notice::addError( $message );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
elseif($this->isSubFolderOf($backupDir) || $this->getPath() == $backupDir->getPath() )
|
||||
{
|
||||
$message = __('This folder contains the ShortPixel Backups. Please select a different folder.','shortpixel-image-optimiser');
|
||||
$this->last_message = $message;
|
||||
|
||||
if (false === $silent)
|
||||
{
|
||||
Notice::addError( $message );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
elseif( $otherMediaController->checkIfMediaLibrary($this) )
|
||||
{
|
||||
$message = __('This folder contains Media Library images. To optimize Media Library images please go to <a href="upload.php?mode=list">Media Library list view</a> or to <a href="upload.php?page=wp-short-pixel-bulk">ShortPixel Bulk page</a>.','shortpixel-image-optimiser');
|
||||
$this->last_message = $message;
|
||||
|
||||
if (false === $silent)
|
||||
{
|
||||
Notice::addError($message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
elseif (! $this->is_writable())
|
||||
{
|
||||
$message = sprintf(__('Folder %s is not writeable. Please check permissions and try again.','shortpixel-image-optimiser'),$this->getPath());
|
||||
$this->last_message = $message;
|
||||
|
||||
if (false === $silent)
|
||||
{
|
||||
Notice::addError( $message );
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
$folders = $otherMediaController->getAllFolders();
|
||||
|
||||
foreach($folders as $folder)
|
||||
{
|
||||
if ($this->isSubFolderOf($folder))
|
||||
{
|
||||
|
||||
if (false === $silent)
|
||||
{
|
||||
Notice::addError(sprintf(__('This folder is a subfolder of an already existing Other Media folder. Folder %s can not be added', 'shortpixel-image-optimiser'), $this->getPath() ));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
public function getFiles($args = array())
|
||||
{
|
||||
// Check if this directory if not forbidden.
|
||||
if (! $this->checkDirectory(true))
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
return parent::getFiles($args);
|
||||
}
|
||||
*/
|
||||
/* public function getSubDirectories()
|
||||
{
|
||||
$dirs = parent::getSubDirectories();
|
||||
$checked = array();
|
||||
foreach($dirs as $dir)
|
||||
{
|
||||
if ($dir->checkDirectory(false))
|
||||
{
|
||||
$checked[] = $dir;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log::addDebug('Illegal directory' . $dir->getPath());
|
||||
}
|
||||
}
|
||||
|
||||
return $checked;
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
private function recurseLastChangeFile($mtime = 0)
|
||||
{
|
||||
$ignore = array('.','..');
|
||||
|
||||
// Directories without read rights should not be checked at all.
|
||||
if (! $this->is_readable())
|
||||
return $mtime;
|
||||
|
||||
$path = $this->getPath();
|
||||
|
||||
$files = scandir($path);
|
||||
|
||||
// no files, nothing to update.
|
||||
if (! is_array($files))
|
||||
{
|
||||
return $mtime;
|
||||
}
|
||||
|
||||
$files = array_diff($files, $ignore);
|
||||
|
||||
$mtime = max($mtime, filemtime($path));
|
||||
|
||||
foreach($files as $file) {
|
||||
|
||||
|
||||
$filepath = $path . $file;
|
||||
|
||||
if (is_dir($filepath)) {
|
||||
$mtime = max($mtime, filemtime($filepath));
|
||||
$subDirObj = new DirectoryOtherMediaModel($filepath);
|
||||
$subdirtime = $subDirObj->recurseLastChangeFile($mtime);
|
||||
if ($subdirtime > $mtime)
|
||||
$mtime = $subdirtime;
|
||||
}
|
||||
}
|
||||
return $mtime;
|
||||
}
|
||||
|
||||
private function timestampToDB($timestamp)
|
||||
{
|
||||
if ($timestamp == 0) // when adding / or empty.
|
||||
$timestamp = time();
|
||||
return date("Y-m-d H:i:s", $timestamp);
|
||||
}
|
||||
|
||||
private function DBtoTimestamp($date)
|
||||
{
|
||||
if (is_null($date))
|
||||
{
|
||||
$timestamp = time();
|
||||
}
|
||||
else {
|
||||
$timestamp =strtotime($date);
|
||||
}
|
||||
return $timestamp;
|
||||
}
|
||||
|
||||
/** This function is called by OtherMediaController / RefreshFolders. Other scripts should not call it
|
||||
* @public
|
||||
* @param Array of CustomMediaImageModel stubs.
|
||||
*/
|
||||
public function addImages($files) {
|
||||
|
||||
global $wpdb;
|
||||
if ( apply_filters('shortpixel/othermedia/addfiles', true, $files, $this) === false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$values = array();
|
||||
|
||||
$optimizeControl = new OptimizeController();
|
||||
$otherMediaControl = OtherMediaController::getInstance();
|
||||
$activeFolders = $otherMediaControl->getActiveDirectoryIDS();
|
||||
|
||||
$fs = \wpSPIO()->filesystem();
|
||||
|
||||
|
||||
foreach($files as $fileObj)
|
||||
{
|
||||
// Note that load is set to false here.
|
||||
$imageObj = $fs->getCustomStub($fileObj->getFullPath(), false);
|
||||
|
||||
// image already exists
|
||||
if ($imageObj->get('in_db') == true)
|
||||
{
|
||||
// Load meta to make it check the folder_id.
|
||||
$imageObj->loadMeta();
|
||||
|
||||
// Check if folder id is something else. This might indicate removed or inactive folders.
|
||||
// If in inactive folder, move to current active.
|
||||
if ($imageObj->get('folder_id') !== $this->id)
|
||||
{
|
||||
if (! in_array($imageObj->get('folder_id'), $activeFolders) )
|
||||
{
|
||||
$imageObj->setFolderId($this->id);
|
||||
$imageObj->saveMeta();
|
||||
}
|
||||
}
|
||||
|
||||
// If in Db, but not optimized and autoprocess is on; add to queue for optimizing
|
||||
if (\wpSPIO()->env()->is_autoprocess && $imageObj->isProcessable())
|
||||
{
|
||||
$optimizeControl->addItemToQueue($imageObj);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
elseif ($imageObj->isProcessable()) // Check strict on Processable here.
|
||||
{
|
||||
$imageObj->setFolderId($this->id);
|
||||
$imageObj->saveMeta();
|
||||
|
||||
if (\wpSPIO()->env()->is_autoprocess)
|
||||
{
|
||||
$optimizeControl->addItemToQueue($imageObj);
|
||||
}
|
||||
}
|
||||
else {
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function loadFolderByPath($path)
|
||||
{
|
||||
//$folders = self::getFolders(array('path' => $path));
|
||||
global $wpdb;
|
||||
|
||||
$sql = 'SELECT * FROM ' . $wpdb->prefix . 'shortpixel_folders where path = %s ';
|
||||
$sql = $wpdb->prepare($sql, $path);
|
||||
|
||||
$folder = $wpdb->get_row($sql);
|
||||
if (! is_object($folder))
|
||||
return false;
|
||||
else
|
||||
{
|
||||
$this->loadFolder($folder);
|
||||
$this->in_db = true; // exists in database
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/** Loads from database into model, the extra data of this model. */
|
||||
private function loadFolder($folder)
|
||||
{
|
||||
// $class = get_class($folder);
|
||||
// Setters before action
|
||||
$this->id = $folder->id;
|
||||
|
||||
if ($this->id > 0)
|
||||
$this->in_db = true;
|
||||
|
||||
$this->updated = property_exists($folder,'ts_updated') ? $this->DBtoTimestamp($folder->ts_updated) : time();
|
||||
$this->created = property_exists($folder,'ts_created') ? $this->DBtoTimestamp($folder->ts_created) : time();
|
||||
$this->checked = property_exists($folder,'ts_checked') ? $this->DBtoTimestamp($folder->ts_checked) : time();
|
||||
$this->fileCount = property_exists($folder,'file_count') ? $folder->file_count : 0; // deprecated, do not rely on.
|
||||
|
||||
$this->status = $folder->status;
|
||||
|
||||
if (strlen($folder->name) == 0)
|
||||
$this->name = basename($folder->path);
|
||||
else
|
||||
$this->name = $folder->name;
|
||||
|
||||
do_action('shortpixel/othermedia/folder/load', $this->id, $this);
|
||||
|
||||
// Making conclusions after action.
|
||||
if ($this->status == -1)
|
||||
$this->is_removed = true;
|
||||
|
||||
if ($this->status == self::DIRECTORY_STATUS_NEXTGEN)
|
||||
{
|
||||
$this->is_nextgen = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,889 @@
|
||||
<?php
|
||||
namespace ShortPixel\Model\File;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
|
||||
use ShortPixel\Helper\UtilHelper as UtilHelper;
|
||||
|
||||
|
||||
/* FileModel class.
|
||||
*
|
||||
*
|
||||
* - Represents a -single- file.
|
||||
* - Can handle any type
|
||||
* - Usually controllers would use a collection of files
|
||||
* - Meant for all low-level file operations and checks.
|
||||
* - Every file can have a backup counterpart.
|
||||
*
|
||||
*/
|
||||
class FileModel extends \ShortPixel\Model
|
||||
{
|
||||
|
||||
// File info
|
||||
protected $fullpath = null;
|
||||
protected $rawfullpath = null;
|
||||
protected $filename = null; // filename + extension
|
||||
protected $filebase = null; // filename without extension
|
||||
protected $directory = null;
|
||||
protected $extension = null;
|
||||
protected $mime = null;
|
||||
protected $permissions = null;
|
||||
protected $filesize = null;
|
||||
|
||||
// File Status
|
||||
protected $exists = null;
|
||||
protected $is_writable = null;
|
||||
protected $is_directory_writable = null;
|
||||
protected $is_readable = null;
|
||||
protected $is_file = null;
|
||||
protected $is_virtual = false;
|
||||
protected $virtual_status = null;
|
||||
|
||||
protected $status;
|
||||
|
||||
protected $backupDirectory;
|
||||
|
||||
const FILE_OK = 1;
|
||||
const FILE_UNKNOWN_ERROR = 2;
|
||||
|
||||
public static $TRUSTED_MODE = false;
|
||||
|
||||
// Constants for is_virtual . Virtual Remote is truly a remote file, not writable from machine. Stateless means it looks remote, but it's a protocol-based filesystem remote or not - that will accept writes / is_writable. Stateless also mean performance issue since it can't be 'translated' to a local path. All communication happens over http wrapper, so check should be very limited.
|
||||
public static $VIRTUAL_REMOTE = 1;
|
||||
public static $VIRTUAL_STATELESS = 2;
|
||||
|
||||
/** Creates a file model object. FileModel files don't need to exist on FileSystem */
|
||||
public function __construct($path)
|
||||
{
|
||||
$this->rawfullpath = $path;
|
||||
|
||||
if (is_null($path))
|
||||
{
|
||||
Log::addWarn('FileModel: Loading null path! ');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (strlen($path) > 0)
|
||||
$path = trim($path);
|
||||
|
||||
$this->fullpath = $path;
|
||||
|
||||
$this->checkTrustedMode();
|
||||
|
||||
$fs = \wpSPIO()->filesystem();
|
||||
|
||||
if ($fs->pathIsUrl($path)) // Asap check for URL's to prevent remote wrappers from running.
|
||||
{
|
||||
$this->UrlToPath($path);
|
||||
}
|
||||
}
|
||||
|
||||
/* Get a string representation of file, the fullpath
|
||||
* Note - this might be risky, without processedpath, in cases.
|
||||
* @return String Full path processed or unprocessed.
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return (string) $this->fullpath;
|
||||
}
|
||||
|
||||
protected function setFileInfo()
|
||||
{
|
||||
$processed_path = $this->processPath($this->fullpath);
|
||||
if ($processed_path !== false)
|
||||
$this->fullpath = $processed_path; // set processed path if that went alright
|
||||
|
||||
|
||||
$info = $this->mb_pathinfo($this->fullpath);
|
||||
// Todo, maybe replace this with splFileINfo.
|
||||
if ($this->is_file()) // only set fileinfo when it's an actual file.
|
||||
{
|
||||
$this->filename = isset($info['basename']) ? $info['basename'] : null; // filename + extension
|
||||
$this->filebase = isset($info['filename']) ? $info['filename'] : null; // only filename
|
||||
$this->extension = isset($info['extension']) ? strtolower($info['extension']) : null; // only (last) extension
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** Call when file status changed, so writable / readable / exists are not reliable anymore */
|
||||
public function resetStatus()
|
||||
{
|
||||
$this->is_writable = null;
|
||||
$this->is_directory_writable = null;
|
||||
$this->is_readable = null;
|
||||
$this->is_file = null;
|
||||
$this->exists = null;
|
||||
$this->is_virtual = null;
|
||||
$this->filesize = null;
|
||||
$this->permissions = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $forceCheck Forces a filesystem check instead of using cached. Use very sparingly. Implemented for retina on trusted mode.
|
||||
*/
|
||||
public function exists($forceCheck = false)
|
||||
{
|
||||
if (true === $forceCheck || is_null($this->exists))
|
||||
{
|
||||
if (true === $this->fileIsRestricted($this->fullpath))
|
||||
{
|
||||
$this->exists = false;
|
||||
}
|
||||
else {
|
||||
$this->exists = (@file_exists($this->fullpath) && is_file($this->fullpath));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$this->exists = apply_filters('shortpixel_image_exists', $this->exists, $this->fullpath, $this); //legacy
|
||||
$this->exists = apply_filters('shortpixel/file/exists', $this->exists, $this->fullpath, $this);
|
||||
return $this->exists;
|
||||
}
|
||||
|
||||
public function is_writable()
|
||||
{
|
||||
// Return when already asked / Stateless might set this
|
||||
if (! is_null($this->is_writable))
|
||||
{
|
||||
return $this->is_writable;
|
||||
}
|
||||
elseif ($this->is_virtual())
|
||||
{
|
||||
$this->is_writable = false; // can't write to remote files
|
||||
}
|
||||
elseif (is_null($this->is_writable))
|
||||
{
|
||||
if ($this->exists())
|
||||
{
|
||||
$this->is_writable = @is_writable($this->fullpath);
|
||||
}
|
||||
else // quite expensive check to see if file is writable.
|
||||
{
|
||||
$res = $this->create();
|
||||
$this->delete();
|
||||
$this->is_writable = $res;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $this->is_writable;
|
||||
}
|
||||
|
||||
public function is_directory_writable()
|
||||
{
|
||||
// Return when already asked / Stateless might set this
|
||||
if (! is_null($this->is_directory_writable))
|
||||
{
|
||||
return $this->is_directory_writable;
|
||||
}
|
||||
elseif ($this->is_virtual())
|
||||
{
|
||||
$this->is_directory_writable = false; // can't write to remote files
|
||||
}
|
||||
elseif (is_null($this->is_directory_writable))
|
||||
{
|
||||
$directory = $this->getFileDir();
|
||||
if (is_object($directory) && $directory->exists())
|
||||
{
|
||||
$this->is_directory_writable = $directory->is_writable();
|
||||
}
|
||||
else {
|
||||
$this->is_directory_writable = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $this->is_directory_writable;
|
||||
}
|
||||
|
||||
public function is_readable()
|
||||
{
|
||||
if (is_null($this->is_readable))
|
||||
$this->is_readable = @is_readable($this->fullpath);
|
||||
|
||||
return $this->is_readable;
|
||||
}
|
||||
|
||||
// A file is virtual when the file is remote with URL and no local alternative is present.
|
||||
public function is_virtual()
|
||||
{
|
||||
if ( is_null($this->is_virtual))
|
||||
$this->is_virtual = false; // return bool
|
||||
return $this->is_virtual;
|
||||
}
|
||||
|
||||
/* Function checks if path is actually a file. This can be used to check possible confusion if a directory path is given to filemodel */
|
||||
public function is_file()
|
||||
{
|
||||
if ($this->is_virtual()) // don't look further when virtual
|
||||
{
|
||||
$this->is_file = true;
|
||||
return $this->is_file;
|
||||
}
|
||||
elseif (is_null($this->is_file))
|
||||
{
|
||||
if ($this->exists())
|
||||
{
|
||||
if (basename($this->fullpath) == '..' || basename($this->fullpath) == '.')
|
||||
$this->is_file = false;
|
||||
else
|
||||
$this->is_file = is_file($this->fullpath);
|
||||
}
|
||||
else // file can not exist, but still have a valid filepath format. In that case, if file should return true.
|
||||
{
|
||||
|
||||
/* if file does not exist on disk, anything can become a file ( with/ without extension, etc). Meaning everything non-existing is a potential file ( or directory ) until created. */
|
||||
|
||||
if (basename($this->fullpath) == '..' || basename($this->fullpath) == '.') // don't see this as file.
|
||||
{
|
||||
$this->is_file = false;
|
||||
}
|
||||
else if (! file_exists($this->fullpath) && ! is_dir($this->fullpath))
|
||||
{
|
||||
$this->is_file = true;
|
||||
}
|
||||
else //if (! is_file($this->fullpath)) // can be a non-existing directory. /
|
||||
{
|
||||
$this->is_file = false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return $this->is_file;
|
||||
}
|
||||
|
||||
public function getModified()
|
||||
{
|
||||
return filemtime($this->fullpath);
|
||||
}
|
||||
|
||||
public function hasBackup()
|
||||
{
|
||||
$directory = $this->getBackupDirectory();
|
||||
if (! $directory)
|
||||
return false;
|
||||
|
||||
$backupFile = $directory . $this->getBackupFileName();
|
||||
|
||||
if (file_exists($backupFile) && ! is_dir($backupFile) )
|
||||
return true;
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Tries to retrieve an *existing* BackupFile. Returns false if not present.
|
||||
* This file might not be writable.
|
||||
* To get writable directory reference to backup, use FileSystemController
|
||||
*/
|
||||
public function getBackupFile()
|
||||
{
|
||||
if ($this->hasBackup())
|
||||
return new FileModel($this->getBackupDirectory() . $this->getBackupFileName() );
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Function returns the filename for the backup. This is an own function so it's possible to manipulate backup file name if needed, i.e. conversion or enumeration */
|
||||
public function getBackupFileName()
|
||||
{
|
||||
return $this->getFileName();
|
||||
}
|
||||
|
||||
/** Returns the Directory Model this file resides in
|
||||
*
|
||||
* @return DirectoryModel Directorymodel Object
|
||||
*/
|
||||
public function getFileDir()
|
||||
{
|
||||
$fullpath = $this->getFullPath(); // triggers a file lookup if needed.
|
||||
// create this only when needed.
|
||||
if (is_null($this->directory) && strlen($fullpath) > 0)
|
||||
{
|
||||
// Feed to full path to DirectoryModel since it checks if input is file, or dir. Using dirname here would cause errors when fullpath is already just a dirpath ( faulty input )
|
||||
$this->directory = new DirectoryModel($fullpath);
|
||||
}
|
||||
|
||||
return $this->directory;
|
||||
}
|
||||
|
||||
public function getFileSize()
|
||||
{
|
||||
if (! is_null($this->filesize))
|
||||
{
|
||||
return $this->filesize;
|
||||
}
|
||||
elseif ($this->exists() && false === $this->is_virtual() )
|
||||
{
|
||||
$this->filesize = filesize($this->fullpath);
|
||||
return $this->filesize;
|
||||
}
|
||||
elseif (true === $this->is_virtual())
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Creates an empty file
|
||||
public function create()
|
||||
{
|
||||
if (! $this->exists() )
|
||||
{
|
||||
$fileDir = $this->getFileDir();
|
||||
|
||||
if (! is_null($fileDir) && $fileDir->exists())
|
||||
{
|
||||
$res = @touch($this->fullpath);
|
||||
$this->exists = $res;
|
||||
return $res;
|
||||
}
|
||||
}
|
||||
else
|
||||
Log::addWarn('Could not create/write file: ' . $this->fullpath);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function append($message)
|
||||
{
|
||||
if (! $this->exists() )
|
||||
$this->create();
|
||||
|
||||
if (! $this->is_writable() )
|
||||
{
|
||||
Log::addWarn('File append failed on ' . $this->getFullPath() . ' - not writable');
|
||||
return false;
|
||||
}
|
||||
$handle = fopen($this->getFullPath(), 'a');
|
||||
fwrite($handle, $message);
|
||||
fclose($handle);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/** Copy a file to somewhere
|
||||
*
|
||||
* @param $destination String Full Path to new file.
|
||||
*/
|
||||
public function copy(FileModel $destination)
|
||||
{
|
||||
$sourcePath = $this->getFullPath();
|
||||
$destinationPath = $destination->getFullPath();
|
||||
Log::addDebug("Copy from $sourcePath to $destinationPath ");
|
||||
|
||||
if (! strlen($sourcePath) > 0 || ! strlen($destinationPath) > 0)
|
||||
{
|
||||
Log::addWarn('Attempted Copy on Empty Path', array($sourcePath, $destinationPath));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! $this->exists())
|
||||
{
|
||||
Log::addWarn('Tried to copy non-existing file - ' . $sourcePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
$is_new = ($destination->exists()) ? false : true;
|
||||
$status = @copy($sourcePath, $destinationPath);
|
||||
|
||||
if (! $status)
|
||||
{
|
||||
Log::addWarn('Could not copy file ' . $sourcePath . ' to' . $destinationPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
$destination->resetStatus();
|
||||
$destination->setFileInfo(); // refresh info.
|
||||
}
|
||||
//
|
||||
do_action('shortpixel/filesystem/addfile', array($destinationPath, $destination, $this, $is_new));
|
||||
return $status;
|
||||
}
|
||||
|
||||
/** Move a file to somewhere
|
||||
* This uses copy and delete functions and will fail if any of those fail.
|
||||
* @param $destination String Full Path to new file.
|
||||
*/
|
||||
public function move(FileModel $destination)
|
||||
{
|
||||
$result = false;
|
||||
if ($this->copy($destination))
|
||||
{
|
||||
$result = $this->delete();
|
||||
if ($result == false)
|
||||
{
|
||||
Log::addError('Move can\'t remove file ' . $this->getFullPath());
|
||||
}
|
||||
|
||||
$this->resetStatus();
|
||||
$destination->resetStatus();
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/** Deletes current file
|
||||
* This uses the WP function since it has a filter that might be useful
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
if ($this->exists())
|
||||
{
|
||||
\wp_delete_file($this->fullpath); // delete file hook via wp_delete_file
|
||||
}
|
||||
else
|
||||
{
|
||||
Log::addWarn('Trying to remove non-existing file: ' . $this->getFullPath());
|
||||
}
|
||||
|
||||
if (! file_exists($this->fullpath))
|
||||
{
|
||||
$this->resetStatus();
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
$writable = ($this->is_writable()) ? 'true' : 'false';
|
||||
Log::addWarn('File seems not removed - ' . $this->getFullPath() . ' (writable:' . $writable . ')');
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function getContents()
|
||||
{
|
||||
return file_get_contents($this->getFullPath());
|
||||
}
|
||||
|
||||
public function getFullPath()
|
||||
{
|
||||
// filename here since fullpath is set unchecked in constructor, but might be a different take
|
||||
if (is_null($this->filename))
|
||||
{
|
||||
$this->setFileInfo();
|
||||
}
|
||||
|
||||
return $this->fullpath;
|
||||
}
|
||||
|
||||
// Testing this. Principle is that when the plugin is absolutely sure this is a file, not something remote, not something non-existing, get the fullpath without any check.
|
||||
// This function should *only* be used when processing mega amounts of files while not doing optimization or any processing.
|
||||
// So far, testing use for file Filter */
|
||||
public function getRawFullPath()
|
||||
{
|
||||
return $this->rawfullpath;
|
||||
}
|
||||
|
||||
public function getFileName()
|
||||
{
|
||||
if (is_null($this->filename))
|
||||
$this->setFileInfo();
|
||||
|
||||
return $this->filename;
|
||||
}
|
||||
|
||||
public function getFileBase()
|
||||
{
|
||||
if (is_null($this->filebase))
|
||||
$this->setFileInfo();
|
||||
|
||||
return $this->filebase;
|
||||
}
|
||||
|
||||
|
||||
public function getExtension()
|
||||
{
|
||||
if (is_null($this->extension))
|
||||
$this->setFileInfo();
|
||||
|
||||
return $this->extension;
|
||||
}
|
||||
|
||||
public function getMime()
|
||||
{
|
||||
if (is_null($this->mime))
|
||||
$this->setFileInfo();
|
||||
|
||||
if ($this->exists() && ! $this->is_virtual() )
|
||||
{
|
||||
$this->mime = wp_get_image_mime($this->fullpath);
|
||||
if (false === $this->mime)
|
||||
{
|
||||
$image_data = wp_check_filetype_and_ext($this->getFullPath(), $this->getFileName());
|
||||
if (is_array($image_data) && isset($image_data['type']) && strlen($image_data['type']) > 0)
|
||||
{
|
||||
$this->mime = $image_data['type'];
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
else
|
||||
$this->mime = false;
|
||||
|
||||
return $this->mime;
|
||||
}
|
||||
/* Util function to get location of backup Directory.
|
||||
* @param Create - If true will try to create directory if it doesn't exist.
|
||||
* @return Boolean | DirectModel Returns false if directory is not properly set, otherwhise with a new directoryModel
|
||||
*/
|
||||
protected function getBackupDirectory($create = false)
|
||||
{
|
||||
|
||||
if (is_null($this->getFileDir()))
|
||||
{
|
||||
Log::addWarn('Could not establish FileDir ' . $this->fullpath);
|
||||
return false;
|
||||
}
|
||||
$fs = \wpSPIO()->filesystem();
|
||||
|
||||
if (is_null($this->backupDirectory))
|
||||
{
|
||||
$directory = $fs->getBackupDirectory($this, $create);
|
||||
|
||||
if ($directory === false || ! $directory->exists()) // check if exists. FileModel should not attempt to create.
|
||||
{
|
||||
//Log::addWarn('Backup Directory not existing ' . $directory-);
|
||||
return false;
|
||||
}
|
||||
elseif ($directory !== false)
|
||||
{
|
||||
$this->backupDirectory = $directory;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->backupDirectory;
|
||||
}
|
||||
|
||||
/* Internal function to check if path is a real path
|
||||
* - Test for URL's based on http / https
|
||||
* - Test if given path is absolute, from the filesystem root.
|
||||
* @param $path String The file path
|
||||
* @param String The Fixed filepath.
|
||||
*/
|
||||
protected function processPath($path)
|
||||
{
|
||||
$original_path = $path;
|
||||
|
||||
$fs = \wpSPIO()->filesystem();
|
||||
|
||||
if ($fs->pathIsUrl($path))
|
||||
{
|
||||
$path = $this->UrlToPath($path);
|
||||
}
|
||||
|
||||
if ($path === false) // don't process further
|
||||
return false;
|
||||
|
||||
$path = UtilHelper::spNormalizePath($path);
|
||||
$abspath = $fs->getWPAbsPath();
|
||||
|
||||
|
||||
// Prevent file operation below if trusted.
|
||||
if (true === self::$TRUSTED_MODE)
|
||||
{
|
||||
return $path;
|
||||
}
|
||||
|
||||
// Check if some openbasedir is active.
|
||||
if (true === $this->fileIsRestricted($path))
|
||||
{
|
||||
$path = $this->relativeToFullPath($path);
|
||||
}
|
||||
|
||||
if ( is_file($path) && ! is_dir($path) ) // if path and file exist, all should be okish.
|
||||
{
|
||||
return $path;
|
||||
}
|
||||
// If attempted file does not exist, but the file is in a dir that exists, that is good enough.
|
||||
elseif ( ! is_dir($path) && is_dir(dirname($path)) )
|
||||
{
|
||||
return $path;
|
||||
}
|
||||
// If path is not in the abspath, it might be relative.
|
||||
elseif (strpos($path, $abspath->getPath()) === false)
|
||||
{
|
||||
// if path does not contain basepath.
|
||||
//$uploadDir = $fs->getWPUploadBase();
|
||||
//$abspath = $fs->getWPAbsPath();
|
||||
|
||||
$path = $this->relativeToFullPath($path);
|
||||
}
|
||||
$path = apply_filters('shortpixel/filesystem/processFilePath', $path, $original_path);
|
||||
/* This needs some check here on malformed path's, but can't be test for existing since that's not a requirement.
|
||||
if (file_exists($path) === false) // failed to process path to something workable.
|
||||
{
|
||||
// Log::addInfo('Failed to process path', array($path));
|
||||
$path = false;
|
||||
} */
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
protected function checkTrustedMode()
|
||||
{
|
||||
// When in trusted mode prevent filesystem checks as much as possible.
|
||||
if (true === self::$TRUSTED_MODE)
|
||||
{
|
||||
|
||||
// At this point file info might not be loaded, because it goes w/ construct -> processpath -> urlToPath etc on virtual files. And called via getFileInfo. Using any of the file info functions can trigger a loop.
|
||||
if (is_null($this->extension))
|
||||
{
|
||||
$extension = pathinfo($this->fullpath, PATHINFO_EXTENSION);
|
||||
}
|
||||
else {
|
||||
$extension = $this->getExtension();
|
||||
}
|
||||
|
||||
$this->exists = true;
|
||||
$this->is_writable = true;
|
||||
$this->is_directory_writable = true;
|
||||
$this->is_readable = true;
|
||||
$this->is_file = true;
|
||||
// Set mime to prevent lookup in IsImage
|
||||
$this->mime = 'image/' . $extension;
|
||||
|
||||
if (is_null($this->filesize))
|
||||
{
|
||||
$this->filesize = 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** Check if path is allowed within openbasedir restrictions. This is an attempt to limit notices in file funtions if so. Most likely the path will be relative in that case.
|
||||
* @param String Path as String
|
||||
*/
|
||||
private function fileIsRestricted($path)
|
||||
{
|
||||
$basedir = ini_get('open_basedir');
|
||||
|
||||
if (false === $basedir || strlen($basedir) == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$restricted = true;
|
||||
$basedirs = preg_split('/:|;/i', $basedir);
|
||||
|
||||
foreach($basedirs as $basepath)
|
||||
{
|
||||
if (strpos($path, $basepath) !== false)
|
||||
{
|
||||
$restricted = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $restricted;
|
||||
}
|
||||
|
||||
/** Resolve an URL to a local path
|
||||
* This partially comes from WordPress functions attempting the same
|
||||
* @param String $url The URL to resolve
|
||||
* @return String/Boolean - False is this seems an external domain, otherwise resolved path.
|
||||
*/
|
||||
private function UrlToPath($url)
|
||||
{
|
||||
|
||||
// If files is present, high chance that it's WPMU old style, which doesn't have in home_url the /files/ needed to properly replace and get the filepath . It would result in a /files/files path which is incorrect.
|
||||
if (strpos($url, '/files/') !== false)
|
||||
{
|
||||
$uploadDir = wp_upload_dir();
|
||||
$site_url = str_replace(array('http:', 'https:'), '', $uploadDir['baseurl']);
|
||||
}
|
||||
else {
|
||||
$site_url = str_replace('http:', '', home_url('', 'http'));
|
||||
}
|
||||
|
||||
$url = str_replace(array('http:', 'https:'), '', $url);
|
||||
$fs = \wpSPIO()->filesystem();
|
||||
|
||||
// The site URL domain is included in the URL string
|
||||
if (strpos($url, $site_url) !== false)
|
||||
{
|
||||
// try to replace URL for Path
|
||||
$abspath = \wpSPIO()->filesystem()->getWPAbsPath();
|
||||
$path = str_replace($site_url, rtrim($abspath->getPath(),'/'), $url);
|
||||
|
||||
if (! $fs->pathIsUrl($path)) // test again.
|
||||
{
|
||||
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
|
||||
$this->is_virtual = true;
|
||||
|
||||
/* This filter checks if some supplier will be able to handle the file when needed.
|
||||
* Use translate filter to correct filepath when needed.
|
||||
* Return could be true, or fileModel virtual constant
|
||||
*/
|
||||
$result = apply_filters('shortpixel/image/urltopath', false, $url);
|
||||
|
||||
if ($result === false)
|
||||
{
|
||||
$this->exists = false;
|
||||
$this->is_readable = false;
|
||||
$this->is_file = false;
|
||||
}
|
||||
else {
|
||||
$this->exists = true;
|
||||
$this->is_readable = true;
|
||||
$this->is_file = true;
|
||||
}
|
||||
|
||||
// If return is a stateless server, assume that it's writable and all that.
|
||||
if ($result === self::$VIRTUAL_STATELESS)
|
||||
{
|
||||
$this->is_writable = true;
|
||||
$this->is_directory_writable = true;
|
||||
$this->virtual_status = self::$VIRTUAL_STATELESS;
|
||||
}
|
||||
elseif ($result === self::$VIRTUAL_REMOTE)
|
||||
{
|
||||
$this->virtual_status = self::$VIRTUAL_REMOTE;
|
||||
}
|
||||
|
||||
return false; // seems URL from other server, use virtual mode.
|
||||
}
|
||||
|
||||
/** Tries to find the full path for a perceived relative path.
|
||||
*
|
||||
* Relative path is detected on basis of WordPress ABSPATH. If this doesn't appear in the file path, it might be a relative path.
|
||||
* Function checks for expections on this rule ( tmp path ) and returns modified - or not - path.
|
||||
* @param $path The path for the file_exists
|
||||
* @returns String The updated path, if that was possible.
|
||||
*/
|
||||
private function relativeToFullPath($path)
|
||||
{
|
||||
$originalPath = $path; // for safe-keeping
|
||||
|
||||
// A file with no path, can never be created to a fullpath.
|
||||
if (strlen($path) == 0)
|
||||
return $path;
|
||||
|
||||
// if the file plainly exists, it's usable /**
|
||||
if (false === $this->fileIsRestricted($path) && file_exists($path))
|
||||
{
|
||||
return $path;
|
||||
}
|
||||
|
||||
// Test if our 'relative' path is not a path to /tmp directory.
|
||||
|
||||
// This ini value might not exist.
|
||||
$tempdirini = ini_get('upload_tmp_dir');
|
||||
if ( (strlen($tempdirini) > 0) && strpos($path, $tempdirini) !== false)
|
||||
return $path;
|
||||
|
||||
$tempdir = sys_get_temp_dir();
|
||||
if ( (strlen($tempdir) > 0) && strpos($path, $tempdir) !== false)
|
||||
return $path;
|
||||
|
||||
// Path contains upload basedir. This happens when upload dir is outside of usual WP.
|
||||
$fs = \wpSPIO()->filesystem();
|
||||
$uploadDir = $fs->getWPUploadBase();
|
||||
$abspath = $fs->getWPAbsPath();
|
||||
|
||||
if (strpos($path, $uploadDir->getPath()) !== false) // If upload Dir is feature in path, consider it ok.
|
||||
{
|
||||
return $path;
|
||||
}
|
||||
elseif (file_exists($abspath->getPath() . $path)) // If upload dir is abspath plus return path. Exceptions.
|
||||
{
|
||||
return $abspath->getPath() . $path;
|
||||
}
|
||||
elseif(file_exists($uploadDir->getPath() . $path)) // This happens when upload_dir is not properly prepended in get_attachment_file due to WP errors
|
||||
{
|
||||
return $uploadDir->getPath() . $path;
|
||||
}
|
||||
|
||||
// this is probably a bit of a sharp corner to take.
|
||||
// if path starts with / remove it due to trailingslashing ABSPATH
|
||||
$path = ltrim($path, '/');
|
||||
$fullpath = $abspath->getPath() . $path;
|
||||
|
||||
// We can't test for file_exists here, since file_model allows non-existing files.
|
||||
// Test if directory exists, perhaps. Otherwise we are in for a failure anyhow.
|
||||
//if (is_dir(dirname($fullpath)))
|
||||
return $fullpath;
|
||||
//else
|
||||
// return $originalPath;
|
||||
}
|
||||
|
||||
public function getPermissions()
|
||||
{
|
||||
if (is_null($this->permissions))
|
||||
$this->permissions = fileperms($this->getFullPath()) & 0777;
|
||||
|
||||
return $this->permissions;
|
||||
}
|
||||
|
||||
// @tozo Lazy IMplementation / copy, should be brought in line w/ other attributes.
|
||||
public function setPermissions($permissions)
|
||||
{
|
||||
@chmod($this->fullpath, $permissions);
|
||||
}
|
||||
|
||||
|
||||
/** Fix for multibyte pathnames and pathinfo which doesn't take into regard the locale.
|
||||
* This snippet taken from PHPMailer.
|
||||
*/
|
||||
private function mb_pathinfo($path, $options = null)
|
||||
{
|
||||
$ret = ['dirname' => '', 'basename' => '', 'extension' => '', 'filename' => ''];
|
||||
$pathinfo = [];
|
||||
if (preg_match('#^(.*?)[\\\\/]*(([^/\\\\]*?)(\.([^.\\\\/]+?)|))[\\\\/.]*$#m', $path, $pathinfo)) {
|
||||
if (array_key_exists(1, $pathinfo)) {
|
||||
$ret['dirname'] = $pathinfo[1];
|
||||
}
|
||||
if (array_key_exists(2, $pathinfo)) {
|
||||
$ret['basename'] = $pathinfo[2];
|
||||
}
|
||||
if (array_key_exists(5, $pathinfo)) {
|
||||
$ret['extension'] = $pathinfo[5];
|
||||
}
|
||||
if (array_key_exists(3, $pathinfo)) {
|
||||
$ret['filename'] = $pathinfo[3];
|
||||
}
|
||||
}
|
||||
switch ($options) {
|
||||
case PATHINFO_DIRNAME:
|
||||
case 'dirname':
|
||||
return $ret['dirname'];
|
||||
case PATHINFO_BASENAME:
|
||||
case 'basename':
|
||||
return $ret['basename'];
|
||||
case PATHINFO_EXTENSION:
|
||||
case 'extension':
|
||||
return $ret['extension'];
|
||||
case PATHINFO_FILENAME:
|
||||
case 'filename':
|
||||
return $ret['filename'];
|
||||
default:
|
||||
return $ret;
|
||||
}
|
||||
}
|
||||
|
||||
public function __debuginfo()
|
||||
{
|
||||
return [
|
||||
'fullpath' => $this->fullpath,
|
||||
'filename' => $this->filename,
|
||||
'filebase' => $this->filebase,
|
||||
'exists' => $this->exists,
|
||||
'is_writable' => $this->is_writable,
|
||||
'is_readable' => $this->is_readable,
|
||||
'is_virtual' => $this->is_virtual,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
} // FileModel Class
|
@ -0,0 +1,349 @@
|
||||
<?php
|
||||
namespace ShortPixel\Model;
|
||||
|
||||
use ShortPixel\Model\Image\ImageModel as ImageModel;
|
||||
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
|
||||
|
||||
|
||||
class FrontImage
|
||||
{
|
||||
protected $raw;
|
||||
protected $image_loaded = false;
|
||||
protected $is_parsable = false;
|
||||
protected $imageBase; // directory path of this image.
|
||||
|
||||
protected $id; // HTML ID of image
|
||||
protected $alt;
|
||||
protected $src; // original src of image
|
||||
protected $srcset; // orginal srcset of image
|
||||
protected $class;
|
||||
protected $width;
|
||||
protected $height;
|
||||
protected $style;
|
||||
protected $sizes;
|
||||
|
||||
// Array of all other attributes.
|
||||
protected $attributes;
|
||||
|
||||
// Parsed items of src /srcset / sizes
|
||||
protected $dataTags = array();
|
||||
|
||||
public function __construct($raw_html)
|
||||
{
|
||||
$this->raw = $raw_html;
|
||||
$this->loadImageDom();
|
||||
}
|
||||
|
||||
public function loadImageDom()
|
||||
{
|
||||
if (function_exists("mb_convert_encoding")) {
|
||||
$this->raw = mb_encode_numericentity($this->raw, [0x80, 0x10FFFF, 0, ~0], 'UTF-8');
|
||||
}
|
||||
|
||||
$dom = new \DOMDocument();
|
||||
libxml_use_internal_errors(true); // disable error emit from libxml
|
||||
|
||||
$result = $dom->loadHTML($this->raw, LIBXML_NOWARNING);
|
||||
|
||||
// HTML failed loading
|
||||
if (false === $result)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$image = $dom->getElementsByTagName('img')->item(0);
|
||||
$attributes = array();
|
||||
|
||||
/* This can happen with mismatches, or extremely malformed HTML.
|
||||
In customer case, a javascript that did for (i<imgDefer) --- </script> */
|
||||
if (! is_object($image))
|
||||
{
|
||||
$this->is_parsable = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($image->attributes as $attr) {
|
||||
// Skip is no value
|
||||
if (strlen($attr->nodeValue) == 0)
|
||||
continue;
|
||||
|
||||
if (property_exists($this, $attr->nodeName))
|
||||
{
|
||||
$this->{$attr->nodeName} = $attr->nodeValue;
|
||||
}
|
||||
|
||||
$this->attributes[$attr->nodeName] = $attr->nodeValue;
|
||||
}
|
||||
|
||||
|
||||
// Parse the directory path and other sources
|
||||
$result = $this->setupSources();
|
||||
|
||||
|
||||
if (true === $result)
|
||||
$this->image_loaded = true;
|
||||
}
|
||||
|
||||
public function hasBackground()
|
||||
{
|
||||
if (! is_null($this->style) && strpos($this->style, 'background') !== false)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function hasPreventClasses()
|
||||
{
|
||||
// no class, no prevent.
|
||||
if (is_null($this->class))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$preventArray = apply_filters('shortpixel/front/preventclasses', array('sp-no-webp', 'rev-sildebg') );
|
||||
|
||||
foreach($preventArray as $classname)
|
||||
{
|
||||
if (false !== strpos($this->class, $classname) )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function hasSource()
|
||||
{
|
||||
if (is_null($this->src) && is_null($this->srcset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isParseable()
|
||||
{
|
||||
if (
|
||||
false === $this->hasPreventClasses() &&
|
||||
false === $this->hasBackground() &&
|
||||
true === $this->hasSource() &&
|
||||
true === $this->image_loaded
|
||||
)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getImageData()
|
||||
{
|
||||
if (! is_null($this->srcset))
|
||||
{
|
||||
$data = $this->getLazyData('srcset');
|
||||
$data = explode(',', $data); // srcset is multiple images, split.
|
||||
}
|
||||
else {
|
||||
$data = $this->getLazyData('src');
|
||||
$data = array($data); // single item, wrap in array
|
||||
}
|
||||
|
||||
$this->getLazyData('sizes'); // sets the sizes.
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function getImageBase()
|
||||
{
|
||||
if (! is_null($this->imageBase))
|
||||
return $this->imageBase->getPath();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function parseReplacement($args)
|
||||
{
|
||||
if (is_null($this->class))
|
||||
{
|
||||
$this->class = '';
|
||||
}
|
||||
|
||||
$this->class .= ' sp-no-webp';
|
||||
|
||||
$output = "<picture>";
|
||||
|
||||
if (isset($args['avif']) && count($args['avif']) > 0)
|
||||
{
|
||||
$output .= $this->buildSource($args['avif'], 'avif');
|
||||
}
|
||||
|
||||
if (isset($args['webp']) && count($args['webp']) > 0)
|
||||
{
|
||||
$output .= $this->buildSource($args['webp'], 'webp');
|
||||
}
|
||||
|
||||
$output .= $this->buildImage();
|
||||
|
||||
$output .= "</picture>";
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
|
||||
protected function setupSources()
|
||||
{
|
||||
$src = null;
|
||||
|
||||
if (! is_null($this->src))
|
||||
{
|
||||
$src = $this->src;
|
||||
}
|
||||
elseif (! is_null($this->srcset))
|
||||
{
|
||||
$parts = preg_split('/\s+/', trim($this->srcset));
|
||||
$image_url = $parts[0];
|
||||
$src = $image_url;
|
||||
}
|
||||
|
||||
if (is_null($src))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filter out extension that are not for us.
|
||||
if (false === $this->checkExtensionConvertable($src))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
$fs = \wpSPIO()->filesystem();
|
||||
$fileObj = $fs->getFile($src);
|
||||
$fileDir = $fileObj->getFileDir();
|
||||
$this->imageBase = $fileObj->getFileDir();
|
||||
|
||||
return true;
|
||||
// If (! is_hnull $srcset)
|
||||
// Get first item from srcset ( remove the size ? , then feed it to FS, get directory from it.
|
||||
}
|
||||
|
||||
/*** Check if the extension is something we want to check
|
||||
* @param String The URL source of the image.
|
||||
**/
|
||||
private function checkExtensionConvertable($source)
|
||||
{
|
||||
$extension = substr($source, strrpos($source, '.') + 1);
|
||||
if (in_array($extension, ImageModel::PROCESSABLE_EXTENSIONS))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
protected function buildSource($sources, $fileFormat)
|
||||
{
|
||||
|
||||
$prefix = (isset($this->dataTags['srcset'])) ? $this->dataTags['srcset'] : $this->dataTags['src'];
|
||||
$srcset = implode(',', $sources);
|
||||
|
||||
$sizeOutput = '';
|
||||
if (! is_null($this->sizes))
|
||||
{
|
||||
$sizeOutput = $this->dataTags['sizes'] . 'sizes="' . $this->sizes . '"';
|
||||
}
|
||||
|
||||
$output = '<source ' . $prefix . 'srcset="' . $srcset . '" ' . $sizeOutput . ' type="image/' . $fileFormat . '">';
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
protected function buildImage()
|
||||
{
|
||||
$src = $this->src;
|
||||
$output = '<img src="' . $src . '" ';
|
||||
|
||||
// Get this from set attributes on class.
|
||||
$attrs = array('id', 'height', 'width', 'srcset', 'sizes', 'class');
|
||||
foreach($attrs as $attr)
|
||||
{
|
||||
if (! is_null($this->{$attr}))
|
||||
{
|
||||
$output .= $attr . '="' . $this->{$attr} . '" ';
|
||||
}
|
||||
}
|
||||
|
||||
// Always output alt tag, because it's important to screen readers and otherwise.
|
||||
$output .= 'alt="' . $this->alt . '" ';
|
||||
|
||||
// Left over attributes that should be harmless, ie extra image data or other custom tags.
|
||||
$leftAttrs = $this->getImageAttributes();
|
||||
foreach($leftAttrs as $name => $value)
|
||||
{
|
||||
$output .= $name . '="' . $value . '" ';
|
||||
}
|
||||
|
||||
$output .= ' > '; // ending image.
|
||||
|
||||
return $output;
|
||||
|
||||
}
|
||||
|
||||
protected function getImageAttributes()
|
||||
{
|
||||
|
||||
$dontuse = array(
|
||||
'src', 'data-src', 'data-lazy-src', 'srcset', 'sizes'
|
||||
|
||||
);
|
||||
$dontuse = array_merge($dontuse, array('id', 'alt', 'height', 'width', 'srcset', 'sizes', 'class'));
|
||||
|
||||
$attributes = $this->attributes;
|
||||
|
||||
$leftAttrs = array();
|
||||
foreach($attributes as $name => $value)
|
||||
{
|
||||
if (! in_array($name, $dontuse ))
|
||||
{
|
||||
$leftAttrs[$name] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $leftAttrs;
|
||||
}
|
||||
|
||||
protected function getLazyData($type)
|
||||
{
|
||||
$attributes = $this->attributes;
|
||||
$value = $prefix = false;
|
||||
|
||||
if (isset($attributes['data-lazy-' . $type]) && strlen($attributes['data-lazy-' . $type]) > 0)
|
||||
{
|
||||
$value = $attributes['data-lazy-' . $type];
|
||||
$prefix = 'data-lazy-';
|
||||
}
|
||||
elseif( isset($attributes['data-' . $type]) && strlen($attributes['data-' . $type]) > 0)
|
||||
{
|
||||
$value = $attributes['data-' . $type];
|
||||
$prefix = 'data-';
|
||||
}
|
||||
elseif(isset($attributes[$type]) && strlen($attributes[$type]) > 0)
|
||||
{
|
||||
$value = $attributes[$type];
|
||||
$prefix = '';
|
||||
}
|
||||
|
||||
$this->dataTags[$type] = $prefix;
|
||||
|
||||
return $value;
|
||||
}
|
||||
} // class FrontImage
|
@ -0,0 +1,684 @@
|
||||
<?php
|
||||
namespace ShortPixel\Model\Image;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
|
||||
use ShortPixel\Controller\OptimizeController as OptimizeController;
|
||||
use ShortPixel\Helper\UtilHelper as UtilHelper;
|
||||
|
||||
use ShortPixel\Controller\ApiController as API;
|
||||
|
||||
|
||||
// @todo Custom Model for adding files, instead of meta DAO.
|
||||
class CustomImageModel extends \ShortPixel\Model\Image\ImageModel
|
||||
{
|
||||
|
||||
protected $folder_id;
|
||||
protected $path_md5;
|
||||
|
||||
protected $type = 'custom';
|
||||
|
||||
protected $thumbnails = array(); // placeholder, should return empty.
|
||||
protected $retinas = array(); // placeholder, should return empty.
|
||||
|
||||
protected $in_db = false;
|
||||
protected $is_stub = false;
|
||||
|
||||
protected $is_main_file = true;
|
||||
|
||||
/** @var array */
|
||||
protected $forceSettings = array(); // option derives from setting or otherwise, request to be forced upon via UI to use specific value.
|
||||
|
||||
|
||||
// @param int $id
|
||||
public function __construct($id)
|
||||
{
|
||||
$this->id = $id;
|
||||
|
||||
if ($id > 0)
|
||||
{
|
||||
$bool = $this->loadMeta();
|
||||
/*if ($bool)
|
||||
{
|
||||
$this->setWebp();
|
||||
$this->setAvif();
|
||||
} */
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->fullpath = ''; // stub
|
||||
$this->image_meta = new ImageMeta();
|
||||
$this->is_stub = true;
|
||||
}
|
||||
parent::__construct($this->fullpath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $folder_id;
|
||||
*/
|
||||
public function setFolderId($folder_id)
|
||||
{
|
||||
$this->folder_id = $folder_id;
|
||||
}
|
||||
|
||||
|
||||
public function getOptimizeUrls()
|
||||
{
|
||||
|
||||
$data = $this->getOptimizeData();
|
||||
return array_values($data['urls']);
|
||||
|
||||
}
|
||||
|
||||
protected function getExcludePatterns()
|
||||
{
|
||||
$args = array(
|
||||
'filter' => true,
|
||||
'is_custom' => true,
|
||||
);
|
||||
|
||||
$patterns = UtilHelper::getExclusions($args);
|
||||
return $patterns;
|
||||
}
|
||||
|
||||
public function getOptimizeData()
|
||||
{
|
||||
$parameters = array(
|
||||
'urls' => array(),
|
||||
'params' => array(),
|
||||
'returnParams' => array(),
|
||||
);
|
||||
|
||||
$fs = \wpSPIO()->filesystem();
|
||||
if ($this->is_virtual())
|
||||
$url = $this->getFullPath();
|
||||
else
|
||||
$url = $this->getURL();
|
||||
|
||||
$settings = \wpSPIO()->settings();
|
||||
$isSmartCrop = ($settings->useSmartcrop == true && $this->getExtension() !== 'pdf') ? true : false;
|
||||
$paramListArgs = array(); // args for the params, yes.
|
||||
|
||||
if (isset($this->forceSettings['smartcrop']) && $this->getExtension() !== 'pdf')
|
||||
{
|
||||
$isSmartCrop = ($this->forceSettings['smartcrop'] == ImageModel::ACTION_SMARTCROP) ? true : false;
|
||||
}
|
||||
$paramListArgs['smartcrop'] = $isSmartCrop;
|
||||
$paramListArgs['main_url'] = $url;
|
||||
$paramListArgs['url'] = $url;
|
||||
|
||||
if ($this->isProcessable(true) || $this->isProcessableAnyFileType())
|
||||
{
|
||||
$parameters['urls'][0] = $url;
|
||||
$parameters['paths'][0] = $this->getFullPath();
|
||||
$parameters['params'][0] = $this->createParamList($paramListArgs);
|
||||
$parameters['returnParams']['sizes'][0] = $this->getFileName();
|
||||
|
||||
if ($isSmartCrop )
|
||||
{
|
||||
$parameters['returnParams']['fileSizes'][0] = $this->getFileSize();
|
||||
}
|
||||
}
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
public function doSetting($setting, $value)
|
||||
{
|
||||
$this->forceSettings[$setting] = $value;
|
||||
}
|
||||
|
||||
public function getURL()
|
||||
{
|
||||
return \wpSPIO()->filesystem()->pathToUrl($this);
|
||||
}
|
||||
|
||||
public function count($type)
|
||||
{
|
||||
// everything is 1 on 1 in the customModel
|
||||
switch($type)
|
||||
{
|
||||
case 'thumbnails':
|
||||
return 0;
|
||||
break;
|
||||
case 'webps':
|
||||
$count = count($this->getWebps());
|
||||
break;
|
||||
case 'avifs':
|
||||
$count = count($this->getAvifs());
|
||||
break;
|
||||
case 'retinas':
|
||||
$count = count($this->getRetinas());
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
return $count; // 0 or 1
|
||||
}
|
||||
|
||||
/* Check if an image in theory could be processed. Check only exclusions, don't check status etc */
|
||||
public function isProcessable($strict = false)
|
||||
{
|
||||
$bool = parent::isProcessable();
|
||||
|
||||
if($strict)
|
||||
{
|
||||
return $bool;
|
||||
}
|
||||
|
||||
// The exclude size on the image - via regex - if fails, prevents the whole thing from optimization.
|
||||
if ($this->processable_status == ImageModel::P_EXCLUDE_SIZE || $this->processable_status == ImageModel::P_EXCLUDE_PATH)
|
||||
{
|
||||
return $bool;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* if ($bool === false && $strict === false)
|
||||
{
|
||||
// Todo check if Webp / Acif is active, check for unoptimized items
|
||||
if ($this->isProcessableFileType('webp'))
|
||||
{
|
||||
$bool = true;
|
||||
}
|
||||
if ($this->isProcessableFileType('avif'))
|
||||
{
|
||||
$bool = true;
|
||||
}
|
||||
|
||||
} */
|
||||
|
||||
// From above to below was implemented because it could not detect file not writable / directory not writable issues if there was any option to generate webp in the settings. Should check for all those file issues first.
|
||||
|
||||
// First test if this file isn't unprocessable for any other reason, then check.
|
||||
if (($this->isProcessable(true) || $this->isOptimized() ) && $this->isProcessableAnyFileType() === true)
|
||||
{
|
||||
if (false === $this->is_directory_writable())
|
||||
{
|
||||
$bool = false;
|
||||
}
|
||||
else {
|
||||
$bool = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $bool;
|
||||
}
|
||||
|
||||
public function isRestorable()
|
||||
{
|
||||
|
||||
$bool = parent::isRestorable();
|
||||
|
||||
// If fine, all fine.
|
||||
if ($bool == true)
|
||||
{
|
||||
return $bool;
|
||||
}
|
||||
|
||||
// If not, check this..
|
||||
if ($this->hasBackup() && $this->getMeta('status') == self::FILE_STATUS_PREVENT)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return $bool;
|
||||
}
|
||||
}
|
||||
|
||||
protected function getWebps()
|
||||
{
|
||||
$webp = array($this->getWebp());
|
||||
return array_filter($webp);
|
||||
}
|
||||
|
||||
protected function getAvifs()
|
||||
{
|
||||
$avif = array($this->getAvif());
|
||||
return array_filter($avif);
|
||||
}
|
||||
|
||||
/** Get FileTypes that might be optimized. Checking for setting should go via isProcessableFileType! */
|
||||
public function getOptimizeFileType($type = 'webp')
|
||||
{
|
||||
// Pdf files can't have special images.
|
||||
if ($this->getExtension() == 'pdf')
|
||||
return array();
|
||||
|
||||
if ($type == 'webp')
|
||||
{
|
||||
$types = $this->getWebps();
|
||||
}
|
||||
elseif ($type == 'avif')
|
||||
{
|
||||
$types = $this->getAvifs();
|
||||
}
|
||||
|
||||
$toOptimize = array();
|
||||
$fs = \WPSPIO()->filesystem();
|
||||
|
||||
// The file must not exist yet.
|
||||
if (count($types) == 0 && ($this->isProcessable(true) || $this->isOptimized()) )
|
||||
return array($fs->pathToUrl($this));
|
||||
else
|
||||
return array();
|
||||
|
||||
}
|
||||
|
||||
public function restore($args = array())
|
||||
{
|
||||
do_action('shortpixel_before_restore_image', $this->get('id'));
|
||||
do_action('shortpixel/image/before_restore', $this);
|
||||
|
||||
$defaults = array(
|
||||
'keep_in_queue' => false, // used for bulk restore.
|
||||
);
|
||||
|
||||
$args = wp_parse_args($args, $defaults);
|
||||
|
||||
$bool = parent::restore();
|
||||
|
||||
$return = true;
|
||||
if ($bool)
|
||||
{
|
||||
$this->setMeta('status', ImageModel::FILE_STATUS_UNPROCESSED);
|
||||
$this->setMeta('compressedSize', 0);
|
||||
$this->setMeta('compressionType', null);
|
||||
|
||||
|
||||
$this->saveMeta();
|
||||
|
||||
$webps = $this->getWebps();
|
||||
foreach($webps as $webpFile)
|
||||
$webpFile->delete();
|
||||
|
||||
$avifs = $this->getAvifs();
|
||||
foreach($avifs as $avifFile)
|
||||
$avifFile->delete();
|
||||
}
|
||||
else
|
||||
{
|
||||
$return = false;
|
||||
}
|
||||
|
||||
if ($args['keep_in_queue'] === false)
|
||||
{
|
||||
$this->dropFromQueue();
|
||||
}
|
||||
do_action('shortpixel/image/after_restore', $this, $this->id, $bool);
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
public function handleOptimized($optimizeData, $args = array())
|
||||
{
|
||||
$bool = true;
|
||||
|
||||
if (isset($optimizeData['files']) && isset($optimizeData['data']))
|
||||
{
|
||||
$files = $optimizeData['files'];
|
||||
$data = $optimizeData['data'];
|
||||
}
|
||||
else {
|
||||
Log::addError('Something went wrong with handleOptimized', $optimizeData);
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (! $this->isOptimized() ) // main file might not be contained in results
|
||||
{
|
||||
$bool = parent::handleOptimized($files[0]);
|
||||
}
|
||||
|
||||
$this->handleOptimizedFileType($files[0]);
|
||||
|
||||
if ($bool)
|
||||
{
|
||||
$this->setMeta('customImprovement', parent::getImprovement());
|
||||
$this->saveMeta();
|
||||
}
|
||||
|
||||
// $this->deleteTempFiles($files);
|
||||
|
||||
return $bool;
|
||||
}
|
||||
|
||||
public function loadMeta()
|
||||
{
|
||||
|
||||
global $wpdb;
|
||||
|
||||
$sql = 'SELECT * FROM ' . $wpdb->prefix . 'shortpixel_meta where id = %d';
|
||||
$sql = $wpdb->prepare($sql, $this->id);
|
||||
|
||||
$imagerow = $wpdb->get_row($sql);
|
||||
|
||||
$metaObj = new ImageMeta();
|
||||
$this->image_meta = $metaObj; // even if not found, load an empty imageMeta.
|
||||
|
||||
if (! is_object($imagerow))
|
||||
return false;
|
||||
|
||||
$this->in_db = true; // record found.
|
||||
|
||||
$this->fullpath = $imagerow->path;
|
||||
$this->folder_id = $imagerow->folder_id;
|
||||
$this->path_md5 = $imagerow->path_md5;
|
||||
|
||||
$status = intval($imagerow->status);
|
||||
$metaObj->status = $status;
|
||||
|
||||
if ($status == ImageModel::FILE_STATUS_SUCCESS)
|
||||
{
|
||||
$metaObj->customImprovement = $imagerow->message;
|
||||
}
|
||||
|
||||
|
||||
$metaObj->compressedSize = intval($imagerow->compressed_size);
|
||||
// The null check is important, otherwise it will always optimize wrongly.
|
||||
$metaObj->compressionType = (is_null($imagerow->compression_type)) ? null : intval($imagerow->compression_type);
|
||||
|
||||
if (! is_numeric($imagerow->message) && ! is_null($imagerow->message))
|
||||
$metaObj->errorMessage = $imagerow->message;
|
||||
|
||||
$metaObj->did_keepExif = (intval($imagerow->keep_exif) == 1) ? true : false;
|
||||
|
||||
$metaObj->did_cmyk2rgb = (intval($imagerow->cmyk2rgb) == 1) ? true : false;
|
||||
|
||||
$metaObj->resize = (intval($imagerow->resize) > 1) ? true : false;
|
||||
|
||||
if (intval($imagerow->resize_width) > 0)
|
||||
$metaObj->resizeWidth = intval($imagerow->resize_width);
|
||||
|
||||
if (intval($imagerow->resize_height) > 0)
|
||||
$metaObj->resizeHeight = intval($imagerow->resize_height);
|
||||
|
||||
//$metaObj->has_backup = (intval($imagerow->backup) == 1) ? true : false;
|
||||
|
||||
$addedDate = UtilHelper::DBtoTimestamp($imagerow->ts_added);
|
||||
$metaObj->tsAdded = $addedDate;
|
||||
|
||||
$optimizedDate = UtilHelper::DBtoTimestamp($imagerow->ts_optimized);
|
||||
$metaObj->tsOptimized = $optimizedDate;
|
||||
|
||||
$extraInfo = property_exists($imagerow, 'extra_info') ? $imagerow->extra_info : null;
|
||||
|
||||
if (! is_null($extraInfo))
|
||||
{
|
||||
$data = json_decode($extraInfo, true);
|
||||
|
||||
if (isset($data['webpStatus']))
|
||||
{
|
||||
$this->setMeta('webp', $data['webpStatus']);
|
||||
}
|
||||
if (isset($data['avifStatus']))
|
||||
{
|
||||
$this->setMeta('avif', $data['avifStatus']);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
$this->image_meta = $metaObj;
|
||||
}
|
||||
|
||||
public function getParent()
|
||||
{
|
||||
return false; // no parents here
|
||||
}
|
||||
|
||||
/** Load a CustomImageModel as Stub ( to be added ) . Checks if the image is already added as well
|
||||
*
|
||||
* @param String $path
|
||||
* @param Boolean $load
|
||||
*/
|
||||
public function setStub($path, $load = true)
|
||||
{
|
||||
$this->fullpath = $path;
|
||||
$this->path_md5 = md5($this->fullpath);
|
||||
|
||||
global $wpdb;
|
||||
|
||||
$sql = 'SELECT id from ' . $wpdb->prefix . 'shortpixel_meta where path = %s';
|
||||
$sql = $wpdb->prepare($sql, $path);
|
||||
|
||||
$result = $wpdb->get_var($sql);
|
||||
if ( ! is_null($result) )
|
||||
{
|
||||
$this->in_db = true;
|
||||
$this->id = $result;
|
||||
if ($load)
|
||||
$this->loadMeta();
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->image_meta = new ImageMeta();
|
||||
$this->image_meta->compressedSize = 0;
|
||||
$this->image_meta->tsOptimized = 0;
|
||||
$this->image_meta->tsAdded = time();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected function preventNextTry($reason = '', $status = self::FILE_STATUS_PREVENT)
|
||||
{
|
||||
$this->setMeta('errorMessage', $reason);
|
||||
$this->setMeta('status', $status);
|
||||
$this->saveMeta();
|
||||
}
|
||||
|
||||
public function markCompleted($reason, $status)
|
||||
{
|
||||
return $this->preventNextTry($reason, $status);
|
||||
}
|
||||
|
||||
public function isOptimizePrevented()
|
||||
{
|
||||
$status = $this->getMeta('status');
|
||||
|
||||
if ($status == self::FILE_STATUS_PREVENT || $status == self::FILE_STATUS_MARKED_DONE )
|
||||
{
|
||||
$this->processable_status = self::P_OPTIMIZE_PREVENTED;
|
||||
$this->optimizePreventedReason = $this->getMeta('errorMessage');
|
||||
|
||||
return $this->getMeta('errorMessage');
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only one item for now, so it's equal
|
||||
public function isSomethingOptimized()
|
||||
{
|
||||
return $this->isOptimized();
|
||||
}
|
||||
|
||||
public function resetPrevent()
|
||||
{
|
||||
|
||||
if ($this->hasBackup())
|
||||
$this->setMeta('status', self::FILE_STATUS_SUCCESS);
|
||||
else
|
||||
$this->setMeta('status', self::FILE_STATUS_UNPROCESSED);
|
||||
|
||||
$this->setMeta('errorMessage', '');
|
||||
$this->saveMeta();
|
||||
}
|
||||
|
||||
public function saveMeta()
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'shortpixel_meta';
|
||||
$where = array('id' => $this->id);
|
||||
|
||||
$metaObj = $this->image_meta;
|
||||
|
||||
if (! is_null($metaObj->customImprovement) && is_numeric($metaObj->customImprovement))
|
||||
$message = $metaObj->customImprovement;
|
||||
elseif (! is_null($metaObj->errorMessage))
|
||||
$message = $metaObj->errorMessage;
|
||||
else
|
||||
$message = null;
|
||||
|
||||
$optimized = new \DateTime();
|
||||
$optimized->setTimestamp($metaObj->tsOptimized);
|
||||
|
||||
$added = new \DateTime();
|
||||
$added->setTimeStamp($metaObj->tsAdded);
|
||||
|
||||
$extra_info = array();
|
||||
if ($this->getMeta('webp') === self::FILETYPE_BIGGER)
|
||||
{
|
||||
$extra_info['webpStatus'] = self::FILETYPE_BIGGER;
|
||||
}
|
||||
if ($this->getMeta('avif') === self::FILETYPE_BIGGER)
|
||||
{
|
||||
$extra_info['avifStatus'] = self::FILETYPE_BIGGER;
|
||||
}
|
||||
|
||||
if (count($extra_info) > 0)
|
||||
{
|
||||
$extra_info = json_encode($extra_info);
|
||||
}
|
||||
else {
|
||||
$extra_info = null;
|
||||
}
|
||||
|
||||
$data = array(
|
||||
'folder_id' => $this->folder_id,
|
||||
'compressed_size' => $metaObj->compressedSize,
|
||||
'compression_type' => $metaObj->compressionType,
|
||||
'keep_exif' => ($metaObj->did_keepExif) ? 1 : 0,
|
||||
'cmyk2rgb' => ($metaObj->did_cmyk2rgb) ? 1 : 0,
|
||||
'resize' => ($metaObj->resize) ? 1 : 0,
|
||||
'resize_width' => $metaObj->resizeWidth,
|
||||
'resize_height' => $metaObj->resizeHeight,
|
||||
'backup' => ($this->hasBackup()) ? 1 : 0,
|
||||
'status' => $metaObj->status,
|
||||
'retries' => 0, // this is unused / legacy
|
||||
'message' => $message, // this is used for improvement line.
|
||||
'ts_added' => UtilHelper::timestampToDB($metaObj->tsAdded),
|
||||
'ts_optimized' => UtilHelper::timestampToDB($metaObj->tsOptimized),
|
||||
'path' => $this->getFullPath(),
|
||||
'name' => $this->getFileName(),
|
||||
'path_md5' => md5($this->getFullPath()), // this is legacy
|
||||
'extra_info' => $extra_info,
|
||||
);
|
||||
// The keys are just for readability.
|
||||
$format = array(
|
||||
'folder_id' => '%d',
|
||||
'compressed_size' => '%d',
|
||||
'compression_type' => '%d' ,
|
||||
'keep_exif' => '%d' ,
|
||||
'cmyk2rgb' => '%d' ,
|
||||
'resize' => '%d' ,
|
||||
'resize_width' => '%d',
|
||||
'resize_height' => '%d',
|
||||
'backup' => '%d',
|
||||
'status' => '%d',
|
||||
'retries' => '%d', // this is unused / legacy
|
||||
'message' => '%s', // this is used for improvement line.
|
||||
'ts_added' => '%s',
|
||||
'ts_optimized' => '%s' ,
|
||||
'path' => '%s',
|
||||
'name' => '%s',
|
||||
'path_md5' => '%s' , // this is legacy
|
||||
'extra_info' => '%s',
|
||||
);
|
||||
|
||||
|
||||
$is_new = false;
|
||||
|
||||
if ($this->in_db)
|
||||
{
|
||||
$res = $wpdb->update($table, $data, $where, $format); // result is amount rows updated.
|
||||
}
|
||||
else
|
||||
{
|
||||
$is_new = true;
|
||||
$res = $wpdb->insert($table, $data, $format); // result is new inserted id
|
||||
}
|
||||
|
||||
if ($is_new)
|
||||
{
|
||||
$this->id = $wpdb->insert_id;
|
||||
}
|
||||
|
||||
if ($res !== false)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
public function deleteMeta()
|
||||
{
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'shortpixel_meta';
|
||||
$where = array('id' => $this->id);
|
||||
|
||||
$result = $wpdb->delete($table, $where, array('%d'));
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function onDelete()
|
||||
{
|
||||
parent::onDelete();
|
||||
$this->deleteMeta();
|
||||
$this->dropfromQueue();
|
||||
}
|
||||
|
||||
public function dropFromQueue()
|
||||
{
|
||||
$optimizeController = new OptimizeController();
|
||||
|
||||
$q = $optimizeController->getQueue($this->type);
|
||||
$q->dropItem($this->get('id'));
|
||||
|
||||
// Drop also from bulk if there.
|
||||
|
||||
$optimizeController->setBulk(true);
|
||||
|
||||
$q = $optimizeController->getQueue($this->type);
|
||||
$q->dropItem($this->get('id'));
|
||||
}
|
||||
|
||||
public function getImprovement($int = false)
|
||||
{
|
||||
return $this->getMeta('customImprovement');
|
||||
}
|
||||
|
||||
public function getImprovements()
|
||||
{
|
||||
$improvements = array();
|
||||
/*$totalsize = $totalperc = $count = 0;
|
||||
if ($this->isOptimized())
|
||||
{
|
||||
$perc = $this->getImprovement();
|
||||
$size = $this->getImprovement(true);
|
||||
$totalsize += $size;
|
||||
$totalperc += $perc;
|
||||
$improvements['main'] = array($perc, $size);
|
||||
$count++;
|
||||
} */
|
||||
$improvement = $this->getImprovement();
|
||||
if (is_null($improvement)) // getImprovement can return null.
|
||||
{
|
||||
$improvement = 0;
|
||||
}
|
||||
$improvements['main'] = array($improvement, 0);
|
||||
$improvements['totalpercentage'] = round($improvement); // the same.
|
||||
|
||||
return $improvements;
|
||||
|
||||
// return $improvements; // we have no thumbnails.
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
<?php
|
||||
namespace ShortPixel\Model\Image;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
|
||||
|
||||
class ImageConvertMeta
|
||||
{
|
||||
|
||||
protected $fileFormat; // png / heic etc
|
||||
protected $isConverted = false;
|
||||
protected $placeholder = false;
|
||||
protected $replacementImageBase = false;
|
||||
// protected $doConversion = false;
|
||||
protected $triedConversion = false;
|
||||
protected $errorReason = false;
|
||||
protected $omitBackup = true; // Don't backup the converted image (again), keeping only the original format. if not, make a backup of the converted file and treat that as the default backup/restore
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function isConverted()
|
||||
{
|
||||
return $this->isConverted;
|
||||
}
|
||||
|
||||
public function didTry()
|
||||
{
|
||||
return $this->triedConversion;
|
||||
}
|
||||
|
||||
public function setTried($value)
|
||||
{
|
||||
$this->triedConversion = $value;
|
||||
}
|
||||
|
||||
public function setConversionDone($omitBackup = true)
|
||||
{
|
||||
$this->isConverted = true;
|
||||
$this->omitBackup = $omitBackup;
|
||||
}
|
||||
|
||||
public function setError($code)
|
||||
{
|
||||
$this->errorReason = $code;
|
||||
}
|
||||
|
||||
public function getError()
|
||||
{
|
||||
return $this->errorReason;
|
||||
}
|
||||
|
||||
public function setFileFormat($ext)
|
||||
{
|
||||
if (is_null($this->fileFormat))
|
||||
$this->fileFormat = $ext;
|
||||
}
|
||||
|
||||
public function getFileFormat()
|
||||
{
|
||||
return $this->fileFormat;
|
||||
}
|
||||
|
||||
public function omitBackup()
|
||||
{
|
||||
return $this->omitBackup;
|
||||
}
|
||||
|
||||
// bool for now, otherwise if needed.
|
||||
public function setPlaceHolder($placeholder = true)
|
||||
{
|
||||
$this->placeholder = $placeholder;
|
||||
}
|
||||
|
||||
public function hasPlaceHolder()
|
||||
{
|
||||
return $this->placeholder;
|
||||
}
|
||||
|
||||
public function setReplacementImageBase($name)
|
||||
{
|
||||
$this->replacementImageBase = $name;
|
||||
|
||||
}
|
||||
|
||||
public function getReplacementImageBase()
|
||||
{
|
||||
return $this->replacementImageBase;
|
||||
|
||||
}
|
||||
|
||||
|
||||
public function fromClass($object)
|
||||
{
|
||||
foreach($object as $property => $value)
|
||||
{
|
||||
if (property_exists($this, $property))
|
||||
{
|
||||
$this->$property = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function toClass()
|
||||
{
|
||||
$class = new \stdClass;
|
||||
$vars = get_object_vars($this);
|
||||
|
||||
foreach($vars as $property => $value) // only used by media library.
|
||||
{
|
||||
$class->$property = $this->$property;
|
||||
}
|
||||
return $class;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
namespace ShortPixel\Model\Image;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
// Base Class for ImageMeta
|
||||
class ImageMeta extends ImageThumbnailMeta
|
||||
{
|
||||
|
||||
public $errorMessage;
|
||||
public $wasConverted = false; // Was converted from legacy format
|
||||
|
||||
protected $convertMeta;
|
||||
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->convertMeta = new ImageConvertMeta();
|
||||
|
||||
}
|
||||
|
||||
public function fromClass($object)
|
||||
{
|
||||
if (property_exists($object, 'convertMeta'))
|
||||
{
|
||||
|
||||
$this->convertMeta->fromClass($object->convertMeta);
|
||||
unset($object->convertMeta);
|
||||
}
|
||||
// legacy.
|
||||
if (property_exists($object, 'tried_png2jpg') && $object->tried_png2jpg)
|
||||
{
|
||||
$this->convertMeta()->setTried($object->tried_png2jpg);
|
||||
}
|
||||
elseif (property_exists($object, 'did_png2jpg') && $object->did_png2jpg)
|
||||
{
|
||||
$this->convertMeta()->setFileFormat('png');
|
||||
$this->convertMeta()->setConversionDone();
|
||||
|
||||
}
|
||||
|
||||
parent::fromClass($object);
|
||||
}
|
||||
|
||||
|
||||
public function convertMeta()
|
||||
{
|
||||
return $this->convertMeta;
|
||||
}
|
||||
|
||||
} // class
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,106 @@
|
||||
<?php
|
||||
namespace ShortPixel\Model\Image;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
|
||||
|
||||
class ImageThumbnailMeta
|
||||
{
|
||||
/** @var int */
|
||||
public $databaseID = null;
|
||||
/** @var int */
|
||||
public $status = 0;
|
||||
/** @var int */
|
||||
public $compressionType;
|
||||
/** @var int */
|
||||
public $compressedSize;
|
||||
/** @var int */
|
||||
public $originalSize;
|
||||
// public $improvement;
|
||||
|
||||
/** @var boolean */
|
||||
public $did_keepExif = false;
|
||||
|
||||
/** @var boolean */
|
||||
public $did_cmyk2rgb = false;
|
||||
|
||||
/** @var int */
|
||||
public $resize;
|
||||
/** @var int */
|
||||
public $resizeWidth;
|
||||
/** @var int */
|
||||
public $resizeHeight;
|
||||
/** @var int */
|
||||
public $resizeType;
|
||||
/** @var int */
|
||||
public $originalWidth;
|
||||
/** @var int */
|
||||
public $originalHeight;
|
||||
|
||||
public $tsAdded;
|
||||
public $tsOptimized;
|
||||
public $webp;
|
||||
public $avif;
|
||||
|
||||
public $file; // **Only for unlisted images. This defines an unlisted image */
|
||||
|
||||
// Only for customImageModel! Exception to prevent having to create a whole class. Second var here, warrants a subclass.
|
||||
public $customImprovement;
|
||||
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->tsAdded = time(); // default
|
||||
}
|
||||
|
||||
|
||||
/** Load data from basic class to prevent issues when class definitions changes over time */
|
||||
public function fromClass($object)
|
||||
{
|
||||
|
||||
foreach($object as $property => $value)
|
||||
{
|
||||
if ($property == 'customImprovement')
|
||||
{ continue; }
|
||||
|
||||
|
||||
if (property_exists($this, $property))
|
||||
{
|
||||
$this->$property = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Save data as basic class to prevent issues when class definitions changes over time */
|
||||
public function toClass()
|
||||
{
|
||||
$class = new \stdClass;
|
||||
$vars = get_object_vars($this);
|
||||
|
||||
foreach($vars as $property => $value) // only used by media library.
|
||||
{
|
||||
if ($property == 'customImprovement')
|
||||
{ continue; }
|
||||
|
||||
if ($property == 'convertMeta' && is_null($this->convertMeta))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
elseif ($property == 'convertMeta') {
|
||||
$class->$property = $this->$property->toClass();
|
||||
continue;
|
||||
}
|
||||
// if (is_null($value)) // don't save default / values without init.
|
||||
// continue;
|
||||
|
||||
|
||||
$class->$property = $this->$property;
|
||||
}
|
||||
|
||||
return $class;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,591 @@
|
||||
<?php
|
||||
namespace ShortPixel\Model\Image;
|
||||
|
||||
use ShortPixel\Helper\DownloadHelper as DownloadHelper;
|
||||
use ShortPixel\Helper\UtilHelper as UtilHelper;
|
||||
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
|
||||
|
||||
use \ShortPixel\Model\File\FileModel as FileModel;
|
||||
|
||||
// Represent a thumbnail image / limited image in mediaLibrary.
|
||||
class MediaLibraryThumbnailModel extends \ShortPixel\Model\Image\ImageModel
|
||||
{
|
||||
|
||||
public $name;
|
||||
|
||||
/* public $width;
|
||||
public $height;
|
||||
public $mime; */
|
||||
protected $prevent_next_try = false;
|
||||
protected $is_main_file = false;
|
||||
protected $is_retina = false; // diffentiate from thumbnail / retina.
|
||||
protected $id; // this is the parent attachment id
|
||||
protected $size; // size name of image in WP, if applicable.
|
||||
protected $sizeDefinition; // size width / height / crop according to WordPress
|
||||
|
||||
public function __construct($path, $id, $size)
|
||||
{
|
||||
|
||||
parent::__construct($path);
|
||||
$this->image_meta = new ImageThumbnailMeta();
|
||||
$this->id = $id;
|
||||
$this->imageType = self::IMAGE_TYPE_THUMB;
|
||||
$this->size = $size;
|
||||
|
||||
}
|
||||
|
||||
|
||||
protected function loadMeta()
|
||||
{
|
||||
}
|
||||
|
||||
protected function saveMeta()
|
||||
{
|
||||
}
|
||||
|
||||
public function __debugInfo() {
|
||||
return array(
|
||||
'image_meta' => $this->image_meta,
|
||||
'name' => $this->name,
|
||||
'path' => $this->getFullPath(),
|
||||
'size' => $this->size,
|
||||
'width' => $this->get('width'),
|
||||
'height' => $this->get('height'),
|
||||
'exists' => ($this->exists()) ? 'yes' : 'no',
|
||||
'is_virtual' => ($this->is_virtual()) ? 'yes' : 'no',
|
||||
'wordpress_size' => $this->sizeDefinition,
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
/** Set the meta name of thumbnail. */
|
||||
public function setName($name)
|
||||
{
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
public function setSizeDefinition($sizedef)
|
||||
{
|
||||
$this->sizeDefinition = $sizedef;
|
||||
}
|
||||
|
||||
public function setImageType($type)
|
||||
{
|
||||
$this->imageType = $type;
|
||||
}
|
||||
|
||||
public function getRetina()
|
||||
{
|
||||
if ($this->is_virtual())
|
||||
{
|
||||
$fs = \wpSPIO()->filesystem();
|
||||
$filepath = apply_filters('shortpixel/file/virtual/translate', $this->getFullPath(), $this);
|
||||
$virtualFile = $fs->getFile($filepath);
|
||||
|
||||
$filebase = $virtualFile->getFileBase();
|
||||
$filepath = (string) $virtualFile->getFileDir();
|
||||
$extension = $virtualFile->getExtension();
|
||||
|
||||
// This function needs an hard check on file exists, which might not be wanted.
|
||||
if (false === \wpSPIO()->env()->useVirtualHeavyFunctions())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
$filebase = $this->getFileBase();
|
||||
$filepath = (string) $this->getFileDir();
|
||||
$extension = $this->getExtension();
|
||||
}
|
||||
|
||||
$retina = new MediaLibraryThumbnailModel($filepath . $filebase . '@2x.' . $extension, $this->id, $this->size); // mind the dot in after 2x
|
||||
$retina->setName($this->size);
|
||||
$retina->setImageType(self::IMAGE_TYPE_RETINA);
|
||||
|
||||
$retina->is_retina = true;
|
||||
|
||||
$forceCheck = true;
|
||||
if ($retina->exists($forceCheck))
|
||||
return $retina;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function isFileTypeNeeded($type = 'webp')
|
||||
{
|
||||
// pdf extension can be optimized, but don't come with these filetypes
|
||||
if ($this->getExtension() == 'pdf')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($type == 'webp')
|
||||
$file = $this->getWebp();
|
||||
elseif ($type == 'avif')
|
||||
$file = $this->getAvif();
|
||||
|
||||
if ( ($this->isThumbnailProcessable() || $this->isOptimized()) && $file === false) // if no file, it can be optimized.
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
// @param FileDelete can be false. I.e. multilang duplicates might need removal of metadata, but not images.
|
||||
public function onDelete($fileDelete = true)
|
||||
{
|
||||
if ($fileDelete == true)
|
||||
$bool = parent::onDelete();
|
||||
else {
|
||||
$bool = true;
|
||||
}
|
||||
|
||||
// minimally reset all the metadata.
|
||||
if ($this->is_main_file)
|
||||
{
|
||||
$this->image_meta = new ImageMeta();
|
||||
}
|
||||
else {
|
||||
$this->image_meta = new ImageThumbnailMeta();
|
||||
}
|
||||
|
||||
return $bool;
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected function setMetaObj($metaObj)
|
||||
{
|
||||
$this->image_meta = clone $metaObj;
|
||||
}
|
||||
|
||||
protected function getMetaObj()
|
||||
{
|
||||
return $this->image_meta;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// get_path param see MediaLibraryModel
|
||||
// This should be unused at the moment!
|
||||
public function getOptimizeUrls()
|
||||
{
|
||||
if (! $this->isProcessable() )
|
||||
return false;
|
||||
|
||||
$url = $this->getURL();
|
||||
|
||||
if (! $url)
|
||||
{
|
||||
return false; //nothing
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
public function getURL()
|
||||
{
|
||||
$fs = \wpSPIO()->filesystem();
|
||||
|
||||
if ($this->size == 'original' && ! $this->get('is_retina'))
|
||||
{
|
||||
$url = wp_get_original_image_url($this->id);
|
||||
}
|
||||
elseif ($this->isUnlisted())
|
||||
{
|
||||
$url = $fs->pathToUrl($this);
|
||||
}
|
||||
else
|
||||
{
|
||||
// We can't trust higher lever function, or any WP functions. I.e. Woocommerce messes with the URL's if they like so.
|
||||
// So get it from intermediate and if that doesn't work, default to pathToUrl - better than nothing.
|
||||
// https://app.asana.com/0/1200110778640816/1202589533659780
|
||||
$size_array = image_get_intermediate_size($this->id, $this->size);
|
||||
|
||||
if ($size_array === false || ! isset($size_array['url']))
|
||||
{
|
||||
$url = $fs->pathToUrl($this);
|
||||
}
|
||||
elseif (isset($size_array['url']))
|
||||
{
|
||||
$url = $size_array['url'];
|
||||
// Even this can go wrong :/
|
||||
if (strpos($url, $this->getFileName() ) === false)
|
||||
{
|
||||
// Taken from image_get_intermediate_size if somebody still messes with the filters.
|
||||
$mainurl = wp_get_attachment_url( $this->id);
|
||||
$url = path_join( dirname( $mainurl ), $this->getFileName() );
|
||||
}
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $this->fs()->checkURL($url);
|
||||
}
|
||||
|
||||
// Just a placeholder for abstract, shouldn't do anything.
|
||||
public function getImprovements()
|
||||
{
|
||||
return parent::getImprovements();
|
||||
}
|
||||
|
||||
|
||||
public function getBackupFileName()
|
||||
{
|
||||
$mainFile = ($this->is_main_file) ? $this : $this->getMainFile();
|
||||
if (false == $mainFile)
|
||||
{
|
||||
return parent::getBackupFileName();
|
||||
}
|
||||
|
||||
if ($mainFile->getMeta()->convertMeta()->getReplacementImageBase() !== false)
|
||||
{
|
||||
if ($this->is_main_file)
|
||||
return $mainFile->getMeta()->convertMeta()->getReplacementImageBase() . '.' . $this->getExtension();
|
||||
else {
|
||||
// $fileBaseNoSize =
|
||||
$name = str_replace($mainFile->getFileBase(), $mainFile->getMeta()->convertMeta()->getReplacementImageBase(), $this->getFileName());
|
||||
|
||||
return $name;
|
||||
}
|
||||
}
|
||||
|
||||
return parent::getBackupFileName();
|
||||
}
|
||||
|
||||
|
||||
protected function preventNextTry($reason = '')
|
||||
{
|
||||
$this->prevent_next_try = $reason;
|
||||
}
|
||||
|
||||
// Don't ask thumbnails this, only the main image
|
||||
public function isOptimizePrevented()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't ask thumbnails this, only the main image
|
||||
public function resetPrevent()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function isThumbnailProcessable()
|
||||
{
|
||||
// if thumbnail processing is off, thumbs are never processable.
|
||||
// This is also used by main file, so check for that!
|
||||
|
||||
if ( $this->excludeThumbnails() && $this->is_main_file === false && $this->get('imageType') !== self::IMAGE_TYPE_ORIGINAL)
|
||||
{
|
||||
$this->processable_status = self::P_EXCLUDE_SIZE;
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
$bool = parent::isProcessable();
|
||||
|
||||
return $bool;
|
||||
}
|
||||
}
|
||||
|
||||
/** Function to check if said thumbnail is a WP-native or something SPIO added as unlisted
|
||||
*
|
||||
*
|
||||
*/
|
||||
protected function isUnlisted()
|
||||
{
|
||||
if (! is_null($this->getMeta('file')))
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// !Important . This doubles as checking excluded image sizes.
|
||||
protected function isSizeExcluded()
|
||||
{
|
||||
|
||||
$excludeSizes = \wpSPIO()->settings()->excludeSizes;
|
||||
|
||||
if (is_array($excludeSizes) && in_array($this->name, $excludeSizes))
|
||||
{
|
||||
$this->processable_status = self::P_EXCLUDE_SIZE;
|
||||
return true;
|
||||
}
|
||||
|
||||
$bool = parent::isSizeExcluded();
|
||||
|
||||
return $bool;
|
||||
}
|
||||
|
||||
public function isProcessableFileType($type = 'webp')
|
||||
{
|
||||
// Prevent webp / avif processing for thumbnails if this is off. Exclude main file
|
||||
if ($this->excludeThumbnails() === true && $this->is_main_file === false )
|
||||
return false;
|
||||
|
||||
return parent::isProcessableFileType($type);
|
||||
}
|
||||
|
||||
protected function getExcludePatterns()
|
||||
{
|
||||
$args = array(
|
||||
'filter' => true,
|
||||
'thumbname' => $this->name,
|
||||
'is_thumbnail' => (true === $this->is_main_file) ? false : true,
|
||||
);
|
||||
|
||||
// @todo Find a way to cache IsProcessable perhaps due to amount of checks being done. Should be release in flushOptimizeCache or elsewhere (?)
|
||||
|
||||
// $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10);
|
||||
|
||||
$patterns = UtilHelper::getExclusions($args);
|
||||
// echo "<PRE>"; print_r($args); print_r($patterns); echo "</PRE>";
|
||||
return $patterns;
|
||||
}
|
||||
|
||||
protected function excludeThumbnails()
|
||||
{
|
||||
return (! \wpSPIO()->settings()->processThumbnails);
|
||||
}
|
||||
|
||||
public function hasBackup($args = array())
|
||||
{
|
||||
$defaults = array(
|
||||
'forceConverted' => false,
|
||||
'noConversionCheck' => false, // do not check on mainfile, this loops when used in loadMeta / legacyConversion
|
||||
);
|
||||
$args = wp_parse_args($args, $defaults);
|
||||
|
||||
// @todo This can probably go.
|
||||
if (true === $args['noConversionCheck'])
|
||||
{
|
||||
return parent::hasBackup();
|
||||
}
|
||||
|
||||
$mainFile = ($this->is_main_file) ? $this : $this->getMainFile();
|
||||
if (false == $mainFile)
|
||||
{
|
||||
return parent::hasBackup();
|
||||
|
||||
}
|
||||
|
||||
// When main file and converted and omitBackup is true ( only original backup ) and not forced.
|
||||
$loadRegular= (false === $mainFile->getMeta()->convertMeta()->isConverted() ||
|
||||
false === $mainFile->getMeta()->convertMeta()->omitBackup()) && false === $args['forceConverted'];
|
||||
|
||||
if (true === $loadRegular)
|
||||
{
|
||||
return parent::hasBackup();
|
||||
}
|
||||
else
|
||||
{
|
||||
$directory = $this->getBackupDirectory();
|
||||
$converted_ext = $mainFile->getMeta()->convertMeta()->getFileFormat();
|
||||
|
||||
if (! $directory)
|
||||
return false;
|
||||
|
||||
|
||||
$backupFile = $directory . $this->getFileBase() . '.' . $converted_ext;
|
||||
|
||||
// Issue with PNG not being scaled on the main file.
|
||||
if (! file_exists($backupFile) && $mainFile->isScaled())
|
||||
{
|
||||
$backupFile = $directory . $mainFile->getOriginalFile()->getFileBase() . '.' . $converted_ext;
|
||||
}
|
||||
if (file_exists($backupFile) && ! is_dir($backupFile) )
|
||||
return true;
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function hasDBRecord()
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$sql = 'SELECT id FROM ' . $wpdb->prefix . 'shortpixel_postmeta WHERE attach_id = %d AND size = %s';
|
||||
$sql = $wpdb->prepare($sql, $this->id, $this->size);
|
||||
|
||||
$id = $wpdb->get_var($sql);
|
||||
|
||||
if (is_null($id))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
elseif (is_numeric($id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function restore()
|
||||
{
|
||||
if ($this->is_virtual())
|
||||
{
|
||||
$fs = \wpSPIO()->filesystem();
|
||||
$filepath = apply_filters('shortpixel/file/virtual/translate', $this->getFullPath(), $this);
|
||||
|
||||
$this->setVirtualToReal($filepath);
|
||||
}
|
||||
|
||||
$bool = parent::restore();
|
||||
|
||||
if ($bool === true)
|
||||
{
|
||||
if ($this->is_main_file)
|
||||
{
|
||||
// If item is converted and will not be moved back to original format ( but converted ) , keep the convert metadata
|
||||
if (true === $this->getMeta()->convertMeta()->isConverted() && false === $this->getMeta()->convertMeta()->omitBackup() )
|
||||
{
|
||||
$convertMeta = clone $this->getMeta()->convertMeta();
|
||||
$imageMeta = new ImageMeta();
|
||||
$imageMeta->convertMeta()->fromClass($convertMeta);
|
||||
$bool = false; // Prevent cleanRestore from deleting the metadata.
|
||||
}
|
||||
else {
|
||||
$imageMeta = new ImageMeta();
|
||||
}
|
||||
|
||||
$this->image_meta = $imageMeta;
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->image_meta = new ImageThumbNailMeta();
|
||||
}
|
||||
}
|
||||
|
||||
return $bool;
|
||||
}
|
||||
|
||||
/** Tries to retrieve an *existing* BackupFile. Returns false if not present.
|
||||
* This file might not be writable.
|
||||
* To get writable directory reference to backup, use FileSystemController
|
||||
*/
|
||||
public function getBackupFile($args = array())
|
||||
{
|
||||
|
||||
$defaults = array(
|
||||
'forceConverted' => false,
|
||||
'noConversionCheck' => false, // do not check on mainfile, this loops when used in loadMeta / legacyConversion
|
||||
);
|
||||
$args = wp_parse_args($args, $defaults);
|
||||
|
||||
if (true === $args['noConversionCheck'])
|
||||
{
|
||||
return parent::getBackupFile();
|
||||
}
|
||||
|
||||
$mainFile = ($this->is_main_file) ? $this : $this->getMainFile();
|
||||
if (false == $mainFile)
|
||||
{
|
||||
return parent::getBackupFile();
|
||||
|
||||
}
|
||||
// When main file and converted and omitBackup is true ( only original backup ) and not forced.
|
||||
$loadRegular= (false === $mainFile->getMeta()->convertMeta()->isConverted() ||
|
||||
false === $mainFile->getMeta()->convertMeta()->omitBackup()) && false === $args['forceConverted'];
|
||||
|
||||
if (true === $loadRegular )
|
||||
{
|
||||
return parent::getBackupFile();
|
||||
}
|
||||
else
|
||||
{
|
||||
if ($this->hasBackup($args))
|
||||
{
|
||||
|
||||
$directory = $this->getBackupDirectory();
|
||||
$converted_ext = $mainFile->getMeta()->convertMeta()->getFileFormat();
|
||||
|
||||
$backupFile = $directory . $this->getFileBase() . '.' . $converted_ext;
|
||||
|
||||
/* Because WP doesn't support big PNG with scaled for some reason, it's possible it doesn't create them. Which means we end up with a scaled images without backup */
|
||||
if (! file_exists($backupFile) && $mainFile->isScaled())
|
||||
{
|
||||
$backupFile = $directory . $mainFile->getOriginalFile()->getFileBase() . '.' . $converted_ext;
|
||||
}
|
||||
|
||||
return new FileModel($backupFile);
|
||||
|
||||
}
|
||||
else
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected function createBackup()
|
||||
{
|
||||
if ($this->is_virtual()) // download remote file to backup.
|
||||
{
|
||||
$fs = \wpSPIO()->filesystem();
|
||||
$filepath = apply_filters('shortpixel/file/virtual/translate', $this->getFullPath(), $this);
|
||||
|
||||
$result = false;
|
||||
if ($this->virtual_status == self::$VIRTUAL_REMOTE)
|
||||
{
|
||||
// filepath is translated. Check if this exists as a local copy, if not remotely download.
|
||||
if ($filepath !== $this->getFullPath())
|
||||
{
|
||||
$fileObj = $fs->getFile($filepath);
|
||||
$fileExists = $fileObj->exists();
|
||||
}
|
||||
else {
|
||||
$fileExists = false;
|
||||
}
|
||||
|
||||
if (false === $fileExists)
|
||||
{
|
||||
$downloadHelper = DownloadHelper::getInstance();
|
||||
$url = $this->getURL();
|
||||
$result = $downloadHelper->downloadFile($url, array('destinationPath' => $filepath));
|
||||
}
|
||||
|
||||
}
|
||||
elseif ($this->virtual_status == self::$VIRTUAL_STATELESS)
|
||||
{
|
||||
$result = $filepath;
|
||||
}
|
||||
else {
|
||||
Log::addWarning('Virtual Status not set. Trying to blindly download vv DownloadHelper');
|
||||
$downloadHelper = DownloadHelper::getInstance();
|
||||
$url = $this->getURL();
|
||||
$result = $downloadHelper->downloadFile($url, array('destinationPath' => $filepath));
|
||||
}
|
||||
|
||||
if ($result == false)
|
||||
{
|
||||
$this->preventNextTry(__('Fatal Issue: Remote virtual file could not be downloaded for backup', 'shortpixel-image-optimiser'));
|
||||
Log::addError('Remote file download failed from : ' . $url . ' to: ' . $filepath, $this->getURL());
|
||||
$this->error_message = __('Remote file could not be downloaded' . $this->getFullPath(), 'shortpixel-image-optimiser');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->setVirtualToReal($filepath);
|
||||
}
|
||||
|
||||
return parent::createBackup();
|
||||
}
|
||||
|
||||
// @todo This is a breach of pattern to realize checking for changes to the main image path on conversion / duplicates.
|
||||
private function getMainFile()
|
||||
{
|
||||
$fs = \wpSPIO()->filesystem();
|
||||
return $fs->getMediaImage($this->id, true, true);
|
||||
}
|
||||
|
||||
} // class
|
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
namespace ShortPixel\Model;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
use ShortPixel\Controller\ResponseController as ResponseController;
|
||||
|
||||
|
||||
class ResponseModel
|
||||
{
|
||||
|
||||
// Identification for Item.
|
||||
public $item_id;
|
||||
public $item_type; // set by queue
|
||||
|
||||
// General item variables
|
||||
public $fileName;
|
||||
public $is_error;
|
||||
public $is_done;
|
||||
|
||||
public $apiStatus;
|
||||
public $fileStatus;
|
||||
|
||||
// Images being processed variables. From APIController
|
||||
public $tries;
|
||||
public $images_done;
|
||||
public $images_waiting;
|
||||
public $images_total;
|
||||
|
||||
public $issue_type; // Optional - if there is any issue to report.
|
||||
public $message; // This can be base text, but decision textually is within responsecontroller.
|
||||
public $action; // Custom Operations use this ( i.e. migrate )
|
||||
|
||||
// public $queueName;
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param $item_id int The attachment_id of the item in process
|
||||
* @param $item_type string item type: media or custom.
|
||||
*
|
||||
**/
|
||||
public function __construct($item_id, $item_type)
|
||||
{
|
||||
$this->item_id = $item_id;
|
||||
$this->item_type = $item_type; // media or custum
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user