diff --git a/.develop-todo b/.develop-todo
new file mode 100644
index 0000000..9b9f77f
--- /dev/null
+++ b/.develop-todo
@@ -0,0 +1,3 @@
+- create controller for webhook
+- create webhook-key settings
+- cache busting 2.0
diff --git a/HtmlcachePlugin.php b/HtmlcachePlugin.php
index 2914cad..40563d3 100644
--- a/HtmlcachePlugin.php
+++ b/HtmlcachePlugin.php
@@ -9,7 +9,7 @@
* @link https://github.com/craftapi
* @package HTMLCache
* @since 1.0.0
- * @version 1.0.5.1
+ * @version 1.0.6
*/
namespace Craft;
@@ -27,7 +27,7 @@ public function init()
include_once 'functions/htmlcache.php';
}
- if (!$this->isEnabled) {
+ if (!$this->isEnabled || !craft()->plugins->getPlugin('htmlcache')->getSettings()['enableGeneral']) {
\htmlcache_indexEnabled(false);
}
@@ -37,7 +37,7 @@ public function init()
craft()->htmlcache_htmlcache->createCacheFile();
});
craft()->on('entries.saveEntry', function (Event $event) {
- craft()->htmlcache_htmlcache->clearCacheFiles();
+ craft()->htmlcache_htmlcache->clearCacheFiles($event);
});
}
}
@@ -71,7 +71,7 @@ public function getDescription()
*/
public function getDocumentationUrl()
{
- return 'https://github.com/craftapi/htmlcache/blob/master/README.md';
+ return 'https://craftapi.github.io/htmlcache/';
}
/**
@@ -93,7 +93,7 @@ public function getReleaseFeedUrl()
*/
public function getVersion()
{
- return '1.0.5.1';
+ return '1.0.6';
}
/**
@@ -161,7 +161,7 @@ protected function defineSettings()
/**
* Returns the plugin settings
*
- * @return html
+ * @return string
*/
public function getSettingsHtml()
{
@@ -171,7 +171,7 @@ public function getSettingsHtml()
/**
* Process the settings and check if the index needs to be altered
*
- * @return function
+ * @return bool|null
*/
public function setSettings($values)
{
@@ -181,6 +181,12 @@ public function setSettings($values)
if (!empty($values['htmlcacheSettingsForm'])) {
// Write these settings to a .json file for offline reference
+ $values['enableCsrfProtection'] = craft()->config->get('enableCsrfProtection');
+ $values['csrfTokenName'] = craft()->config->get('csrfTokenName');
+ $values['csrfCookieDomain'] = craft()->config->get('defaultCookieDomain');
+ $values['csrfValidationKey'] = \Yii::app()->getGlobalState(\CSecurityManager::STATE_VALIDATION_KEY);
+ $values['csrfStateCookie'] = craft()->userSession->getStateCookie('');
+ $values['csrfSecureCookies'] = craft()->config->get('useSecureCookies');
$path = craft()->path->getStoragePath() . 'runtime' . DIRECTORY_SEPARATOR . 'htmlcache' . DIRECTORY_SEPARATOR;
IOHelper::ensureFolderExists($path);
$fp = fopen($path . 'settings.json', 'w+');
@@ -205,7 +211,7 @@ public function setSettings($values)
/**
* Set the default settings
*
- * @return function
+ * @return void
*/
public function onAfterInstall()
{
@@ -233,8 +239,8 @@ public function onBeforeUninstall()
*/
public function registerCachePaths()
{
- return array(
+ return [
craft()->htmlcache_htmlcache->clearCacheFiles() => Craft::t('Htmlcache cached pages')
- );
+ ];
}
}
diff --git a/README.md b/README.md
index 1b9e820..ccb84fa 100644
--- a/README.md
+++ b/README.md
@@ -7,49 +7,64 @@ This Craft plugin will generate static HTML files for your website. No need for
* Cache duration time can be set; defaults to 3600 seconds (1 hour)
* Active development and support through [Craft's Slack](https://craftcms.com/community)
* Make sure to check out the Roadmap (further below) and add your wishes/requirements through an issue
+* Respects redirects and CSRF-tokens
> This plugin is still in *beta*, so please test if this plugin works as expected on a *development* environment _before_ pushing to a production site.
Brought to you by [CraftAPI](https://github.com/craftapi)
## :beers: HTMLCache is Beerware
-I've decided to keep this project Open Source/Beerware and to not publish it as a "premium" plugin. If you like the project/find it usefull and you have a few bucks to spare, you're welcome to donate a beer :beer: through Pledgie!
+I've decided to keep this project Open Source/Beerware and to not publish it as a "premium" plugin. If you like the project/find it useful and you have a few bucks to spare, you're welcome to donate a beer :beer: through Pledgie!
+A big thank you for all donations so far!
+
## Installation
To install HTML Cache, follow these steps:
-1. Download & unzip the file and place the `htmlcache-master` directory into your `craft/plugins` directory
-2. -OR- do a `git clone https://github.com/craftapi/htmlcache.git` directly into your `craft/plugins` folder. You can then update it with `git pull`
-3. Install plugin in the Craft Control Panel under Settings > Plugins
-4. The plugin folder should be named `htmlcache` for Craft to see it. GitHub recently started appending `-master` (the branch name) to the name of the folder for zip file downloads.
+1. Download & unzip the file and place the `htmlcache` directory into your `craft/plugins` directory
+2. Install plugin in the Craft Control Panel under `Settings > Plugins`
+3. Set your preferred settings
HTML Cache works on Craft 2.5.x and Craft 2.6.x, both PHP 5.6 and 7.0
## HTML Cache Overview
-Creates a HTML Cached page for any non-cp GET request for the duration of one hour (configurable) or untill an entry has been updated. Will not serve a cached request when in DEV-mode
+Creates a HTML Cached page for any non-cp GET request for the duration of one hour (configurable) or until an entry has been updated. Will not serve a cached request when in DEV-mode
## Configuring HTML Cache
-After installing HTML Cache, you'll be redirected to the settings page.
+After installing HTML Cache, you'll be redirected to the settings page. Afterwards you can find the settings in the Craft Control Panel under `Settings > Plugins > HTMLCache Settings`
+
+If you've enabled the webhook, you can call the webhook at `https://yourdomain.dev/actions/htmlcache/webhook?key=XXXX`
## Using HTML Cache
-HTML Cache has a settings page where you can enable/disable both normal and ubercache. The ubercache alters the public/index.php file to include extra functionality before Craft gets initialised, eliminating the TTFB caused by Yii.
+HTML Cache has a settings page where you can enable/disable both normal and _ubercache_. The _ubercache_ alters the public/index.php file to include extra functionality before Craft gets initialised, eliminating the TTFB caused by Yii 1 and any slow databases (400+ queries per page?).
## HTML Cache Roadmap
-* Fix cached CSRF-requests
+* done: _Fix cached CSRF-requests_
* CP Widget with amount of cache files and size, plus a button to purge the cache directly
-* Move files inside _cached directory to storage/runtime directory as those permissions should work at all times
-* Cache bust by webhook
-* 1.1: Improve cache busting by checking the impact of an updated entry; do we really need to bust everything?
+* done: _Move files inside cached directory to storage/runtime directory as those permissions should work at all times_
+* done: _Cache bust by webhook_
+* done: _Improve cache busting by checking the impact of an updated entry; do we really need to bust everything?_
+* Implement Twig Tags to prevent pages from getting cached
## HTML Cache Changelog
+### 1.0.6 -- 2016.12.26
+
+* Fixes CSRF requests and redirects
+* Enables cache busting by webhook
+* Busts single cache instead of all pages
+
+### 1.0.5.1 -- 2016.12.15
+
+* Bugfixes and improvements
+
### 1.0.4-2 -- 2016.03.08
* Bugfix
diff --git a/functions/htmlcache.php b/functions/htmlcache.php
index 04f5ea7..a5a1bad 100644
--- a/functions/htmlcache.php
+++ b/functions/htmlcache.php
@@ -70,6 +70,9 @@ function htmlcache_indexEnabled($enabled = true)
function htmlcache_checkCache($direct = true)
{
+ if ($_SERVER['REQUEST_METHOD'] != 'GET') {
+ return false;
+ }
$file = htmlcache_filename(true);
if (file_exists($file)) {
if (file_exists($settingsFile = htmlcache_directory() . 'settings.json')) {
@@ -81,7 +84,10 @@ function htmlcache_checkCache($direct = true)
unlink($file);
return false;
}
- $content = file_get_contents($file);
+ $content = trim(file_get_contents($file));
+ if (empty($content)) {
+ return false;
+ }
// Check the content type
$isJson = false;
@@ -99,10 +105,68 @@ function htmlcache_checkCache($direct = true)
header('Content-type:application/json');
}
echo $content;
- } else {
+ }
+ else {
+ // Check for CSRF-tokens could be expected
+ if ($settings['enableCsrfProtection'] === true) {
+ // Check if CSRF-tokens are found in the body
+ if (stristr($content, $settings['csrfTokenName']) !== false) {
+ // do not cache this page for now, until csrf is stable
+ /*
+ $token = false;//@$_COOKIE[$settings['csrfTokenName']];
+ $sm = new SecurityManager();
+ $sm->setValidationKey($settings['csrfValidationKey']);
+ $nonce = $sm->generateRandomString(40, false);
+ if (!($token)) {
+
+ }
+ else {
+ // They have an existing CSRF cookie.
+ $value = ($token);
+
+ // serialize crap
+ if (strpos($value, ':') !== false && strpos($value, '"') !== false) {
+ $value = explode('"', $value)[1];
+ }
+
+ // It's a CSRF cookie that came from an authenticated request.
+ if (strpos($value, '|') !== false)
+ {
+ // Grab the existing nonce.
+ $parts = explode('|', $value);
+ $nonce = rtrim($parts[0], '";');
+ }
+ else
+ {
+ // It's a CSRF cookie from an unauthenticated request.
+ $nonce = $value;
+ }
+ }
+
+ setcookie(
+ $settings['csrfTokenName'],
+ $nonce,
+ time() + 3600,
+ '/',
+ $settings['csrfCookieDomain'] ? $settings['csrfCookieDomain'] : $_SERVER['HTTP_HOST'],
+ $settings['csrfSecureCookies'],
+ false
+ );
+
+ $content = preg_replace(
+ '/name="' . $settings['csrfTokenName'] . '" value="(.*)"/',
+ 'name="' . $settings['csrfTokenName'] . '" value="' . htmlentities($nonce) . '"',
+ $content
+ );
+ */
+ return false;
+ }
+ }
+
if ($direct) {
header('Content-type:text/html;charset=UTF-8');
}
+
// Output the content
echo $content;
}
@@ -114,4 +178,529 @@ function htmlcache_checkCache($direct = true)
}
return true;
}
+
+ // Shorthand and standalone class from Yii
+ class SecurityManager
+ {
+ const STATE_VALIDATION_KEY='Yii.CSecurityManager.validationkey';
+ const STATE_ENCRYPTION_KEY='Yii.CSecurityManager.encryptionkey';
+ /**
+ * @var array known minimum lengths per encryption algorithm
+ */
+ protected static $encryptionKeyMinimumLengths=array(
+ 'blowfish'=>4,
+ 'arcfour'=>5,
+ 'rc2'=>5,
+ );
+ /**
+ * @var boolean if encryption key should be validated
+ * @deprecated
+ */
+ public $validateEncryptionKey=true;
+ /**
+ * @var string the name of the hashing algorithm to be used by {@link computeHMAC}.
+ * See {@link http://php.net/manual/en/function.hash-algos.php hash-algos} for the list of possible
+ * hash algorithms. Note that if you are using PHP 5.1.1 or below, you can only use 'sha1' or 'md5'.
+ *
+ * Defaults to 'sha1', meaning using SHA1 hash algorithm.
+ * @since 1.1.3
+ */
+ public $hashAlgorithm='sha1';
+ /**
+ * @var mixed the name of the crypt algorithm to be used by {@link encrypt} and {@link decrypt}.
+ * This will be passed as the first parameter to {@link http://php.net/manual/en/function.mcrypt-module-open.php mcrypt_module_open}.
+ *
+ * This property can also be configured as an array. In this case, the array elements will be passed in order
+ * as parameters to mcrypt_module_open. For example, array('rijndael-128', '', 'ofb', '').
+ *
+ * Defaults to AES
+ *
+ * Note: MCRYPT_RIJNDAEL_192 and MCRYPT_RIJNDAEL_256 are *not* AES-192 and AES-256. The numbers of the MCRYPT_RIJNDAEL
+ * constants refer to the block size, whereas the numbers of the AES variants refer to the key length. AES is Rijndael
+ * with a block size of 128 bits and a key length of 128 bits, 192 bits or 256 bits. So to use AES in Mcrypt, you need
+ * MCRYPT_RIJNDAEL_128 and a key with 16 bytes (AES-128), 24 bytes (AES-192) or 32 bytes (AES-256). The other two
+ * Rijndael variants in Mcrypt should be avoided, because they're not standardized and have been analyzed much less
+ * than AES.
+ *
+ * @since 1.1.3
+ */
+ public $cryptAlgorithm='rijndael-128';
+ private $_validationKey;
+ private $_encryptionKey;
+ private $_mbstring;
+ public function init()
+ {
+ parent::init();
+ $this->_mbstring=extension_loaded('mbstring');
+ }
+ /**
+ * @return string a randomly generated private key.
+ * @deprecated in favor of {@link generateRandomString()} since 1.1.14. Never use this method.
+ */
+ protected function generateRandomKey()
+ {
+ return $this->generateRandomString(32);
+ }
+ /**
+ * @return string the private key used to generate HMAC.
+ * If the key is not explicitly set, a random one is generated and returned.
+ * @throws CException in case random string cannot be generated.
+ */
+ public function getValidationKey()
+ {
+ if($this->_validationKey!==null)
+ return $this->_validationKey;
+ else
+ {
+ if(($key=Yii::app()->getGlobalState(self::STATE_VALIDATION_KEY))!==null)
+ $this->setValidationKey($key);
+ else
+ {
+ if(($key=$this->generateRandomString(32,true))===false)
+ if(($key=$this->generateRandomString(32,false))===false)
+ throw new CException(Yii::t('yii',
+ 'CSecurityManager::generateRandomString() cannot generate random string in the current environment.'));
+ $this->setValidationKey($key);
+ Yii::app()->setGlobalState(self::STATE_VALIDATION_KEY,$key);
+ }
+ return $this->_validationKey;
+ }
+ }
+ /**
+ * @param string $value the key used to generate HMAC
+ * @throws CException if the key is empty
+ */
+ public function setValidationKey($value)
+ {
+ if(!empty($value))
+ $this->_validationKey=$value;
+ else
+ throw new CException(Yii::t('yii','CSecurityManager.validationKey cannot be empty.'));
+ }
+ /**
+ * @return string the private key used to encrypt/decrypt data.
+ * If the key is not explicitly set, a random one is generated and returned.
+ * @throws CException in case random string cannot be generated.
+ */
+ public function getEncryptionKey()
+ {
+ if($this->_encryptionKey!==null)
+ return $this->_encryptionKey;
+ else
+ {
+ if(($key=Yii::app()->getGlobalState(self::STATE_ENCRYPTION_KEY))!==null)
+ $this->setEncryptionKey($key);
+ else
+ {
+ if(($key=$this->generateRandomString(32,true))===false)
+ if(($key=$this->generateRandomString(32,false))===false)
+ throw new CException(Yii::t('yii',
+ 'CSecurityManager::generateRandomString() cannot generate random string in the current environment.'));
+ $this->setEncryptionKey($key);
+ Yii::app()->setGlobalState(self::STATE_ENCRYPTION_KEY,$key);
+ }
+ return $this->_encryptionKey;
+ }
+ }
+ /**
+ * @param string $value the key used to encrypt/decrypt data.
+ * @throws CException if the key is empty
+ */
+ public function setEncryptionKey($value)
+ {
+ $this->validateEncryptionKey($value);
+ $this->_encryptionKey=$value;
+ }
+ /**
+ * This method has been deprecated since version 1.1.3.
+ * Please use {@link hashAlgorithm} instead.
+ * @return string -
+ * @deprecated
+ */
+ public function getValidation()
+ {
+ return $this->hashAlgorithm;
+ }
+ /**
+ * This method has been deprecated since version 1.1.3.
+ * Please use {@link hashAlgorithm} instead.
+ * @param string $value -
+ * @deprecated
+ */
+ public function setValidation($value)
+ {
+ $this->hashAlgorithm=$value;
+ }
+ /**
+ * Encrypts data.
+ * @param string $data data to be encrypted.
+ * @param string $key the decryption key. This defaults to null, meaning using {@link getEncryptionKey EncryptionKey}.
+ * @return string the encrypted data
+ * @throws CException if PHP Mcrypt extension is not loaded or key is invalid
+ */
+ public function encrypt($data,$key=null)
+ {
+ if($key===null)
+ $key=$this->getEncryptionKey();
+ $this->validateEncryptionKey($key);
+ $module=$this->openCryptModule();
+ srand();
+ $iv=mcrypt_create_iv(mcrypt_enc_get_iv_size($module), MCRYPT_RAND);
+ mcrypt_generic_init($module,$key,$iv);
+ $encrypted=$iv.mcrypt_generic($module,$data);
+ mcrypt_generic_deinit($module);
+ mcrypt_module_close($module);
+ return $encrypted;
+ }
+ /**
+ * Decrypts data
+ * @param string $data data to be decrypted.
+ * @param string $key the decryption key. This defaults to null, meaning using {@link getEncryptionKey EncryptionKey}.
+ * @return string the decrypted data
+ * @throws CException if PHP Mcrypt extension is not loaded or key is invalid
+ */
+ public function decrypt($data,$key=null)
+ {
+ if($key===null)
+ $key=$this->getEncryptionKey();
+ $this->validateEncryptionKey($key);
+ $module=$this->openCryptModule();
+ $ivSize=mcrypt_enc_get_iv_size($module);
+ $iv=$this->substr($data,0,$ivSize);
+ mcrypt_generic_init($module,$key,$iv);
+ $decrypted=mdecrypt_generic($module,$this->substr($data,$ivSize,$this->strlen($data)));
+ mcrypt_generic_deinit($module);
+ mcrypt_module_close($module);
+ return rtrim($decrypted,"\0");
+ }
+ /**
+ * Opens the mcrypt module with the configuration specified in {@link cryptAlgorithm}.
+ * @throws CException if failed to initialize the mcrypt module or PHP mcrypt extension
+ * @return resource the mycrypt module handle.
+ * @since 1.1.3
+ */
+ protected function openCryptModule()
+ {
+ if(extension_loaded('mcrypt'))
+ {
+ if(is_array($this->cryptAlgorithm))
+ $module=@call_user_func_array('mcrypt_module_open',$this->cryptAlgorithm);
+ else
+ $module=@mcrypt_module_open($this->cryptAlgorithm,'', MCRYPT_MODE_CBC,'');
+ if($module===false)
+ throw new CException(Yii::t('yii','Failed to initialize the mcrypt module.'));
+ return $module;
+ }
+ else
+ throw new CException(Yii::t('yii','CSecurityManager requires PHP mcrypt extension to be loaded in order to use data encryption feature.'));
+ }
+ /**
+ * Prefixes data with an HMAC.
+ * @param string $data data to be hashed.
+ * @param string $key the private key to be used for generating HMAC. Defaults to null, meaning using {@link validationKey}.
+ * @return string data prefixed with HMAC
+ */
+ public function hashData($data,$key=null)
+ {
+ return $this->computeHMAC($data,$key).$data;
+ }
+ /**
+ * Validates if data is tampered.
+ * @param string $data data to be validated. The data must be previously
+ * generated using {@link hashData()}.
+ * @param string $key the private key to be used for generating HMAC. Defaults to null, meaning using {@link validationKey}.
+ * @return string the real data with HMAC stripped off. False if the data
+ * is tampered.
+ */
+ public function validateData($data,$key=null)
+ {
+ if (!is_string($data))
+ return false;
+ $len=$this->strlen($this->computeHMAC('test'));
+ if($this->strlen($data)>=$len)
+ {
+ $hmac=$this->substr($data,0,$len);
+ $data2=$this->substr($data,$len,$this->strlen($data));
+ return $this->compareString($hmac,$this->computeHMAC($data2,$key))?$data2:false;
+ }
+ else
+ return false;
+ }
+ /**
+ * Computes the HMAC for the data with {@link getValidationKey validationKey}. This method has been made public
+ * since 1.1.14.
+ * @param string $data data to be generated HMAC.
+ * @param string|null $key the private key to be used for generating HMAC. Defaults to null, meaning using
+ * {@link validationKey} value.
+ * @param string|null $hashAlgorithm the name of the hashing algorithm to be used.
+ * See {@link http://php.net/manual/en/function.hash-algos.php hash-algos} for the list of possible
+ * hash algorithms. Note that if you are using PHP 5.1.1 or below, you can only use 'sha1' or 'md5'.
+ * Defaults to null, meaning using {@link hashAlgorithm} value.
+ * @return string the HMAC for the data.
+ * @throws CException on unsupported hash algorithm given.
+ */
+ public function computeHMAC($data,$key=null,$hashAlgorithm=null)
+ {
+ if($key===null)
+ $key=$this->getValidationKey();
+ if($hashAlgorithm===null)
+ $hashAlgorithm=$this->hashAlgorithm;
+ if(function_exists('hash_hmac'))
+ return hash_hmac($hashAlgorithm,$data,$key);
+ if(0===strcasecmp($hashAlgorithm,'sha1'))
+ {
+ $pack='H40';
+ $func='sha1';
+ }
+ elseif(0===strcasecmp($hashAlgorithm,'md5'))
+ {
+ $pack='H32';
+ $func='md5';
+ }
+ else
+ {
+ throw new CException(Yii::t('yii','Only SHA1 and MD5 hashing algorithms are supported when using PHP 5.1.1 or below.'));
+ }
+ if($this->strlen($key)>64)
+ $key=pack($pack,$func($key));
+ if($this->strlen($key)<64)
+ $key=str_pad($key,64,chr(0));
+ $key=$this->substr($key,0,64);
+ return $func((str_repeat(chr(0x5C), 64) ^ $key) . pack($pack, $func((str_repeat(chr(0x36), 64) ^ $key) . $data)));
+ }
+ /**
+ * Generate a random ASCII string. Generates only [0-9a-zA-z_~] characters which are all
+ * transparent in raw URL encoding.
+ * @param integer $length length of the generated string in characters.
+ * @param boolean $cryptographicallyStrong set this to require cryptographically strong randomness.
+ * @return string|boolean random string or false in case it cannot be generated.
+ * @since 1.1.14
+ */
+ public function generateRandomString($length,$cryptographicallyStrong=true)
+ {
+ if(($randomBytes=$this->generateRandomBytes($length+2,$cryptographicallyStrong))!==false)
+ return strtr($this->substr(base64_encode($randomBytes),0,$length),array('+'=>'_','/'=>'~'));
+ return false;
+ }
+ /**
+ * Generates a string of random bytes.
+ * @param integer $length number of random bytes to be generated.
+ * @param boolean $cryptographicallyStrong whether to fail if a cryptographically strong
+ * result cannot be generated. The method attempts to read from a cryptographically strong
+ * pseudorandom number generator (CS-PRNG), see
+ * {@link https://en.wikipedia.org/wiki/Cryptographically_secure_pseudorandom_number_generator#Requirements Wikipedia}.
+ * However, in some runtime environments, PHP has no access to a CS-PRNG, in which case
+ * the method returns false if $cryptographicallyStrong is true. When $cryptographicallyStrong is false,
+ * the method always returns a pseudorandom result but may fall back to using {@link generatePseudoRandomBlock}.
+ * This method does not guarantee that entropy, from sources external to the CS-PRNG, was mixed into
+ * the CS-PRNG state between each successive call. The caller can therefore expect non-blocking
+ * behavior, unlike, for example, reading from /dev/random on Linux, see
+ * {@link http://eprint.iacr.org/2006/086.pdf Gutterman et al 2006}.
+ * @return boolean|string generated random binary string or false on failure.
+ * @since 1.1.14
+ */
+ public function generateRandomBytes($length,$cryptographicallyStrong=true)
+ {
+ $bytes='';
+ if(function_exists('openssl_random_pseudo_bytes'))
+ {
+ $bytes=openssl_random_pseudo_bytes($length,$strong);
+ if($this->strlen($bytes)>=$length && ($strong || !$cryptographicallyStrong))
+ return $this->substr($bytes,0,$length);
+ }
+ if(function_exists('mcrypt_create_iv') &&
+ ($bytes=mcrypt_create_iv($length, MCRYPT_DEV_URANDOM))!==false &&
+ $this->strlen($bytes)>=$length)
+ {
+ return $this->substr($bytes,0,$length);
+ }
+ if(($file=@fopen('/dev/urandom','rb'))!==false &&
+ ($bytes=@fread($file,$length))!==false &&
+ (fclose($file) || true) &&
+ $this->strlen($bytes)>=$length)
+ {
+ return $this->substr($bytes,0,$length);
+ }
+ $i=0;
+ while($this->strlen($bytes)<$length &&
+ ($byte=$this->generateSessionRandomBlock())!==false &&
+ ++$i<3)
+ {
+ $bytes.=$byte;
+ }
+ if($this->strlen($bytes)>=$length)
+ return $this->substr($bytes,0,$length);
+ if ($cryptographicallyStrong)
+ return false;
+ while($this->strlen($bytes)<$length)
+ $bytes.=$this->generatePseudoRandomBlock();
+ return $this->substr($bytes,0,$length);
+ }
+ /**
+ * Generate a pseudo random block of data using several sources. On some systems this may be a bit
+ * better than PHP's {@link mt_rand} built-in function, which is not really random.
+ * @return string of 64 pseudo random bytes.
+ * @since 1.1.14
+ */
+ public function generatePseudoRandomBlock()
+ {
+ $bytes='';
+ if (function_exists('openssl_random_pseudo_bytes')
+ && ($bytes=openssl_random_pseudo_bytes(512))!==false
+ && $this->strlen($bytes)>=512)
+ {
+ return $this->substr($bytes,0,512);
+ }
+ for($i=0;$i<32;++$i)
+ $bytes.=pack('S',mt_rand(0,0xffff));
+ // On UNIX and UNIX-like operating systems the numerical values in `ps`, `uptime` and `iostat`
+ // ought to be fairly unpredictable. Gather the non-zero digits from those.
+ foreach(array('ps','uptime','iostat') as $command) {
+ @exec($command,$commandResult,$retVal);
+ if(is_array($commandResult) && !empty($commandResult) && $retVal==0)
+ $bytes.=preg_replace('/[^1-9]/','',implode('',$commandResult));
+ }
+ // Gather the current time's microsecond part. Note: this is only a source of entropy on
+ // the first call! If multiple calls are made, the entropy is only as much as the
+ // randomness in the time between calls.
+ $bytes.=$this->substr(microtime(),2,6);
+ // Concatenate everything gathered, mix it with sha512. hash() is part of PHP core and
+ // enabled by default but it can be disabled at compile time but we ignore that possibility here.
+ return hash('sha512',$bytes,true);
+ }
+ /**
+ * Get random bytes from the system entropy source via PHP session manager.
+ * @return boolean|string 20-byte random binary string or false on error.
+ * @since 1.1.14
+ */
+ public function generateSessionRandomBlock()
+ {
+ ini_set('session.entropy_length',20);
+ if(ini_get('session.entropy_length')!=20)
+ return false;
+ // These calls are (supposed to be, according to PHP manual) safe even if
+ // there is already an active session for the calling script.
+ @session_start();
+ @session_regenerate_id();
+ $bytes=session_id();
+ if(!$bytes)
+ return false;
+ // $bytes has 20 bytes of entropy but the session manager converts the binary
+ // random bytes into something readable. We have to convert that back.
+ // SHA-1 should do it without losing entropy.
+ return sha1($bytes,true);
+ }
+ /**
+ * Returns the length of the given string.
+ * If available uses the multibyte string function mb_strlen.
+ * @param string $string the string being measured for length
+ * @return integer the length of the string
+ */
+ private function strlen($string)
+ {
+ return $this->_mbstring ? mb_strlen($string,'8bit') : strlen($string);
+ }
+ /**
+ * Returns the portion of string specified by the start and length parameters.
+ * If available uses the multibyte string function mb_substr
+ * @param string $string the input string. Must be one character or longer.
+ * @param integer $start the starting position
+ * @param integer $length the desired portion length
+ * @return string the extracted part of string, or FALSE on failure or an empty string.
+ */
+ private function substr($string,$start,$length)
+ {
+ return $this->_mbstring ? mb_substr($string,$start,$length,'8bit') : substr($string,$start,$length);
+ }
+
+ /**
+ * Checks if a key is valid for {@link cryptAlgorithm}.
+ * @param string $key the key to check
+ * @return boolean the validation result
+ * @throws CException if the supported key lengths of the cipher are unknown
+ */
+ protected function validateEncryptionKey($key)
+ {
+ if(is_string($key))
+ {
+ $cryptAlgorithm = is_array($this->cryptAlgorithm) ? $this->cryptAlgorithm[0] : $this->cryptAlgorithm;
+ $supportedKeyLengths=mcrypt_module_get_supported_key_sizes($cryptAlgorithm);
+ if($supportedKeyLengths)
+ {
+ if(!in_array($this->strlen($key),$supportedKeyLengths)) {
+ throw new CException(Yii::t('yii','Encryption key length can be {keyLengths}.',array('{keyLengths}'=>implode(',',$supportedKeyLengths))));
+ }
+ }
+ elseif(isset(self::$encryptionKeyMinimumLengths[$cryptAlgorithm]))
+ {
+ $minLength=self::$encryptionKeyMinimumLengths[$cryptAlgorithm];
+ $maxLength=mcrypt_module_get_algo_key_size($cryptAlgorithm);
+ if($this->strlen($key)<$minLength || $this->strlen($key)>$maxLength)
+ throw new CException(Yii::t('yii','Encryption key length must be between {minLength} and {maxLength}.',array('{minLength}'=>$minLength,'{maxLength}'=>$maxLength)));
+ }
+ else
+ throw new CException(Yii::t('yii','Failed to validate key. Supported key lengths of cipher not known.'));
+ }
+ else
+ throw new CException(Yii::t('yii','Encryption key should be a string.'));
+ }
+
+ /**
+ * Decrypts legacy ciphertext which was produced by the old, broken implementation of encrypt().
+ * @deprecated use only to convert data encrypted prior to 1.1.16
+ * @param string $data data to be decrypted.
+ * @param string $key the decryption key. This defaults to null, meaning the key should be loaded from persistent storage.
+ * @param string|array $cipher the algorithm to be used
+ * @return string the decrypted data
+ * @throws CException if PHP Mcrypt extension is not loaded
+ * @throws CException if the key is missing
+ */
+ public function legacyDecrypt($data,$key=null,$cipher='des')
+ {
+ if (!$key)
+ {
+ $key=Yii::app()->getGlobalState(self::STATE_ENCRYPTION_KEY);
+ if(!$key)
+ throw new CException(Yii::t('yii','No encryption key specified.'));
+ $key = md5($key);
+ }
+ if(extension_loaded('mcrypt'))
+ {
+ if(is_array($cipher))
+ $module=@call_user_func_array('mcrypt_module_open',$cipher);
+ else
+ $module=@mcrypt_module_open($cipher,'', MCRYPT_MODE_CBC,'');
+ if($module===false)
+ throw new CException(Yii::t('yii','Failed to initialize the mcrypt module.'));
+ }
+ else
+ throw new CException(Yii::t('yii','CSecurityManager requires PHP mcrypt extension to be loaded in order to use data encryption feature.'));
+ $derivedKey=$this->substr($key,0,mcrypt_enc_get_key_size($module));
+ $ivSize=mcrypt_enc_get_iv_size($module);
+ $iv=$this->substr($data,0,$ivSize);
+ mcrypt_generic_init($module,$derivedKey,$iv);
+ $decrypted=mdecrypt_generic($module,$this->substr($data,$ivSize,$this->strlen($data)));
+ mcrypt_generic_deinit($module);
+ mcrypt_module_close($module);
+ return rtrim($decrypted,"\0");
+ }
+ /**
+ * Performs string comparison using timing attack resistant approach.
+ * @see http://codereview.stackexchange.com/questions/13512
+ * @param string $expected string to compare.
+ * @param string $actual user-supplied string.
+ * @return boolean whether strings are equal.
+ */
+ public function compareString($expected,$actual)
+ {
+ $expected.="\0";
+ $actual.="\0";
+ $expectedLength=$this->strlen($expected);
+ $actualLength=$this->strlen($actual);
+ $diff=$expectedLength-$actualLength;
+ for($i=0;$i<$actualLength;$i++)
+ $diff|=(ord($actual[$i])^ord($expected[$i%$expectedLength]));
+ return $diff===0;
+ }
+ }
}
diff --git a/services/Htmlcache_HtmlcacheService.php b/services/Htmlcache_HtmlcacheService.php
index ad34407..d7224c7 100644
--- a/services/Htmlcache_HtmlcacheService.php
+++ b/services/Htmlcache_HtmlcacheService.php
@@ -23,9 +23,7 @@ public function checkForCacheFile()
}
$file = $this->getCacheFileName();
- if (file_exists($file)) {
- \htmlcache_checkCache(false);
-
+ if (file_exists($file) && \htmlcache_checkCache(false)) {
return craft()->end();
}
// Turn output buffering on
@@ -71,6 +69,9 @@ public function createCacheFile()
{
if ($this->canCreateCacheFile() && http_response_code() == 200) {
$content = ob_get_contents();
+ if (empty($content)) {
+ return false;
+ }
ob_end_flush();
$file = $this->getCacheFileName();
$fp = fopen($file, 'w+');
@@ -84,10 +85,13 @@ public function createCacheFile()
}
}
- public function clearCacheFiles()
+ public function clearCacheFiles(Event $event)
{
// @todo split between all/single cache file
- foreach (glob($this->getCacheFileDirectory() . '*.html') as $file) {
+ foreach (glob($this->getCacheFileDirectory() . '*') as $file) {
+ if (basename($file) == 'settings.json') {
+ continue;
+ }
unlink($file);
}
return true;