diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..757fee3 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/.idea \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..399ed6c --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2017, Emagedev +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/add-to-nginx.conf b/add-to-nginx.conf new file mode 100644 index 0000000..faf5e32 --- /dev/null +++ b/add-to-nginx.conf @@ -0,0 +1,8 @@ + + +server { + location ~* .(css|js)$ { + rewrite "^(?.*)(?\.[\da-f]{8})\.(?[\d\w]{2,5})$" $file_request_uri.$ext last; + expires 31d; + } +} \ No newline at end of file diff --git a/app/code/community/Emagedev/CacheControl/Block/Html/Head.php b/app/code/community/Emagedev/CacheControl/Block/Html/Head.php new file mode 100755 index 0000000..2fa87ad --- /dev/null +++ b/app/code/community/Emagedev/CacheControl/Block/Html/Head.php @@ -0,0 +1,321 @@ + + */ + +/** + * Class Emagedev_CacheControl_Block_Html_Head + * + * Append crc hashsum codes to scripts and styles to prevent customer browser caching + */ +class Emagedev_CacheControl_Block_Html_Head extends Mage_Page_Block_Html_Head +{ + /** + * Append hash like %path%/%file%.%hash%.%ext% + */ + const METHOD_VERSION = 'version'; + + /** + * Append hash like %path%/%file%.%ext%?%hash% + */ + const METHOD_QUERY = 'query'; + + /** + * @var bool Check is cache for this block is enabled + */ + protected $_cacheEnabled; + + /** + * Version method recommended by RFC + * + * @var string Method which used to append hash + */ + protected $method = self::METHOD_VERSION; + + /** + * Get magento cache instance + * + * @return Mage_Core_Model_Cache + */ + protected function _getMagentoCache() + { + return Mage::app()->getCache(); + } + + /** + * Get cache key for hashed url + * + * @param string $name + * @param string $type + * + * @return string + */ + protected function _getCacheName($name, $type = 'tag') + { + return implode('_', array('head_tag', $type, $name)); + } + + protected function _isBlockCacheEnabled() + { + if (is_null($this->_cacheEnabled)) { + $this->_cacheEnabled = Mage::app()->useCache( + Mage_Core_Block_Abstract::CACHE_GROUP + ); + } + return $this->_cacheEnabled; + } + + /** + * Merge static and skin files of the same format into 1 set of HEAD directives or even into 1 directive + * + * Will attempt to merge into 1 directive, if merging callback is provided. In this case it will generate + * filenames, rather than render urls. + * The merger callback is responsible for checking whether files exist, merging them and giving result URL + * + * @param string $format - HTML element format for sprintf('', $src, $params) + * @param array $staticItems - array of relative names of static items to be grabbed from js/ folder + * @param array $skinItems - array of relative names of skin items to be found in skins according to design config + * @param callback $mergeCallback + * + * @return string + */ + protected function &_prepareStaticAndSkinElements( + $format, array $staticItems, array $skinItems, + $mergeCallback = null + ) { + $designPackage = Mage::getDesign(); + $baseJsUrl = Mage::getBaseUrl('js'); + $items = array(); + if ($mergeCallback && !is_callable($mergeCallback)) { + $mergeCallback = null; + } + + // get static files from the js folder, no need in lookups + foreach ($staticItems as $params => $rows) { + foreach ($rows as $name) { + $items[$params][] = $mergeCallback ? + Mage::getBaseDir() . DS . 'js' . DS . $name + : $baseJsUrl . $this->_appendJsFileHashsum($name); + } + } + + // lookup each file basing on current theme configuration + foreach ($skinItems as $params => $rows) { + foreach ($rows as $name) { + $items[$params][] = $mergeCallback + ? $designPackage->getFilename( + $name, array('_type' => 'skin') + ) + : $this->_appendSkinFileHashsum($name); + } + } + + $html = ''; + foreach ($items as $params => $rows) { + // attempt to merge + $mergedUrl = false; + if ($mergeCallback) { + $mergedUrl = call_user_func($mergeCallback, $rows); + $mergedUrl = $this->_appendHashToMergedFile( + $mergedUrl, $rows, $mergeCallback[1] + ); + } + // render elements + $params = trim($params); + $params = $params ? ' ' . $params : ''; + if ($mergedUrl) { + $html .= sprintf($format, $mergedUrl, $params); + } else { + foreach ($rows as $src) { + $html .= sprintf($format, $src, $params); + } + } + } + return $html; + } + + /** + * Append crc32 hashsum to js files + * + * @param string $name + * + * @return string + */ + protected function _appendJsFileHashsum($name) + { + $cache = $this->_getMagentoCache(); + $cacheKey = $this->_getCacheName('js', $name); + + if ($this->_isBlockCacheEnabled()) { + $url = $cache->load($cacheKey); + if ($url) { + return $url; + } + } + + $baseJsDir = Mage::getBaseDir() . DS . 'js'; + $file = $baseJsDir . DS . $name; + $url = $this->_appendFileHashToUrl($name, $file); + + if ($this->_isBlockCacheEnabled()) { + $cache->save( + $url, $cacheKey, array(Mage_Core_Block_Abstract::CACHE_GROUP) + ); + } + + return $url; + } + + /** + * Append crc32 hashsum to skin js/css + * + * @param string $name + * + * @return string + */ + protected function _appendSkinFileHashsum($name) + { + $cache = $this->_getMagentoCache(); + $cacheKey = $this->_getCacheName('skin', $name); + + if ($this->_isBlockCacheEnabled()) { + $url = $cache->load($cacheKey); + if ($url) { + return $url; + } + } + + $designPackage = Mage::getDesign(); + $url = $designPackage->getSkinUrl($name); + $file = $designPackage->getFilename( + trim($name, DS), array('_type' => 'skin') + ); + $url = $this->_appendFileHashToUrl($url, $file); + + if ($this->_isBlockCacheEnabled()) { + $cache->save( + $url, $cacheKey, array(Mage_Core_Block_Abstract::CACHE_GROUP) + ); + } + + return $url; + } + + /** + * Get merged file, append crc32 hashsum to url + * + * @param string $url merged file url + * @param string $files files to merge + * @param string $mergeCallbackFunction + * + * @return string + */ + protected function _appendHashToMergedFile( + $url, $files, $mergeCallbackFunction + ) { + $cache = $this->_getMagentoCache(); + $cacheKey = $this->_getCacheName('url', $url); + + if ($this->_isBlockCacheEnabled()) { + $hashedUrl = $cache->load($cacheKey); + if ($hashedUrl) { + return $hashedUrl; + } + } + + if (strpos($mergeCallbackFunction, 'Js') != false) { + $type = 'js'; + $extension = '.js'; + } else { + if (strpos($mergeCallbackFunction, 'Css') != false) { + $isSecure = Mage::app()->getRequest()->isSecure(); + $type = $isSecure ? 'css_secure' : 'css'; + $extension = '.css'; + } else { + return $url; + } + } + + $baseMediaUrl = Mage::getBaseUrl('media', $isSecure); + $hostname = parse_url($baseMediaUrl, PHP_URL_HOST); + $port = parse_url($baseMediaUrl, PHP_URL_PORT); + + $targetFilename + = md5(implode(',', $files) . "|{$hostname}|{$port}") . $extension; + $file = Mage::getBaseDir('media') . DS . $type . DS . $targetFilename; + $hashedUrl = $this->_appendFileHashToUrl($url, $file); + + if ($this->_isBlockCacheEnabled()) { + $cache->save( + $hashedUrl, $cacheKey, + array(Mage_Core_Block_Abstract::CACHE_GROUP) + ); + } + + return $hashedUrl; + } + + /** + * Append crc32 hash of $file as parameter to $url + * + * @param string $url + * @param string $file + * + * @return string + */ + protected function _appendFileHashToUrl($url, $file) + { + if (!file_exists($file)) { + return $url; + } + + $hash = hash_file('crc32', $file); + return $this->_appendHashToUrl($url, $hash); + } + + /** + * Append $hash as parameter to $url + * + * @param $url + * @param $hash + * + * @return string + */ + protected function _appendHashToUrl($url, $hash) + { + if ($this->method == self::METHOD_QUERY) { + return $hash ? sprintf('%s?%s', $url, $hash) : $url; + } else { + $matches = array(); + preg_match('/^(?.*)(?\.[\d\w]{2,5})(?$|\?.*)$/i', $url, $matches); + return $matches['uri'] . '.' . $hash . $matches['ext'] . $matches['query']; + } + } +} \ No newline at end of file diff --git a/app/code/community/Emagedev/CacheControl/etc/config.xml b/app/code/community/Emagedev/CacheControl/etc/config.xml new file mode 100755 index 0000000..1c2ec18 --- /dev/null +++ b/app/code/community/Emagedev/CacheControl/etc/config.xml @@ -0,0 +1,20 @@ + + + + + 0.9.0 + + + + + + Emagedev_CacheControl_Block + + + + Emagedev_CacheControl_Block_Html_Head + + + + + \ No newline at end of file diff --git a/app/etc/modules/Emagedev_CacheControl.xml b/app/etc/modules/Emagedev_CacheControl.xml new file mode 100644 index 0000000..c3a7e4d --- /dev/null +++ b/app/etc/modules/Emagedev_CacheControl.xml @@ -0,0 +1,12 @@ + + + + + true + community + + + + + + \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..c3dfbde --- /dev/null +++ b/composer.json @@ -0,0 +1,25 @@ +{ + "name": "emagedev/cachecontrol", + "description": "Add Hashes To Your Magento™ 1.9 Skin CSS, JS, And Merged Files To Prevent Browser Caching ", + "type": "magento-module", + "license": "BSD-3-Clause", + "version": "0.1.0", + "authors": [ + { + "name": "Dmitry Burlakov", + "email": "dantaeusb@icloud.com" + } + ], + "require": { + "magento-hackathon/magento-composer-installer": "*" + }, + "require-dev": { + "ecomdev/ecomdev_phpunit": "*" + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/Emagedev/CacheControl.git" + } + ] +} \ No newline at end of file diff --git a/modman b/modman new file mode 100644 index 0000000..865771f --- /dev/null +++ b/modman @@ -0,0 +1,3 @@ +app/code/community/Emagedev/CacheControl app/code/community/Emagedev/CacheControl +app/etc/modules/CacheControl.xml app/etc/modules/CacheControl.xml +LICENSE app/code/community/Emagedev/CacheControl/LICENSE \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..9b68b0f --- /dev/null +++ b/readme.md @@ -0,0 +1,3 @@ +# Add Hashes To Your Magento™ 1.9 Skin CSS, JS, And Merged Files To Prevent Browser Caching + +> **N.B. This is a early pre-release of module, use at your own risk.** \ No newline at end of file