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! Click here to lend your support to: Craft HTMLCache donations and make a donation at pledgie.com ! +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;