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

View File

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

View File

@ -0,0 +1,5 @@
<?php
require_once (__DIR__ . "/PackageLoader.php");
$loader = new ShortPixel\Build\PackageLoader();
$loader->load(__DIR__);

View File

@ -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"}}}

View File

@ -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" }
}
}

View File

@ -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']);
}
}
}

View File

@ -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

View File

@ -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>

View File

@ -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" }
}
}

View File

@ -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');
}
}
}

View File

@ -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});";
}
}

View File

@ -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 */

View File

@ -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"}

View File

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

View File

@ -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" }
}
}

View File

@ -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)
);
}
}

View File

@ -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.

View File

@ -0,0 +1,153 @@
Polyfill unserialize [![Build Status](https://travis-ci.org/dbrumann/polyfill-unserialize.svg?branch=master)](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
```

View File

@ -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());
}
}

View File

@ -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();
}
}

View File

@ -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
}
}

View File

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

View File

@ -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

View File

@ -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" }
}
}

View File

@ -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();
}

View File

@ -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
}
}

View File

@ -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);
}
}

View File

@ -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

View File

@ -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();
}
}

View File

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

View File

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

View File

@ -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