<?php
namespace W3TCL\Minify;

/**
 * Class Minify_Cache_File
 * @package Minify
 */

class Minify_Cache_File {
	private $_path = null;
	private $_exclude = null;
	private $_locking = null;
	private $_flushTimeLimit = null;
	private $_flush_path = null;
	
	public function __construct($path = '', $exclude = array(), $locking = false, $flushTimeLimit = 0, $flush_path = null) {
		if (! $path) {
			$path = self::tmp();
		}

		$this->_path = $path;
		$this->_exclude = $exclude;
		$this->_locking = $locking;
		$this->_flushTimeLimit = $flushTimeLimit;

		$this->_flush_path = (is_null($flush_path) ? $path : $flush_path);

		if (!file_exists($this->_path .'/index.html')) {
			if (!is_dir($this->_path))
				\W3TC\Util_File::mkdir_from_safe($this->_path, W3TC_CACHE_DIR);
			@file_put_contents($this->_path .'/index.html', '');
		}
	}

	/**
	 * Resolve a cache id to a path under the cache root.
	 *
	 * @param string $id Cache id (filename or relative nested key).
	 *
	 * @return string|false Absolute path under `_path`, or false when rejected.
	 *
	 * @since 2.10.3
	 */
	private function _path_for_id($id)
	{
		if (!is_string($id) || '' === $id || false !== strpos($id, "\0")) {
			return false;
		}

		$id_norm = str_replace('\\', '/', $id);
		if (false !== strpos($id_norm, '..')) {
			return false;
		}

		if ('/' === $id_norm[0] || preg_match('#^[a-zA-Z]:/#', $id_norm)) {
			return false;
		}

		$base = realpath($this->_path);
		if (false === $base) {
			return false;
		}

		$base_norm   = rtrim(str_replace('\\', '/', $base), '/');
		$base_prefix = $base_norm . '/';
		$candidate   = $base_prefix . $id_norm;

		$probe = $candidate;
		while (true) {
			$fs = realpath($probe);
			if (false !== $fs) {
				$resolved = str_replace('\\', '/', $fs);
				if ($resolved !== $base_norm && 0 !== strpos($resolved, $base_prefix)) {
					return false;
				}
				break;
			}

			$parent = dirname($probe);
			if ($parent === $probe || '' === $parent) {
				if (0 !== strpos(str_replace('\\', '/', $candidate), $base_prefix)) {
					return false;
				}
				break;
			}
			$probe = $parent;
		}

		return $candidate;
	}

	/**
	 * Write data to cache.
	 *
	 * @param string $id cache id (e.g. a filename)
	 *
	 * @param string $data
	 *
	 * @return bool success
	 */
	public function store($id, $data)
	{
		$path = $this->_path_for_id($id);
		if (false === $path) {
			return false;
		}

		$flag = $this->_locking ? LOCK_EX : null;

		if (is_file($path)) {
			@unlink($path);
		}

		if (!@file_put_contents($path, $data['content'], $flag)) {
			// retry with make dir
			\W3TC\Util_File::mkdir_from_safe(dirname($path), W3TC_CACHE_DIR);

			if (!@file_put_contents($path, $data['content'], $flag))
				return false;
		}

		if ( file_exists( $path . '_old' ) ) {
			@unlink($path . '_old');
		}
		if ( file_exists( $path . '_meta_old' ) ) {
			@unlink($path . '_meta_old');
		}

		$content = $data['content'];
		unset($data['content']);
		if (count($data) > 0)
			@file_put_contents($path . '_meta', serialize($data), $flag);

		// write control
		$read = $this->fetch($id);
		if (!isset($read['content']) || $content != $read['content']) {
			@unlink($path);

			return false;
		}
		return true;
	}

	/**
	 * Get the size of a cache entry
	 *
	 * @param string $id cache id (e.g. a filename)
	 *
	 * @return int|false Size in bytes, or false when the cache id is rejected or unreadable.
	 */
	public function getSize($id)
	{
		$path = $this->_path_for_id($id);
		if (false === $path) {
			return false;
		}

		return filesize($path);
	}

	/**
	 * Does a valid cache entry exist?
	 *
	 * @param string $id cache id (e.g. a filename)
	 *
	 * @param int $srcMtime mtime of the original source file(s)
	 *
	 * @return bool exists
	 */
	public function isValid($id, $srcMtime)
	{
		$file = $this->_path_for_id($id);
		if (false === $file) {
			return false;
		}

		return (is_file($file) && (filemtime($file) >= $srcMtime));
	}

	/**
	 * Send the cached content to output
	 *
	 * @param string $id cache id (e.g. a filename)
	 */
	public function display($id)
	{
		$path = $this->_path_for_id($id);
		if (false === $path) {
			return false;
		}

		$fp = @fopen($path, 'rb');

		if ($fp) {
			if ($this->_locking)
				@flock($fp, LOCK_SH);
			@fpassthru($fp);
			if ($this->_locking)
				@flock($fp, LOCK_UN);
			@fclose($fp);

			return true;
		}

		return false;
	}

	/**
	 * Fetch the cached content
	 *
	 * @param string $id cache id (e.g. a filename)
	 *
	 * @return string|false
	 */
	public function fetch($id)
	{
		$path = $this->_path_for_id($id);
		if (false === $path) {
			return false;
		}

		$data = @file_get_contents($path . '_meta');
		if ( ! empty( $data ) ) {
			$data = @unserialize($data);
			if (!is_array($data))
				$data = array();
		} else {
			$data = array();
		}

		if (is_readable($path)) {
			if ($this->_locking) {
				$fp = @fopen($path, 'rb');

				if ($fp) {
					@flock($fp, LOCK_SH);

					$ret = @stream_get_contents($fp);

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

					$data['content'] = $ret;
					return $data;
				}
			} else {
				$data['content'] = @file_get_contents($path);
				return $data;
			}
		} else {
			$path_old = $path . '_old';
			$too_old_time = time() - 30;

			$file_time = @filemtime($path_old);
			if ($file_time) {
				if ($file_time > $too_old_time) {
					// return old data
					$data['content'] = @file_get_contents($path_old);
					return $data;
				}

				@touch($path_old);
			}
		}

		return false;
	}

		/**
	 * Returns the OS-specific directory for temporary files
	 *
	 * @author Paul M. Jones <pmjones@solarphp.com>
	 * @license http://opensource.org/licenses/bsd-license.php BSD
	 * @link http://solarphp.com/trac/core/browser/trunk/Solar/Dir.php
	 *
	 * @return string
	 */
	protected static function _tmp()
	{
		// non-Windows system?
		if (strtolower(substr(PHP_OS, 0, 3)) != 'win') {
			$tmp = empty($_ENV['TMPDIR']) ? getenv('TMPDIR') : $_ENV['TMPDIR'];
			if ($tmp) {
				return $tmp;
			} else {
				return '/tmp';
			}
		}
		// Windows 'TEMP'
		$tmp = empty($_ENV['TEMP']) ? getenv('TEMP') : $_ENV['TEMP'];
		if ($tmp) {
			return $tmp;
		}
		// Windows 'TMP'
		$tmp = empty($_ENV['TMP']) ? getenv('TMP') : $_ENV['TMP'];
		if ($tmp) {
			return $tmp;
		}
		// Windows 'windir'
		$tmp = empty($_ENV['windir']) ? getenv('windir') : $_ENV['windir'];
		if ($tmp) {
			return $tmp;
		}
		// final fallback for Windows
		return getenv('SystemRoot') . '\\temp';
	}

	/**
	 * Flush cache
	 *
	 * @return bool
	 */
	public function flush() {
		@set_time_limit($this->_flushTimeLimit);

		return \W3TC\Util_File::emptydir($this->_flush_path, $this->_exclude);
	}

	/**
	 * Fetch the cache path used
	 *
	 * @return string
	 */
	public function getPath() {
		return $this->_path;
	}

	/**
	 * Returns size statistics about cache files
	 */
	public function get_stats_size($timeout_time)
	{
		$dir = @opendir($this->_path);

		$stats = array(
			'css' => array(
				'items' => 0,
				'original_length' => 0,
				'output_length' => 0
			),
			'js' => array(
				'items' => 0,
				'original_length' => 0,
				'output_length' => 0
			),
			'timeout_occurred' => false
		);

		if (!$dir)
			return $stats;

		$n = 0;
		while (!$stats['timeout_occurred'] &&
				($entry = @readdir($dir)) !== false) {
			$n++;
			if ($n % 1000 == 0)
				$stats['timeout_occurred'] |= (time() > $timeout_time);

			if (substr($entry, -8) == '.js_meta' || substr($entry, -13) == '.js_gzip_meta')
				$type = 'js';
			else if (substr($entry, -9) == '.css_meta' || substr($entry, -14) == '.css_gzip_meta')
				$type = 'css';
			else
				continue;

			$full_path = $this->_path . DIRECTORY_SEPARATOR . $entry;
			$data = @file_get_contents($full_path);
			if (!$data)
				continue;

			$data = @unserialize($data);
			if (!is_array($data) || !isset($data['originalLength']))
				continue;

			$stats[$type]['items']++;
			$stats[$type]['original_length'] += (int)$data['originalLength'];
			$stats[$type]['output_length'] +=
				@filesize(substr($full_path, 0, strlen($full_path) - 5));
		}

		@closedir($dir);
		return $stats;
	}
}
