diff --git a/.travis.yml b/.travis.yml index 372ff5c..63f45f4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,10 @@ language: php php: - - 5.6 - 7.0 - 7.1 + - 7.2 + - 7.3 env: matrix: diff --git a/README.md b/README.md index 7c2b602..47a27fe 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,13 @@ protected $middlewareGroups = [ ]; ``` +## Publish config + +```php +php artisan vendor:publish --provider="JacobBennett\Http2ServerPush\ServiceProvider" +``` + + ## Usage When you route a request through the `AddHttp2ServerPush` middleware, the response is scanned for any `link`, `script` or `img` tags that could benefit from being loaded using Server Push. diff --git a/src/Middleware/AddHttp2ServerPush.php b/src/Middleware/AddHttp2ServerPush.php index 253cf28..8a87f0f 100644 --- a/src/Middleware/AddHttp2ServerPush.php +++ b/src/Middleware/AddHttp2ServerPush.php @@ -24,7 +24,7 @@ class AddHttp2ServerPush * * @return mixed */ - public function handle(Request $request, Closure $next, $limit = null) + public function handle(Request $request, Closure $next, $limit = null, $sizeLimit = null, $excludeKeywords=null) { $response = $next($request); @@ -32,30 +32,53 @@ public function handle(Request $request, Closure $next, $limit = null) return $response; } - $this->generateAndAttachLinkHeaders($response, $limit); + $this->generateAndAttachLinkHeaders($response, $limit, $sizeLimit, $excludeKeywords); return $response; } + public function getConfig($key, $default=false) { + if(!function_exists('config')) { // for tests.. + return $default; + } + return config('http2serverpush.'.$key, $default); + } + /** * @param \Illuminate\Http\Response $response * * @return $this */ - protected function generateAndAttachLinkHeaders(Response $response, $limit = null) + protected function generateAndAttachLinkHeaders(Response $response, $limit = null, $sizeLimit = null, $excludeKeywords=null) { + $excludeKeywords ?? $this->getConfig('exclude_keywords', []); $headers = $this->fetchLinkableNodes($response) ->flatten(1) ->map(function ($url) { return $this->buildLinkHeaderString($url); }) - ->filter() ->unique() - ->take($limit) - ->implode(','); + ->filter(function($value, $key) use ($excludeKeywords){ + if(!$value) return false; + $exclude_keywords = collect($excludeKeywords)->map(function ($keyword) { + return preg_quote($keyword); + }); + if($exclude_keywords->count() <= 0) { + return true; + } + return !preg_match('%('.$exclude_keywords->implode('|').')%i', $value); + }) + ->take($limit); + + $sizeLimit = $sizeLimit ?? max(1, intval($this->getConfig('size_limit', 32*1024))); + $headersText = trim($headers->implode(',')); + while(strlen($headersText) > $sizeLimit) { + $headers->pop(); + $headersText = trim($headers->implode(',')); + } - if (!empty(trim($headers))) { - $this->addLinkHeader($response, $headers); + if (!empty($headersText)) { + $this->addLinkHeader($response, $headersText); } return $this; @@ -115,6 +138,12 @@ private function buildLinkHeaderString($url) $type = collect($linkTypeMap)->first(function ($type, $extension) use ($url) { return str_contains(strtoupper($url), $extension); }); + + + if(!preg_match('%^https?://%i', $url)) { + $basePath = $this->getConfig('base_path', '/'); + $url = $basePath . ltrim($url, $basePath); + } return is_null($type) ? null : "<{$url}>; rel=preload; as={$type}"; } diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php new file mode 100644 index 0000000..2f37cea --- /dev/null +++ b/src/ServiceProvider.php @@ -0,0 +1,22 @@ +publishes([ + __DIR__ . '/config.php' => config_path('http2serverpush.php'), + ]); + } + +} diff --git a/src/config.php b/src/config.php new file mode 100644 index 0000000..28e0206 --- /dev/null +++ b/src/config.php @@ -0,0 +1,8 @@ + '6000', // in bytes + 'base_path' => '/', + 'exclude_keywords' => [] +]; diff --git a/tests/AddHttp2ServerPushTest.php b/tests/AddHttp2ServerPushTest.php index de33856..2b416af 100644 --- a/tests/AddHttp2ServerPushTest.php +++ b/tests/AddHttp2ServerPushTest.php @@ -14,6 +14,32 @@ public function setUp() $this->middleware = new AddHttp2ServerPush(); } + + /** @test */ + public function it_will_not_exceed_size_limit() + { + $request = new Request(); + + $limit = 50; + $response = $this->middleware->handle($request, $this->getNext('pageWithCssAndJs'), null, $limit, []); + + $this->assertTrue($this->isServerPushResponse($response)); + $this->assertTrue(strlen($response->headers->get('link')) <= $limit ); + $this->assertCount(1, explode(",", $response->headers->get('link'))); + } + + /** @test */ + public function it_will_not_add_excluded_asset() + { + $request = new Request(); + + $response = $this->middleware->handle($request, $this->getNext('pageWithCssAndJs'), null, null, ['thing']); + + $this->assertTrue($this->isServerPushResponse($response)); + $this->assertTrue(!str_contains($response->headers, 'thing')); + $this->assertCount(1, explode(",", $response->headers->get('link'))); + } + /** @test */ public function it_will_not_modify_a_response_with_no_server_push_assets() { @@ -77,7 +103,7 @@ public function it_returns_well_formatted_link_headers() $response = $this->middleware->handle($request, $this->getNext('pageWithCss')); - $this->assertEquals("; rel=preload; as=style", $response->headers->get('link')); + $this->assertEquals("; rel=preload; as=style", $response->headers->get('link')); } /** @test */