<?php use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log; class ShortPixelQueue { protected $ctrl; protected $settings; const BULK_TYPE_OPTIMIZE = 0; const BULK_TYPE_RESTORE = 1; const BULK_TYPE_CLEANUP = 2; const BULK_TYPE_CLEANUP_PENDING = 3; const BULK_NEVER = 0; const BULK_RUNNING = 1; const BULK_PAUSED = 2; const BULK_FINISHED = 3; public function __construct($controller, $settings) { $this->ctrl = $controller; $this->settings = $settings; } public function ShortPixelQueue($controller) { $this->__construct($controller); } public static function get() { $fp = self::openQ(LOCK_SH); if (!$fp) { return array(); } $itemsRaw = fgets($fp); $items = strlen($itemsRaw) ? self::parseQ($itemsRaw) : array(); self::closeQ($fp); return $items; } public static function set($items) { $fp = self::openQ(); if (!$fp) { return false; } fseek($fp, 0); ftruncate($fp, 0); fwrite($fp, implode(',', $items)); fflush($fp); self::closeQ($fp); return true; } public function apply($callable, $extra = false) { $fp = self::openQ(); if (!$fp) { return false; } $itemsRaw = fgets($fp); $items = strlen($itemsRaw) ? self::parseQ($itemsRaw) : array(); if ($extra) { $items = call_user_func($callable, $items, $extra); } else { $items = call_user_func($callable, $items); } fseek($fp, 0); ftruncate($fp, 0); fwrite($fp, implode(',', $items)); fflush($fp); self::closeQ($fp); return $items; } public static function testQ() { $fp = self::openQ(); if ($fp === false) { return false; } self::closeQ($fp); return true; } protected static function openQ($lock = LOCK_EX) { $queueName = SHORTPIXEL_UPLOADS_BASE . "/.shortpixel-q-" . get_current_blog_id(); $fp = @fopen($queueName, "r+"); if (!$fp) { $fp = @fopen($queueName, "w"); if (!$fp) { return false; } } flock($fp, $lock); return $fp; } protected static function closeQ($fp) { flock($fp, LOCK_UN); fclose($fp); } protected static function parseQ($items) { return explode(',', preg_replace("/[^0-9,C-]/", "", $items)); } public function skip($id) { if (is_array($this->settings->prioritySkip)) { $this->settings->prioritySkip = array_merge($this->settings->prioritySkip, array($id)); } else { $this->settings->prioritySkip = array($id); } } public function unskip($id) { $prioSkip = $this->settings->prioritySkip; $this->settings->prioritySkip = is_array($prioSkip) ? array_diff($prioSkip, array($id)) : array(); } public function allSkipped() { if (!is_array($this->settings->prioritySkip)) { return false; } count(array_diff($this->get(), $this->settings->prioritySkip)); } public function skippedCount() { return is_array($this->settings->prioritySkip) ? count($this->settings->prioritySkip) : 0; } public function isSkipped($id) { return is_array($this->settings->prioritySkip) && in_array($id, $this->settings->prioritySkip); } public function isPrio($id) { $prioItems = $this->get(); return is_array($prioItems) && in_array($id, $prioItems); } public function getSkipped() { return $this->settings->prioritySkip; } public function reverse() { $this->apply('array_reverse'); } protected function pushCallback($priorityQueue, $ID) { Log::addDebug("PUSH: Push ID {$ID} into queue " . json_encode($priorityQueue)); array_push($priorityQueue, $ID); $prioQ = array_unique($priorityQueue); return $prioQ; } public function push($ID) { $this->apply(array(&$this, 'pushCallback'), $ID); } protected function enqueueCallback($priorityQueue, $ID) { WPShortPixel::log("ENQUEUE: Enqueue ID {$ID} into queue " . json_encode($priorityQueue)); array_unshift($priorityQueue, $ID); $prioQ = array_unique($priorityQueue); WPShortPixel::log("ENQUEUE: Updated: " . json_encode($prioQ)); return $prioQ; } public function enqueue($ID) { $this->apply(array(&$this, 'enqueueCallback'), $ID); } public function getFirst($count = 1) { $priorityQueue = $this->get(); $count = min(count($priorityQueue), $count); return array_slice($priorityQueue, count($priorityQueue) - $count, $count); } public function getFromPrioAndCheck() { $idsPrio = $this->get(); $ids = array(); $removeIds = array(); for ($i = count($idsPrio) - 1, $cnt = 0; $i >= 0 && $cnt < 3; $i--) { if (!isset($idsPrio[$i])) { continue; } $id = $idsPrio[$i]; if (!$this->isSkipped($id) && $this->ctrl->isValidMetaId($id)) { $ids[] = $id; $cnt++; } elseif (!$this->isSkipped($id)) { $removeIds[] = $id; } } foreach ($removeIds as $rId) { WPShortPixel::log("HIP: Unfound ID {$rId} Remove from Priority Queue: " . json_encode($this->get())); $this->remove($rId); } return $ids; } public function remove($ID) { $fp = $this->openQ(); if (!$fp) { return false; } $items = fgets($fp); $items = self::parseQ($items); $items = is_array($items) ? $items : array(); $newItems = array(); $found = false; foreach ($items as $item) { if ($item != $ID) { $newItems[] = $item; } else { $found = true; } } if ($found) { fseek($fp, 0); ftruncate($fp, 0); fwrite($fp, implode(',', $newItems)); fflush($fp); } $this->closeQ($fp); return $found; } public function removeFromFailed($ID) { $failed = explode(",", $this->settings->failedImages); $key = array_search($ID, $failed); if ($key !== false) { unset($failed[$key]); $failed = array_values($failed); $this->settings->failedImages = implode(",", $failed); } } public function addToFailed($ID) { $failed = $this->settings->failedImages; if (!in_array($ID, explode(",", $failed))) { $this->settings->failedImages = (strlen($failed) ? $failed . "," : "") . $ID; } } public function getFailed() { $failed = $this->settings->failedImages; if (!strlen($failed)) { return array(); } $ret = explode(",", $failed); $fails = array(); foreach ($ret as $fail) { if (ShortPixelMetaFacade::isCustomQueuedId($fail)) { $meta = $this->ctrl->getSpMetaDao()->getMeta(ShortPixelMetaFacade::stripQueuedIdType($fail)); if ($meta) { $fails[] = (object) array("id" => ShortPixelMetaFacade::stripQueuedIdType($fail), "type" => ShortPixelMetaFacade::CUSTOM_TYPE, "meta" => $meta); } } else { $meta = wp_get_attachment_metadata($fail); if (!$meta || isset($meta["ShortPixelImprovement"]) && is_numeric($meta["ShortPixelImprovement"])) { $this->removeFromFailed($fail); } else { $fails[] = (object) array("id" => $fail, "type" => ShortPixelMetaFacade::MEDIA_LIBRARY_TYPE, "meta" => $meta); } } } return $fails; } public function bulkRunning() { return $this->settings->startBulkId > $this->settings->stopBulkId; } public function bulkPaused() { return $this->settings->cancelPointer; } public function bulkRan() { return $this->settings->bulkEverRan != 0; } public function processing() { return $this->bulkRunning() || count($this->get()); } public function getFlagBulkId() { return $this->settings->flagId; } public function getStartBulkId() { return $this->settings->startBulkId; } public function resetStartBulkId() { $this->setStartBulkId(ShortPixelMetaFacade::getMaxMediaId()); } public function setStartBulkId($start) { $this->settings->startBulkId = $start; } public function getStopBulkId() { return $this->settings->stopBulkId; } public function resetStopBulkId() { $this->settings->stopBulkId = ShortPixelMetaFacade::getMinMediaId(); } public function setBulkPreviousPercent() { $res = $this->settings->currentStats; $this->settings->bulkCount = $res["mainFiles"]; switch (0 + $this->ctrl->getCompressionType()) { case 2: $this->settings->bulkAlreadyDoneCount = $res["mainProcessedFiles"] - $res["mainProcLossyFiles"] - $res["mainProcLosslessFiles"]; break; case 1: $this->settings->bulkAlreadyDoneCount = $res["mainProcessedFiles"] - $res["mainProcGlossyFiles"] - $res["mainProcLosslessFiles"]; break; default: $this->settings->bulkAlreadyDoneCount = $res["mainProcessedFiles"] - $res["mainProcLossyFiles"] - $res["mainProcGlossyFiles"]; break; } if ($this->settings->processThumbnails) { $this->settings->bulkAlreadyDoneCount -= $res["mainUnprocessedThumbs"]; } $this->settings->bulkPreviousPercent = round($this->settings->bulkAlreadyDoneCount / ($this->settings->bulkCount ? $this->settings->bulkCount : 1) * 100); } public function getBulkToProcess() { if (!is_numeric($this->settings->bulkCount)) { $this->settings->bulkCount = 0; } if (!is_numeric($this->settings->bulkAlreadyDoneCount)) { $this->settings->bulkAlreadyDoneCount = 0; } return $this->settings->bulkCount - $this->settings->bulkAlreadyDoneCount; } public function flagBulkStart() { $this->settings->flagId = $this->settings->startBulkId; $this->settings->bulkProcessingStatus = 'running'; } public function setBulkType($type) { $this->settings->bulkType = $type; } public function getBulkType() { return $this->settings->bulkType; } public function getBulkTypeForDisplay() { $bulk = $this->settings->bulkType; $this->settings->bulkType = null; return $bulk; } public function startBulk($type = self::BULK_TYPE_OPTIMIZE) { Log::addDebug('startBulk 0'); $this->resetStartBulkId(); $this->resetStopBulkId(); $this->flagBulkStart(); $this->setBulkPreviousPercent(); $this->resetBulkCurrentlyProcessed(); $this->setBulkType($type); $this->settings->bulkEverRan = 1; } public function pauseBulk() { $cancelPointer = $this->settings->startBulkId; $bulkStartId = $this->getFlagBulkId(); $this->settings->cancelPointer = $cancelPointer; $this->settings->skipToCustom = NULL; Log::addDebug("PAUSE: Pointer = " . $this->settings->cancelPointer); foreach ($this->get() as $qItem) { if ($qItem < $bulkStartId) { $this->remove($qItem); } } $this->stopBulk(); } public function cancelBulk() { $this->pauseBulk(); Log::addDebug("STOP, delete pointer."); $this->settings->cancelPointer = NULL; } public function stopBulk() { $this->settings->startBulkId = ShortPixelMetaFacade::getMaxMediaId(); $this->settings->stopBulkId = $this->settings->startBulkId; $this->settings->bulkProcessingStatus = null; return $this->settings->bulkEverRan; } public function resumeBulk() { $this->settings->startBulkId = $this->settings->cancelPointer; $this->settings->stopBulkId = ShortPixelMetaFacade::getMinMediaId(); $this->settings->bulkProcessingStatus = 'running'; $this->settings->cancelPointer = null; WPShortPixel::log("Resumed: (pause says: " . $this->bulkPaused() . ") Start from: " . $this->settings->startBulkId . " to " . $this->settings->stopBulkId); } public function resetBulkCurrentlyProcessed() { $this->settings->bulkCurrentlyProcessed = 0; } public function incrementBulkCurrentlyProcessed() { $this->settings->bulkCurrentlyProcessed = $this->settings->bulkCurrentlyProcessed + 1; } public function markBulkComplete() { $this->settings->bulkProcessingStatus = null; $this->settings->cancelPointer = null; } public static function resetBulk() { delete_option('wp-short-pixel-bulk-type'); delete_option('bulkProcessingStatus'); delete_option('wp-short-pixel-cancel-pointer'); delete_option("wp-short-pixel-flag-id"); $startBulkId = $stopBulkId = ShortPixelMetaFacade::getMaxMediaId(); update_option('wp-short-pixel-query-id-stop', $startBulkId, 'no'); update_option('wp-short-pixel-query-id-start', $startBulkId, 'no'); delete_option("wp-short-pixel-bulk-previous-percent"); delete_option("wp-short-pixel-bulk-processed-items"); delete_option('wp-short-pixel-bulk-running-time'); delete_option('wp-short-pixel-last-bulk-start-time'); delete_option('wp-short-pixel-last-bulk-success-time'); delete_option("wp-short-pixel-bulk-processed-items"); delete_option("wp-short-pixel-bulk-count"); delete_option("wp-short-pixel-bulk-done-count"); } public static function resetPrio() { self::set(array()); } public function logBulkProgress() { $t = time(); $this->incrementBulkCurrentlyProcessed(); $successTime = $this->settings->lastBulkSuccessTime; if ($t - $successTime > 120) { $this->settings->bulkRunningTime += $successTime - $this->settings->lastBulkStartTime; $this->settings->lastBulkStartTime = $t; $this->settings->lastBulkSuccessTime = $t; } else { $this->settings->lastBulkSuccessTime = $t; } } public function getBulkPercent() { $previousPercent = $this->settings->bulkPreviousPercent; if ($this->getBulkToProcess() <= 0) { return $this->processing() ? 99 : 100; } $percent = $previousPercent + round($this->settings->bulkCurrentlyProcessed / $this->getBulkToProcess() * (100 - $previousPercent)); return min(99, $percent); } public function getDeltaBulkPercent() { return $this->getBulkPercent() - $this->settings->bulkPreviousPercent; } public function getTimeRemaining() { $p = $this->getBulkPercent(); $pAlready = $this->settings->bulkCount == 0 ? 0 : round($this->settings->bulkAlreadyDoneCount / $this->settings->bulkCount * 100); if ($p - $pAlready == 0) { return 0; } return round((100 - $p) / ($p - $pAlready) * ($this->settings->bulkRunningTime + $this->settings->lastBulkSuccessTime - $this->settings->lastBulkStartTime) / 60); } }