<?php /* Question2Answer by Gideon Greenspan and contributors http://www.question2answer.org/ File: qa-include/Q2A/Storage/FileCache.php Description: File-based driver for caching system. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. More about this license: http://www.question2answer.org/license.php */ /** * Caches data (typically from database queries) to the filesystem. */ class Q2A_Storage_FileCacheDriver { private $enabled = false; private $error; private $cacheDir; /** * Creates a new FileCache instance and checks we can write to the cache directory. * @param array $config Configuration data, including cache storage directory. */ public function __construct($config) { if (!$config['enabled']) { return; } if (isset($config['dir'])) { $this->cacheDir = realpath($config['dir']); if (!is_writable($this->cacheDir)) { $this->error = qa_lang_html_sub('admin/caching_dir_error', $config['dir']); } elseif (strpos($this->cacheDir, realpath($_SERVER['DOCUMENT_ROOT'])) === 0 || strpos($this->cacheDir, realpath(QA_BASE_DIR)) === 0) { // check the folder is outside the public root - checks against server root and Q2A root, in order to handle symbolic links $this->error = qa_lang_html_sub('admin/caching_dir_public', $config['dir']); } } else { $this->error = qa_lang_html('admin/caching_dir_missing'); } $this->enabled = empty($this->error); } /** * Get the cached data for the supplied key. * @param string $key The unique cache identifier. * @return string The cached data, or null otherwise. */ public function get($key) { if (!$this->enabled) { return null; } $file = $this->getFilename($key); if (is_readable($file)) { $lines = file($file, FILE_IGNORE_NEW_LINES); $actualKey = array_shift($lines); // double check this is the correct data if ($key === $actualKey) { $expiry = array_shift($lines); if (is_numeric($expiry) && time() < $expiry) { $encData = implode("\n", $lines); // decode data, ignoring any notices $data = @unserialize($encData); if ($data !== false) { return $data; } } } } return null; } /** * Store something in the cache along with the key and expiry time. Data gets 'serialized' to a string before storing. * @param string $key The unique cache identifier. * @param mixed $data The data to cache (in core Q2A this is usually an array). * @param int $ttl Number of minutes for which to cache the data. * @return bool Whether the file was successfully cached. */ public function set($key, $data, $ttl) { $success = false; $ttl = (int) $ttl; if ($this->enabled && $ttl > 0) { $encData = serialize($data); $expiry = time() + ($ttl * 60); $cache = $key . "\n" . $expiry . "\n" . $encData; $file = $this->getFilename($key); $dir = dirname($file); if (is_dir($dir) || mkdir($dir, 0777, true)) { $success = file_put_contents($file, $cache) !== false; } } return $success; } /** * Whether caching is available. * @return bool */ public function isEnabled() { return $this->enabled; } /** * Get the last error. * @return string */ public function getError() { return $this->error; } /** * Generates filename for cache key, of the form `1/23/123abc` * @param string $key The unique cache key. * @return string */ private function getFilename($key) { $filename = sha1($key); return $this->cacheDir . '/' . substr($filename, 0, 1) . '/' . substr($filename, 1, 2) . '/' . $filename; } }