/sparkplug/plugs/sparkpagecache/sparkpagecache.php
PHP | 397 lines | 276 code | 68 blank | 53 comment | 54 complexity | 8b521ed72f7eac8f5ffdb43365eb75a5 MD5 | raw file
Possible License(s): GPL-3.0, LGPL-2.1
- <?php
- /*
- Copyright 2009-2012 Sam Weiss
- All Rights Reserved.
- This file is part of Spark/Plug.
- Spark/Plug 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 3 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.
- You should have received a copy of the GNU General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
- if (!defined('spark/plug'))
- {
- header('HTTP/1.1 403 Forbidden');
- exit('<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"><html><head><title>403 Forbidden</title></head><body><h1>Forbidden</h1><p>You don\'t have permission to access the requested resource on this server.</p></body></html>');
- }
- // -----------------------------------------------------------------------------
- class SparkPageCache extends SparkApplication
- {
- private $_page_cache;
- private $_page_cache_active;
- private $_page_cache_static;
- private $_page_cache_reject_query_vars;
- private $_send_content_type;
- private $_send_last_modified;
- private $_send_etag;
- private $_expires_headers;
- private $_hasQueryVars;
- private $_cacheable;
- private $_ttl;
- private $_lmts;
- // --------------------------------------------------------------------------
- public function __construct($spark, $config = array())
- {
- parent::__construct($spark, $config);
-
- $cache_params = array_merge($this->config->get('cache', array()), $this->config->get('page_cache', array()));
- if (!empty($cache_params['active']) && method_exists($this, 'loadCacher'))
- {
- try
- {
- $this->_page_cache = $this->loadCacher($cache_params);
- }
- catch (Exception $e)
- {
- error_log('Could not load page cache: ' . $e->getMessage());
- }
-
- $this->_page_cache_active = true;
- $this->_page_cache_static = !empty($cache_params['static']);
- $this->_page_cache_reject_query_vars = isset($cache_params['reject_query_vars']) ? !empty($cache_params['reject_query_vars']) : true;
- $this->_send_content_type = !empty($cache_params['send_content_type']);
- $this->_send_last_modified = !empty($cache_params['send_last_modified']);
- $this->_send_etag = !empty($cache_params['send_etag']);
- $this->_expires_headers = @$cache_params['expires_headers'];
- }
-
- $this->_hasQueryVars = false;
- $this->_cacheable = NULL;
- $this->_ttl = NULL;
- $this->_lmts = NULL;
- // observe exception events so we don't cache error pages
- $this->observer->observe(array($this, 'handleException'), 'SparkApplication:run:exception');
- // observe cache flush requests
- $this->observer->observe(array($this, 'purgeCache'), 'SparkPageCache:request_flush');
- // observe cache disable requests
- $this->observer->observe(array($this, 'disableCache'), 'SparkPageCache:request_disable');
- // observe dispatch events so we can grab the params to check for query variables
- if ($this->_page_cache_reject_query_vars)
- {
- $this->observer->observe(array($this, 'checkQueryVars'), 'SparkApplication:dispatch:before');
- }
- }
- // --------------------------------------------------------------------------
- public function setDefaultTTL($ttl)
- {
- if ($this->_page_cache)
- {
- $this->_page_cache->setDefaultTimeout($ttl);
- }
- }
-
- // --------------------------------------------------------------------------
- public function getNameSpace()
- {
- return $this->_page_cache ? $this->_page_cache->getNameSpace() : '';
- }
-
- // --------------------------------------------------------------------------
- public function setNameSpace($namespace)
- {
- if ($this->_page_cache)
- {
- $this->_page_cache->setNameSpace($namespace);
- }
- }
-
- // --------------------------------------------------------------------------
- public function display($output, $contentType = 'text/html', $status = NULL, $headers = NULL)
- {
- $expDate = false;
-
- if ($this->_page_cache_active && $this->_page_cache && $this->isCacheable($ttl, $lmts))
- {
- if ($expDate = $this->checkExpires($contentType, $this->_expires_headers, $lifetime))
- {
- header('Expires: ' . $expDate);
- header('Cache-Control: max-age='.$lifetime*3600);
- }
- $preTag = '';
- if ($this->_send_content_type)
- {
- $preTag .= $contentType;
- }
- if ($this->_send_last_modified && isset($lmts))
- {
- $lmts = min($lmts, time());
- $preTag .= ';' . intval($lmts);
- header('Last-Modified: ' . gmstrftime('%a, %d %b %Y %H:%M:%S GMT', $lmts));
- }
- if ($this->_send_etag)
- {
- $etag = '"' . 'sparkplug-' . strlen($output) . '.' . crc32($output) . '"';
- $preTag .= ';' . $etag;
- header("ETag: $etag");
- }
- if (!empty($preTag))
- {
- $preTag .= ':';
- }
- if ($this->_page_cache_static)
- {
- $preTag = '';
- }
- $postTag = ($contentType === 'text/html') ? "\n<!-- served from page cache -->\n" : '';
- $this->_page_cache->set($this->generateCacheKey(), $preTag . $output . $postTag, $ttl);
- }
-
- if (!$expDate)
- {
- header('Cache-Control: max-age=0, no-cache, must-revalidate');
- }
-
- return parent::display($output, $contentType, $status, $headers);
- }
-
- // --------------------------------------------------------------------------
- public function setCacheable($cacheable, $ttl = NULL, $lmts = NULL)
- {
- // allow change from true to false, but not vice-versa
- if (!isset($this->_cacheable) || $this->_cacheable)
- {
- $this->_cacheable = $cacheable;
- $this->_ttl = $ttl;
- $this->_lmts = $lmts;
- }
- }
- // --------------------------------------------------------------------------
- public function disableCache($event = NULL)
- {
- $this->_page_cache_active = false;
- }
- // --------------------------------------------------------------------------
- public function checkQueryVars($event, $class, $method, $controller, $params)
- {
- $this->_hasQueryVars = !empty($params['qv']);
- }
- // --------------------------------------------------------------------------
- public function purgeCache($event = NULL)
- {
- if ($this->_page_cache)
- {
- $this->_page_cache->clear();
- }
- }
- // --------------------------------------------------------------------------
- public function handleException($event, $e)
- {
- // don't cache error pages as this can present a DOS vulnerability
- $this->setCacheable(false);
- }
- // --------------------------------------------------------------------------
- protected function runSafe()
- {
- // is the page cached?
-
- if ($this->_page_cache_active && $this->_page_cache && ($this->_requestMethod === SparkUtil::kRequestMethod_GET))
- {
- if (!$this->_page_cache_reject_query_vars || empty($_GET))
- {
- if ($page = $this->_page_cache->get($this->generateCacheKey()))
- {
- $this->sendCachedPage($page);
- return;
- }
- }
- }
-
- return parent::runSafe();
- }
-
- // --------------------------------------------------------------------------
- protected function runExit()
- {
- // do nothing - we don't want parent to send the SparkApplication:run:exit notification
- }
- // --------------------------------------------------------------------------
- private function isCacheable(&$ttl, &$lmts)
- {
- $ttl = $this->_ttl;
- $lmts = $this->_lmts;
- return ($this->_cacheable && ($this->_requestMethod === SparkUtil::kRequestMethod_GET) && !$this->_hasQueryVars);
- }
- // --------------------------------------------------------------------------
- private function sendCachedPage($page)
- {
- $contentType = 'text/html';
- if (!$this->_page_cache_static && ($this->_send_content_type || $this->_send_last_modified || $this->_send_etag))
- {
- if (($splitAt = strpos($page, ':')) !== false)
- {
- $preTag = substr($page, 0, $splitAt);
- $splits = explode(';', $preTag);
- $contentType = $splits[0];
-
- // send cache headers and decide if we can return a 304 status
-
- if ($this->_send_last_modified || $this->_send_etag)
- {
- if (isset($splits[2]))
- {
- if (is_numeric($splits[2]))
- {
- $lmts = $splits[2];
- $etag = $splits[1];
- }
- else
- {
- $lmts = $splits[1];
- $etag = $splits[2];
- }
- }
- elseif (isset($splits[1]))
- {
- if (is_numeric($splits[1]))
- {
- $lmts = $splits[1];
- }
- else
- {
- $etag = $splits[1];
- }
- }
-
- if (isset($lmts) && $this->_send_last_modified)
- {
- header('Last-Modified: ' . gmstrftime('%a, %d %b %Y %H:%M:%S GMT', $lmts));
- }
-
- if (!empty($etag) && $this->_send_etag)
- {
- header("ETag: $etag");
- }
-
- // can we send a "304 Not Modified"?
- //
- // prefer eTag because it is a more accurate indicator of page changes than a date
-
- if (!empty($etag) && ($match = isset($_SERVER['HTTP_IF_NONE_MATCH']) ? stripslashes($_SERVER['HTTP_IF_NONE_MATCH']) : ''))
- {
- if ($match === $etag)
- {
- while (@ob_end_clean());
- header('HTTP/1.1 304 Not Modified');
- if ($expDate = $this->checkExpires($contentType, $this->_expires_headers, $lifetime))
- {
- header('Expires: ' . $expDate);
- header('Cache-Control: max-age='.$lifetime*3600);
- }
- exit;
- }
- }
-
- elseif (isset($lmts) && ($match = isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? stripslashes($_SERVER['HTTP_IF_MODIFIED_SINCE']) : ''))
- {
- if (@strtotime($match) >= intval($lmts))
- {
- while (@ob_end_clean());
- header('HTTP/1.1 304 Not Modified');
- if ($expDate = $this->checkExpires($contentType, $this->_expires_headers, $lifetime))
- {
- header('Expires: ' . $expDate);
- header('Cache-Control: max-age='.$lifetime*3600);
- }
- exit;
- }
- }
- }
-
- $page = substr($page, $splitAt + 1);
- }
- }
-
- if ($expDate = $this->checkExpires($contentType, $this->_expires_headers, $lifetime))
- {
- header('Expires: ' . $expDate);
- header('Cache-Control: max-age='.$lifetime*3600);
- }
- else
- {
- header('Cache-Control: max-age=0, no-cache, must-revalidate');
- }
- parent::display($page, $contentType);
- }
- // --------------------------------------------------------------------------
- private function checkExpires($contentType, $expiresList, &$lifetime)
- {
- if (!empty($expiresList))
- {
- foreach ($expiresList as $type => $ttl)
- {
- $type = '#^' . str_replace('*', '.*', $type) . '$#';
- if (preg_match($type, $contentType))
- {
- $lifetime = $ttl;
- return gmdate('D, d M Y H:i:s', strtotime("+ {$ttl} hours")) . ' GMT';
- }
- }
- }
- return false;
- }
- // --------------------------------------------------------------------------
- private function generateCacheKey($key = NULL)
- {
- // Whether or not the application suppresses caching of pages with query parameters,
- // we always strip them if present to avoid denial-of-service vulnerability.
- // This means that all pages with the same base url will share the same cached page,
- // regardless of query parameters.
-
- return rtrim(!empty($key) ? $key : preg_replace('/\?.*$/', '', SparkUtil::request_uri()), '/');
- }
- // --------------------------------------------------------------------------
- }