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
|
Reference in New Issue
Block a user