<?php

/**
 * Simple Machines Forum (SMF)
 *
 * @package SMF
 * @author Simple Machines https://www.simplemachines.org
 * @copyright 2023 Simple Machines and individual contributors
 * @license https://www.simplemachines.org/about/smf/license.php BSD
 *
 * @version 2.1.4
 */

namespace SMF\Cache\APIs;

use GlobIterator;
use FilesystemIterator;
use SMF\Cache\CacheApi;
use SMF\Cache\CacheApiInterface;

if (!defined('SMF'))
	die('No direct access...');

/**
 * Our Cache API class
 *
 * @package CacheAPI
 */
class FileBased extends CacheApi implements CacheApiInterface
{
	/**
	 * @var string The path to the current $cachedir directory.
	 */
	private $cachedir = null;

	/**
	 * {@inheritDoc}
	 */
	public function __construct()
	{
		parent::__construct();

		// Set our default cachedir.
		$this->setCachedir();
	}

	/**
	 * {@inheritDoc}
	 */
	public function isSupported($test = false)
	{
		$supported = is_writable($this->cachedir);

		if ($test)
			return $supported;

		return parent::isSupported() && $supported;
	}

	private function readFile($file)
	{
		if (($fp = @fopen($file, 'rb')) !== false)
		{
			if (!flock($fp, LOCK_SH))
			{
				fclose($fp);
				return false;
			}
			$string = '';
			while (!feof($fp))
				$string .= fread($fp, 8192);

			flock($fp, LOCK_UN);
			fclose($fp);

			return $string;
		}

		return false;
	}

	private function writeFile($file, $string)
	{
		if (($fp = fopen($file, 'cb')) !== false)
		{
			if (!flock($fp, LOCK_EX))
			{
				fclose($fp);
				return false;
			}
			ftruncate($fp, 0);
			$bytes = 0;
			$pieces = str_split($string, 8192);
			foreach ($pieces as $piece)
			{
				if (($val = fwrite($fp, $piece, 8192)) !== false)
					$bytes += $val;
				else
					return false;
			}
			fflush($fp);
			flock($fp, LOCK_UN);
			fclose($fp);

			return $bytes;
		}

		return false;
	}

	/**
	 * {@inheritDoc}
	 */
	public function connect()
	{
		return true;
	}

	/**
	 * {@inheritDoc}
	 */
	public function getData($key, $ttl = null)
	{
		$file = sprintf('%s/data_%s.cache',
			$this->cachedir,
			$this->prefix . strtr($key, ':/', '-_')
		);

		// SMF Data returns $value and $expired.  $expired has a unix timestamp of when this expires.
		if (file_exists($file) && ($raw = $this->readFile($file)) !== false)
		{
			if (($value = smf_json_decode($raw, true, false)) !== array() && isset($value['expiration']) && $value['expiration'] >= time())
				return $value['value'];
			else
				@unlink($file);
		}

		return null;
	}

	/**
	 * {@inheritDoc}
	 */
	public function putData($key, $value, $ttl = null)
	{
		$file = sprintf('%s/data_%s.cache',
			$this->cachedir,
			$this->prefix . strtr($key, ':/', '-_')
		);
		$ttl = $ttl !== null ? $ttl : $this->ttl;

		if ($value === null)
			@unlink($file);
		else
		{
			$cache_data = json_encode(
				array(
					'expiration' => time() + $ttl,
					'value' => $value
				),
				JSON_NUMERIC_CHECK
			);

			// Write out the cache file, check that the cache write was successful; all the data must be written
			// If it fails due to low diskspace, or other, remove the cache file
			if ($this->writeFile($file, $cache_data) !== strlen($cache_data))
			{
				@unlink($file);
				return false;
			}
			else
				return true;
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public function cleanCache($type = '')
	{
		// No directory = no game.
		if (!is_dir($this->cachedir))
			return;

		// Remove the files in SMF's own disk cache, if any
		$files = new GlobIterator($this->cachedir . '/' . $type . '*.cache', FilesystemIterator::NEW_CURRENT_AND_KEY);

		foreach ($files as $file => $info)
			unlink($this->cachedir . '/' . $file);

		// Make this invalid.
		$this->invalidateCache();

		return true;
	}

	/**
	 * {@inheritDoc}
	 */
	public function invalidateCache()
	{
		// We don't worry about $cachedir here, since the key is based on the real $cachedir.
		parent::invalidateCache();

		// Since SMF is file based, be sure to clear the statcache.
		clearstatcache();

		return true;
	}

	/**
	 * {@inheritDoc}
	 */
	public function cacheSettings(array &$config_vars)
	{
		global $context, $txt;

		$class_name = $this->getImplementationClassKeyName();
		$class_name_txt_key = strtolower($class_name);

		$config_vars[] = $txt['cache_'. $class_name_txt_key .'_settings'];
		$config_vars[] = array('cachedir', $txt['cachedir'], 'file', 'text', 36, 'cache_cachedir');

		if (!isset($context['settings_post_javascript']))
			$context['settings_post_javascript'] = '';

		if (empty($context['settings_not_writable']))
			$context['settings_post_javascript'] .= '
			$("#cache_accelerator").change(function (e) {
				var cache_type = e.currentTarget.value;
				$("#cachedir").prop("disabled", cache_type != "'. $class_name .'");
			});';
	}

	/**
	 * Sets the $cachedir or uses the SMF default $cachedir..
	 *
	 * @access public
	 * @param string $dir A valid path
	 * @return boolean If this was successful or not.
	 */
	public function setCachedir($dir = null)
	{
		global $cachedir;

		// If its invalid, use SMF's.
		if (is_null($dir) || !is_writable($dir))
			$this->cachedir = $cachedir;

		else
			$this->cachedir = $dir;
	}

	/**
	 * Gets the current $cachedir.
	 *
	 * @access public
	 * @return string the value of $ttl.
	 */
	public function getCachedir()
	{
		return $this->cachedir;
	}

	/**
	 * {@inheritDoc}
	 */
	public function getVersion()
	{
		return SMF_VERSION;
	}
}

?>