* @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\CMS\Cache\Controller; \defined('JPATH_PLATFORM') or die; use Joomla\CMS\Cache\Cache; use Joomla\CMS\Cache\CacheController; use Joomla\CMS\Document\HtmlDocument; use Joomla\CMS\Factory; /** * Joomla! Cache callback type object * * @since 1.7.0 */ class CallbackController extends CacheController { /** * Executes a cacheable callback if not found in cache else returns cached output and result * * @param callable $callback Callback or string shorthand for a callback * @param array $args Callback arguments * @param mixed $id Cache ID * @param boolean $wrkarounds True to use workarounds * @param array $woptions Workaround options * * @return mixed Result of the callback * * @since 1.7.0 */ public function get($callback, $args = array(), $id = false, $wrkarounds = false, $woptions = array()) { if (!\is_array($args)) { $referenceArgs = !empty($args) ? array(&$args) : array(); } else { $referenceArgs = &$args; } // Just execute the callback if caching is disabled. if (empty($this->options['caching'])) { return \call_user_func_array($callback, $referenceArgs); } if (!$id) { // Generate an ID $id = $this->_makeId($callback, $args); } $data = $this->cache->get($id); $locktest = (object) array('locked' => null, 'locklooped' => null); if ($data === false) { $locktest = $this->cache->lock($id); // If locklooped is true try to get the cached data again; it could exist now. if ($locktest->locked === true && $locktest->locklooped === true) { $data = $this->cache->get($id); } } if ($data !== false) { if ($locktest->locked === true) { $this->cache->unlock($id); } $data = unserialize(trim($data)); if ($wrkarounds) { echo Cache::getWorkarounds( $data['output'], array('mergehead' => $woptions['mergehead'] ?? 0) ); } else { echo $data['output']; } return $data['result']; } if ($locktest->locked === false && $locktest->locklooped === true) { // We can not store data because another process is in the middle of saving return \call_user_func_array($callback, $referenceArgs); } $coptions = array('modulemode' => 0); if (isset($woptions['modulemode']) && $woptions['modulemode'] == 1) { /** @var HtmlDocument $document */ $document = Factory::getDocument(); if (method_exists($document, 'getHeadData')) { $coptions['headerbefore'] = $document->getHeadData(); // Reset document head before rendering module. Module will cache only assets added by itself. $document->resetHeadData(); $document->getWebAssetManager()->reset(); $coptions['modulemode'] = 1; } } $coptions['nopathway'] = $woptions['nopathway'] ?? 1; $coptions['nohead'] = $woptions['nohead'] ?? 1; $coptions['nomodules'] = $woptions['nomodules'] ?? 1; ob_start(); ob_implicit_flush(false); $result = \call_user_func_array($callback, $referenceArgs); $output = ob_get_clean(); $data = array('result' => $result); if ($wrkarounds) { $data['output'] = Cache::setWorkarounds($output, $coptions); } else { $data['output'] = $output; } // Restore document head data and merge module head data. if ($coptions['modulemode'] == 1) { $moduleHeadData = $document->getHeadData(); $document->resetHeadData(); $document->mergeHeadData($coptions['headerbefore']); $document->mergeHeadData($moduleHeadData); } // Store the cache data $this->cache->store(serialize($data), $id); if ($locktest->locked === true) { $this->cache->unlock($id); } echo $output; return $result; } /** * Store data to cache by ID and group * * @param mixed $data The data to store * @param string $id The cache data ID * @param string $group The cache data group * @param boolean $wrkarounds True to use wrkarounds * * @return boolean True if cache stored * * @since 4.0.0 */ public function store($data, $id, $group = null, $wrkarounds = true) { $locktest = $this->cache->lock($id, $group); if ($locktest->locked === false && $locktest->locklooped === true) { // We can not store data because another process is in the middle of saving return false; } $result = $this->cache->store(serialize($data), $id, $group); if ($locktest->locked === true) { $this->cache->unlock($id, $group); } return $result; } /** * Generate a callback cache ID * * @param mixed $callback Callback to cache * @param array $args Arguments to the callback method to cache * * @return string MD5 Hash * * @since 1.7.0 */ protected function _makeId($callback, $args) { if (\is_array($callback) && \is_object($callback[0])) { $vars = get_object_vars($callback[0]); $vars[] = strtolower(\get_class($callback[0])); $callback[0] = $vars; } // A Closure can't be serialized, so to generate the ID we'll need to get its hash if ($callback instanceof \closure) { $hash = spl_object_hash($callback); return md5($hash . serialize(array($args))); } return md5(serialize(array($callback, $args))); } }