├── .gitignore ├── .travis.yml ├── README.md ├── composer.json ├── phpunit.xml ├── public └── .gitkeep ├── src ├── Themonkeys │ └── Cachebuster │ │ ├── AssetURLGenerator.php │ │ ├── Cachebuster.php │ │ ├── CachebusterServiceProvider.php │ │ ├── SessionCookiesStripper.php │ │ └── StripSessionCookiesFilter.php ├── config │ ├── .gitkeep │ └── config.php ├── lang │ └── .gitkeep ├── migrations │ └── .gitkeep └── views │ └── .gitkeep └── tests └── .gitkeep /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.phar 3 | composer.lock 4 | .DS_Store -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.3 5 | - 5.4 6 | 7 | before_script: 8 | - curl -s http://getcomposer.org/installer | php 9 | - php composer.phar install --dev 10 | 11 | script: phpunit -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![The Monkeys](http://www.themonkeys.com.au/img/monkey_logo.png) 2 | 3 | 4 | Laravel Cachebuster 5 | =================== 6 | 7 | Adds MD5 hashes to the URLs of your application's assets, so when they change, their URL changes. URLs contained in your 8 | css files are transformed automatically; other URLs (such as those referenced via ` 156 | ``` 157 | 158 | ...will look like this to your users: 159 | 160 | ```HTML 161 | 162 | ``` 163 | 164 | Or if you've configured a CDN it might look like: 165 | 166 | ```HTML 167 | 168 | ``` 169 | 170 | The same goes for `` tags: 171 | 172 | ```HTML 173 | 174 | ``` 175 | 176 | will look like this to your users: 177 | 178 | ```HTML 179 | 180 | ``` 181 | 182 | The final piece of the puzzle is your css: 183 | 184 | ```HTML 185 | 186 | ``` 187 | 188 | comes out looking like this: 189 | 190 | ```HTML 191 | 192 | ``` 193 | 194 | Some real magic happens here - all the URLs inside your CSS file (images, fonts etc.) are automatically passed through 195 | the cachebuster, so they now have hashes in their filenames too. Open the CSS file in your browser and have a look! 196 | 197 | ### Absolute URLs 198 | 199 | Sometimes you might want to specify an absolute URL, for example in an OpenGraph meta tag. That's easy: 200 | 201 | ```HTML 202 | 203 | ``` 204 | 205 | might come out as: 206 | 207 | ```HTML 208 | 209 | ``` 210 | 211 | This uses Laravel's built-in URL generators so the URLs will be generated depending on your environment. 212 | 213 | 214 | 215 | Contribute 216 | ---------- 217 | 218 | In lieu of a formal styleguide, take care to maintain the existing coding style. 219 | 220 | License 221 | ------- 222 | 223 | MIT License 224 | (c) [The Monkeys](http://www.themonkeys.com.au/) 225 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "themonkeys/cachebuster", 3 | "description": "Adds MD5 hashes to the URLs of your application's assets, so when they change, their URL changes.", 4 | "homepage": "http://github.com/TheMonkeys/laravel-cachebuster", 5 | "keywords": ["assets", "cachebuster", "cdn", "laravel"], 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "themonkeys", 10 | "email": "developers@themonkeys.com.au" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=5.4.0", 15 | "illuminate/support": "4.*|5.*", 16 | "symfony/http-kernel": "2.*|3.*|4.*" 17 | }, 18 | "autoload": { 19 | "classmap": [ 20 | "src/migrations" 21 | ], 22 | "psr-0": { 23 | "Themonkeys\\Cachebuster": "src/" 24 | } 25 | }, 26 | "minimum-stability": "dev" 27 | } 28 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests/ 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/The-Monkeys-and-MAUD/laravel-cachebuster/a0ee0e1487ff7a115b20278fd3b088390b648b76/public/.gitkeep -------------------------------------------------------------------------------- /src/Themonkeys/Cachebuster/AssetURLGenerator.php: -------------------------------------------------------------------------------- 1 | cachebusted($asset); 25 | 26 | $base = Config::get("cachebuster.cdn"); 27 | if ($base === '' && $absolute) { 28 | $base = URL::to('/'); 29 | } 30 | return $base . $url; 31 | } 32 | 33 | public function cachebusted($asset) { 34 | $url = $asset; 35 | 36 | if (Config::get("cachebuster.enabled")) { 37 | $md5 = $this->md5($url); 38 | 39 | 40 | if ($md5) { 41 | $parts = pathinfo($url); 42 | $dirname = ends_with($parts['dirname'], '/') ? $parts['dirname'] : $parts['dirname'] . '/'; 43 | $url = "{$dirname}{$parts['filename']}-$md5.{$parts['extension']}"; 44 | } 45 | } 46 | 47 | return $url; 48 | } 49 | 50 | public function md5($asset) { 51 | 52 | 53 | $expiry = Config::get('cachebuster.expiry'); 54 | $self = $this; 55 | $calculate = function() use($asset, $self) { 56 | $path = public_path() . DIRECTORY_SEPARATOR . $self->map_path($asset); 57 | if (File::exists($path) && File::isFile($path)) { 58 | return md5_file($path); 59 | } else { 60 | throw new \Exception("Asset '$path' not found"); 61 | } 62 | }; 63 | if ($expiry) { 64 | return Cache::remember('url.md5.' . $asset, $expiry, $calculate); 65 | } else { 66 | return $calculate(); 67 | } 68 | } 69 | 70 | /** 71 | * Loads the css file at the given URL, replaces all urls within it to cachebusted CDN urls, 72 | * and returns the resulting css source code as a Response object suitable for the Laravel router. 73 | * @param $url 74 | */ 75 | public function css($url) { 76 | if (Session::isStarted() && Session::has('flash.old')) { 77 | Session::reflash(); // in case any flash data would have been lost here 78 | } 79 | 80 | // strip out cachebuster from the url, if necessary 81 | $url = $this->map_path($url); 82 | $public = public_path(); 83 | $path = $public . DIRECTORY_SEPARATOR . $url; 84 | if (File::exists($path)) { 85 | $source = File::get($path); 86 | $base = realpath(dirname($path)); 87 | 88 | // search for url('*') and replace with processed url 89 | $self = $this; 90 | if (Config::get("cachebuster.enabled")) { 91 | $source = preg_replace_callback('/url\\((["\']?)([^\\)\'"\\?]+)((\\?[^\\)\'"]+)?[\'"]?)\\)/', function ($matches) use ($base, $public, $self) { 92 | $url = $matches[2]; 93 | $qs = $matches[3]; 94 | 95 | // determine the absolute path of the given URL (resolve ../ etc against the path to the css file) 96 | if (substr($url, 0, 1) != '/') { 97 | $abs = realpath($base . '/' . $url); 98 | if (File::exists($abs) && starts_with($abs, $public)) { 99 | $url = substr($abs, strlen($public)); 100 | } 101 | } 102 | // if the url is absolute, we can process; otherwise, have to leave it alone 103 | $replacement = $matches[0]; 104 | if (substr($url, 0, 1) == '/') { 105 | $replacement = 'url(' . $matches[1] . $self->url($url) . $matches[3] . ')'; 106 | } 107 | return $replacement; 108 | 109 | }, $source); 110 | } 111 | 112 | return Response::make( 113 | $source, 114 | 200, 115 | array( 116 | 'Content-Type' => 'text/css', 117 | ) 118 | ); 119 | 120 | } else { 121 | App::abort(404, 'Page not found'); 122 | } 123 | } 124 | 125 | protected function strip_cachebuster($url) { 126 | return preg_replace('/-[0-9a-f]{32}\./', '.', $url); 127 | } 128 | 129 | public function map_path($url) { 130 | $url = '/' . preg_replace(';(^/+|#.*$);', '', $this->strip_cachebuster($url)); 131 | foreach (Config::get('cachebuster.path_maps') as $from => $to) { 132 | if (starts_with($url, $from)) { 133 | $part = substr($url, strlen($from)); 134 | if (starts_with($part, '/')) { 135 | $part = substr($part, 1); 136 | } 137 | if (ends_with($to, '/')) { 138 | $to = substr($to, 0, strlen($to) - 1); 139 | } 140 | $url = $to . '/' . $part; 141 | } 142 | } 143 | if (starts_with($url, '/')) { 144 | $url = substr($url, 1); 145 | } 146 | return $url; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/Themonkeys/Cachebuster/Cachebuster.php: -------------------------------------------------------------------------------- 1 | package('themonkeys/cachebuster'); 23 | 24 | $rc = new \ReflectionClass($this->app); 25 | if ($rc->hasMethod('close')) { 26 | $this->app->close('cachebuster.StripSessionCookiesFilter'); 27 | } 28 | 29 | $configPath = realpath(__DIR__ . '/../..'); 30 | 31 | $this->publishes([ 32 | $configPath . "/config/config.php" => config_path('cachebuster.php'), 33 | ]); 34 | 35 | 36 | } 37 | 38 | /** 39 | * Register the service provider. 40 | * 41 | * @return void 42 | */ 43 | public function register() 44 | { 45 | $this->app->singleton('cachebuster.url', function () { 46 | return new AssetURLGenerator(); 47 | }); 48 | $this->app->singleton('cachebuster.StripSessionCookiesFilter', function ($app) { 49 | return new StripSessionCookiesFilter($app); 50 | }); 51 | $rc = new \ReflectionClass($this->app); 52 | if ($rc->hasMethod('middleware')) { 53 | $this->app->middleware(function($app) { 54 | return new SessionCookiesStripper($app, App::make('cachebuster.StripSessionCookiesFilter')); 55 | }); 56 | } 57 | } 58 | 59 | /** 60 | * Get the services provided by the provider. 61 | * 62 | * @return array 63 | */ 64 | public function provides() 65 | { 66 | return array(); 67 | } 68 | 69 | } -------------------------------------------------------------------------------- /src/Themonkeys/Cachebuster/SessionCookiesStripper.php: -------------------------------------------------------------------------------- 1 | cacheControl = $copy->cacheControl; 15 | $this->headers = $copy->headers; 16 | $this->computedCacheControl = $copy->computedCacheControl; 17 | $this->headerNames = $copy->headerNames; 18 | // don't copy cookies 19 | 20 | } 21 | 22 | public function setCookie(Cookie $cookie) 23 | { 24 | // do nothing 25 | } 26 | 27 | } 28 | 29 | class SessionCookiesStripper implements HttpKernelInterface { 30 | 31 | /** 32 | * The wrapped kernel implementation. 33 | * 34 | * @var \Symfony\Component\HttpKernel\HttpKernelInterface 35 | */ 36 | protected $app; 37 | 38 | /** 39 | * The wrapped filter 40 | * 41 | * @var StripSessionCookiesFilter 42 | */ 43 | private $filter; 44 | 45 | /** 46 | * Create a new SessionCookiesStripper instance. 47 | * 48 | * @param \Symfony\Component\HttpKernel\HttpKernelInterface $app 49 | * @param StripSessionCookiesFilter $filter 50 | * @return void 51 | */ 52 | public function __construct(HttpKernelInterface $app, StripSessionCookiesFilter $filter) 53 | { 54 | $this->app = $app; 55 | $this->filter = $filter; 56 | } 57 | 58 | /** 59 | * Handles a Request to convert it to a Response. 60 | * 61 | * When $catch is true, the implementation must catch all exceptions 62 | * and do its best to convert them to a Response instance. 63 | * 64 | * @param \Symfony\Component\HttpFoundation\Request $request A Request instance 65 | * @param integer $type The type of the request 66 | * (one of HttpKernelInterface::MASTER_REQUEST or HttpKernelInterface::SUB_REQUEST) 67 | * @param Boolean $catch Whether to catch exceptions or not 68 | * 69 | * @return Response A Response instance 70 | * 71 | * @throws \Exception When an Exception occurs during processing 72 | * 73 | * @api 74 | */ 75 | public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true) 76 | { 77 | $response = $this->app->handle($request, $type, $catch); 78 | if ($this->filter->matches($request)) { 79 | $this->filter->filter($request, null); 80 | // wrap the response so it refuses any extra cookies 81 | $response->headers = new NoCookiesResponseHeaderBag($response->headers); 82 | } 83 | return $response; 84 | } 85 | } -------------------------------------------------------------------------------- /src/Themonkeys/Cachebuster/StripSessionCookiesFilter.php: -------------------------------------------------------------------------------- 1 | patterns []= $pattern; 10 | } 11 | 12 | public function matches($request) { 13 | $url = Request::path(); 14 | foreach ($this->patterns as $pattern) { 15 | if (preg_match($pattern, $url)) { 16 | return true; 17 | } 18 | } 19 | return false; 20 | } 21 | 22 | public function filter($request, $response = null) { 23 | if ($this->matches($request)) { 24 | header_remove('Set-Cookie'); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/config/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/The-Monkeys-and-MAUD/laravel-cachebuster/a0ee0e1487ff7a115b20278fd3b088390b648b76/src/config/.gitkeep -------------------------------------------------------------------------------- /src/config/config.php: -------------------------------------------------------------------------------- 1 | env('CACHEBUSTER_CDN', ''), 19 | 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Cached MD5 expiry 24 | |-------------------------------------------------------------------------- 25 | | 26 | | Cache MD5 hashes of resources for this many minutes. 27 | | 28 | | Specify 0 not to cache at all. 29 | | 30 | | NOTE: Set an environment specific dotEnv file where this can be overwritten, 31 | | e.g. for Stage / Beta or Production. The default value is used below. 32 | */ 33 | 'expiry' => env('CACHEBUSTER_EXPIRY', 0), 34 | 35 | /* 36 | |-------------------------------------------------------------------------- 37 | | Path maps for location assets in alternative locations 38 | |-------------------------------------------------------------------------- 39 | | 40 | | This is useful if you have a .htaccess file rewriting URLs. 41 | | 42 | | Provide a hashmap of user-facing URL base paths to their corresponding 43 | | filesystem paths, for example '/assets' => 'path/to/assets', 44 | | 45 | | NOTE: Set an environment specific dotEnv file where this can be overwritten, 46 | | e.g. for Stage / Beta or Production. The default value is used below. 47 | */ 48 | 'path_maps' => env('CACHEBUSTER_PATH', array()), 49 | 50 | /* 51 | |-------------------------------------------------------------------------- 52 | | Enable or disable cachebusting functionality 53 | |-------------------------------------------------------------------------- 54 | | 55 | | You may want to disable cachebusting functionality in some environments, 56 | | for example the testing environment if you use the built-in Laravel 57 | | development web server which doesn't support the required URL rewriting. 58 | | 59 | | As an aside, if you still want cachebusting enabled in the development 60 | | server then see https://gist.github.com/felthy/3fc1675a6a89db891396 61 | | 62 | | NOTE: Set an environment specific dotEnv file where this can be overwritten, 63 | | e.g. for Stage / Beta or Production. The default value is used below. 64 | */ 65 | 'enabled' => env('CACHEBUSTER_ENABLED', false), 66 | 67 | ); -------------------------------------------------------------------------------- /src/lang/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/The-Monkeys-and-MAUD/laravel-cachebuster/a0ee0e1487ff7a115b20278fd3b088390b648b76/src/lang/.gitkeep -------------------------------------------------------------------------------- /src/migrations/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/The-Monkeys-and-MAUD/laravel-cachebuster/a0ee0e1487ff7a115b20278fd3b088390b648b76/src/migrations/.gitkeep -------------------------------------------------------------------------------- /src/views/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/The-Monkeys-and-MAUD/laravel-cachebuster/a0ee0e1487ff7a115b20278fd3b088390b648b76/src/views/.gitkeep -------------------------------------------------------------------------------- /tests/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/The-Monkeys-and-MAUD/laravel-cachebuster/a0ee0e1487ff7a115b20278fd3b088390b648b76/tests/.gitkeep --------------------------------------------------------------------------------