├── .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 | 
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
--------------------------------------------------------------------------------