diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0aa4476
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+composer.lock
+tests/temp
+vendor
diff --git a/.styleci.yml b/.styleci.yml
new file mode 100644
index 0000000..c303f4e
--- /dev/null
+++ b/.styleci.yml
@@ -0,0 +1,6 @@
+preset: laravel
+
+linting: true
+
+disabled:
+ - single_class_element_per_statement
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..5a1e77f
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,19 @@
+language: php
+
+php:
+ - 5.5.9
+ - 5.6
+ - 7.0
+ - 7.1
+
+env:
+ matrix:
+ - COMPOSER_FLAGS="--prefer-lowest"
+ - COMPOSER_FLAGS=""
+
+before_script:
+ - travis_retry composer self-update
+ - travis_retry composer update ${COMPOSER_FLAGS} --no-interaction --prefer-source
+
+script:
+ - phpunit
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..75851c8
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) [2016] [Sébastien Nikolaou]
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..15f7aa8
--- /dev/null
+++ b/README.md
@@ -0,0 +1,67 @@
+# Laravel SRI
+
+Subresource Integrity (SRI) package for Laravel
+
+[![Latest Version on Packagist](https://img.shields.io/packagist/v/sebdesign/laravel-sri.svg)](https://packagist.org/packages/sebdesign/laravel-sri)
+[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE.md)
+[![Build Status](https://img.shields.io/travis/sebdesign/laravel-sri/master.svg)](https://travis-ci.org/sebdesign/laravel-sri)
+
+Reference and generate [Subresource Integrity (SRI)](https://www.w3.org/TR/SRI/) hashes from your Laravel Elixir asset pipeline.
+
+## Installation
+
+You can install the package via composer:
+
+```bash
+$ composer require sebdesign/laravel-sri
+```
+
+Next, you must install the service provider:
+
+```php
+// config/app.php
+'providers' => [
+ ...
+ Sebdesign\SRI\SubresourceIntegrityServiceProvider::class,
+];
+```
+
+## Usage
+
+This package is aimed to reference SRI hashes for `css` and `js` files from a `sri.json` file in your `/public` folder. In order to generate this file, see the [laravel-elixir-sri]('https://github.com/sebdesign/laravel-elixir-sri') repository.
+
+To reference the generated hashes from the `sri.json` in your views, you may use the `integrity` helper function with the name of the file you are using in your `elixir` or `asset` function.
+
+As a fallback, if the given file is not found in the `sri.json`, **it will generate the appropriate hashes on the fly** for your convenience.
+
+```php
+{{-- Use with elixir() function --}}
+
+
+{{-- Use with asset() function --}}
+
+```
+
+If you have set the output folder for the `sri.json` in a different location in your Gulpfile, you can specify its `path` on the `config/sri.php`.
+
+```php
+// config/sri.php
+'path' => '/public/assets',
+```
+
+You can also override the config options by passing an array as a second argument on the `integrity` helper function:
+
+```php
+{{-- Use different hash algorithm --}}
+
+```
+
+## Testing
+
+``` bash
+$ composer test
+```
+
+## License
+
+The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..05a1cd2
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,42 @@
+{
+ "name": "sebdesign/laravel-sri",
+ "description": "Subresource Integrity (SRI) package for Laravel",
+ "keywords": [
+ "laravel",
+ "sri"
+ ],
+ "homepage": "https://github.com/sebdesign/laravel-sri",
+ "type": "library",
+ "require": {
+ "laravel/framework": "~5.1.16|~5.2.0|~5.3.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~4.0",
+ "mockery/mockery": "^0.9.5",
+ "orchestra/testbench": "^3.3"
+ },
+ "autoload": {
+ "files": [
+ "src/helpers.php"
+ ],
+ "psr-4": {
+ "Sebdesign\\SRI\\": "src"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Sebdesign\\SRI\\Test\\": "tests"
+ }
+ },
+ "scripts": {
+ "test": "vendor/bin/phpunit"
+ },
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Sébastien Nikolaou",
+ "email": "info@sebdesign.eu",
+ "homepage": "http://sebdesign.eu"
+ }
+ ]
+}
diff --git a/config/sri.php b/config/sri.php
new file mode 100644
index 0000000..7c5faeb
--- /dev/null
+++ b/config/sri.php
@@ -0,0 +1,28 @@
+ [
+ 'sha256',
+ // 'sha384',
+ // 'sha512',
+ ],
+
+ /**
+ * Integrity attribute delimiter.
+ */
+ 'delimiter' => ' ',
+
+ /**
+ * Output filename.
+ */
+ 'filename' => 'sri.json',
+
+ /**
+ * Output path.
+ */
+ 'path' => '/public',
+];
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..f2eebe0
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,22 @@
+
+
+
+
+ tests
+
+
+
+
+ src/
+
+
+
diff --git a/src/Hasher.php b/src/Hasher.php
new file mode 100644
index 0000000..f0b7f46
--- /dev/null
+++ b/src/Hasher.php
@@ -0,0 +1,119 @@
+supportedAlgorithms = $supportedAlgorithms;
+ $this->options = $options;
+ }
+
+ /**
+ * Hash the given file.
+ *
+ * @param string $file
+ * @param array $options
+ * @return string
+ */
+ public function make($file, array $options = [])
+ {
+ $options = $this->getOptions($options);
+
+ return collect($this->getAlgorithms($options))
+ ->map(function ($algorithm) use ($file) {
+ $digest = base64_encode(hash_file($algorithm, $file, true));
+
+ return $algorithm.'-'.$digest;
+ })
+ ->implode($options['delimiter']);
+ }
+
+ /**
+ * Check the given file against a hash.
+ *
+ * @param string $file
+ * @param string $integrity
+ * @param array $options
+ * @return bool
+ */
+ public function check($file, $integrity, array $options = [])
+ {
+ return $this->make($file, $options) === $integrity;
+ }
+
+ /**
+ * Check if the given hash has been hashed using the given options.
+ *
+ * @param string $integrity
+ * @param array $options
+ * @return bool
+ */
+ public function needsRehash($integrity, array $options = [])
+ {
+ $options = $this->getOptions($options);
+
+ $algorithms = collect(explode($options['delimiter'], $integrity))
+ ->map(function ($hash) {
+ return head(explode('-', $hash));
+ });
+
+ return $this->getAlgorithms($options) != $algorithms->all();
+ }
+
+ /**
+ * Replace the given options with the defaults.
+ *
+ * @param array $options
+ * @return array
+ */
+ protected function getOptions(array $options)
+ {
+ return array_replace_recursive($this->options, $options);
+ }
+
+ /**
+ * Get the hashing algorithms from the options.
+ *
+ * @param array $options
+ * @return array
+ * @throws \InvalidArgumentException
+ */
+ protected function getAlgorithms(array $options)
+ {
+ if (!isset($options['algorithms'])
+ || !is_array($options['algorithms'])
+ || empty($options['algorithms'])) {
+ throw new \InvalidArgumentException('No hashing algorithms are set.');
+ }
+
+ if ($notSupported = array_diff($options['algorithms'], $this->supportedAlgorithms)) {
+ throw new \InvalidArgumentException(sprintf(
+ "The hashing algorithms [%s] are not supported.",
+ implode(', ', $notSupported)
+ ));
+ }
+
+ return $options['algorithms'];
+ }
+}
diff --git a/src/SubresourceIntegrityServiceProvider.php b/src/SubresourceIntegrityServiceProvider.php
new file mode 100644
index 0000000..8a14d36
--- /dev/null
+++ b/src/SubresourceIntegrityServiceProvider.php
@@ -0,0 +1,50 @@
+app->runningInConsole()) {
+ $this->publishes([
+ __DIR__.'/../config/sri.php' => config_path('sri.php'),
+ ], 'config');
+ }
+ }
+
+ /**
+ * Register the application services.
+ */
+ public function register()
+ {
+ $this->mergeConfigFrom(__DIR__.'/../config/sri.php', 'sri');
+
+ $this->app->singleton(Hasher::class, function ($app) {
+ return new Hasher(hash_algos(), $app['config']->get('sri'));
+ });
+ }
+
+ /**
+ * Get the services provided by the provider.
+ *
+ * @return array
+ */
+ public function provides()
+ {
+ return [Hasher::class];
+ }
+}
diff --git a/src/helpers.php b/src/helpers.php
new file mode 100644
index 0000000..9f14bca
--- /dev/null
+++ b/src/helpers.php
@@ -0,0 +1,47 @@
+version(), '5.3', '<')) {
+ $hashCollection = $hashCollection->flip();
+ }
+
+ $file = public_path($file);
+
+ return $hashCollection->first(function ($hash, $hashedFile) use ($file) {
+ return base_path($hashedFile) == $file;
+ }, function () use ($file, $options) {
+ if (file_exists($file)) {
+ return app(Hasher::class)->make($file, $options);
+ }
+
+ throw new \InvalidArgumentException("File {$file} not defined in {$options['filename']}.");
+ });
+ }
+}
diff --git a/tests/HasherTest.php b/tests/HasherTest.php
new file mode 100644
index 0000000..81f5eb4
--- /dev/null
+++ b/tests/HasherTest.php
@@ -0,0 +1,180 @@
+getCss();
+ $hasher = $this->app->make(Hasher::class);
+
+ // act
+
+ $hash = $hasher->make($css);
+
+ // assert
+
+ $this->assertStringStartsWith('sha256-', $hash);
+ }
+
+ /**
+ * @test
+ */
+ public function it_accepts_different_algorithms()
+ {
+ // arrange
+
+ $css = $this->getCss();
+ $this->app['config']->set('sri.algorithms', ['sha384']);
+ $hasher = $this->app->make(Hasher::class);
+
+ // act
+
+ $hash = $hasher->make($css);
+
+ // assert
+
+ $this->assertStringStartsWith('sha384-', $hash);
+
+ // act
+
+ $hash = $hasher->make($css, ['algorithms' => ['sha512']]);
+
+ // assert
+
+ $this->assertStringStartsWith('sha512-', $hash);
+ }
+
+ /**
+ * @test
+ * @expectedException \InvalidArgumentException
+ * @expectedExceptionMessage sha1024
+ */
+ public function it_does_not_accept_invalid_algorithms()
+ {
+ // arrange
+
+ $css = $this->getCss();
+ $this->app['config']->set('sri.algorithms', ['sha512', 'sha1024']);
+ $hasher = $this->app->make(Hasher::class);
+
+ // act
+
+ $hasher->make($css);
+ }
+
+ /**
+ * @test
+ */
+ public function it_accepts_multiple_algorithms()
+ {
+ // arrange
+
+ $css = $this->getCss();
+ $this->app['config']->set('sri.algorithms', ['sha256', 'sha384']);
+ $hasher = $this->app->make(Hasher::class);
+
+ // act
+
+ $hash = $hasher->make($css);
+
+ // assert
+
+ $this->assertRegExp('/^sha256-.+ sha384-.+$/', $hash);
+
+ // act
+
+ $hash = $hasher->make($css, ['algorithms' => ['sha384', 'sha512']]);
+
+ // assert
+
+ $this->assertRegExp('/^sha384-.+ sha512-.+$/', $hash);
+ }
+
+ /**
+ * @test
+ */
+ public function it_accepts_a_different_delimiter()
+ {
+ // arrange
+
+ $css = $this->getCss();
+ $this->app['config']->set('sri.algorithms', ['sha256', 'sha384']);
+ $this->app['config']->set('sri.delimiter', '_');
+ $hasher = $this->app->make(Hasher::class);
+
+ // act
+
+ $hash = $hasher->make($css);
+
+ // assert
+
+ $this->assertRegExp('/^sha256-.+_sha384-.+$/', $hash);
+
+ // act
+
+ $hash = $hasher->make($css, ['delimiter' => ':']);
+
+ // assert
+
+ $this->assertRegExp('/^sha256-.+:sha384-.+$/', $hash);
+ }
+
+ /**
+ * @test
+ */
+ public function it_checks_a_hash()
+ {
+ // arrange
+
+ $css = $this->getCss();
+ $hasher = $this->app->make(Hasher::class);
+
+ // act
+
+ $hash = $hasher->make($css);
+
+ // assert
+
+ $this->assertTrue($hasher->check($css, $hash));
+ $this->assertFalse($hasher->check($css, $hash, ['algorithms' => ['sha512']]));
+ }
+
+ /**
+ * @test
+ */
+ public function it_checks_if_the_hash_needs_rehash()
+ {
+ // arrange
+
+ $css = $this->getCss();
+ $hasher = $this->app->make(Hasher::class);
+
+ // act
+
+ $hash = $hasher->make($css, ['algorithms' => ['sha256', 'sha384']]);
+
+ // assert
+
+ $this->assertFalse($hasher->needsRehash($hash, [
+ 'algorithms' => ['sha256', 'sha384'],
+ ]));
+
+ $this->assertTrue($hasher->needsRehash($hash, [
+ 'algorithms' => ['sha384'],
+ ]));
+
+ $this->assertTrue($hasher->needsRehash($hash, [
+ 'algorithms' => ['sha256', 'sha384'],
+ 'delimiter' => ':',
+ ]));
+ }
+}
diff --git a/tests/HelpersTest.php b/tests/HelpersTest.php
new file mode 100644
index 0000000..ba262d0
--- /dev/null
+++ b/tests/HelpersTest.php
@@ -0,0 +1,60 @@
+app->make(Hasher::class);
+
+ $integrity = integrity('app.css');
+
+ $this->assertTrue($hasher->check($this->getCss(), $integrity));
+ }
+
+ /**
+ * @test
+ */
+ public function it_generates_the_hash_if_it_does_not_exist()
+ {
+ $hasher = $this->app->make(Hasher::class);
+
+ $integrity = integrity('app.js');
+
+ $this->assertTrue($hasher->check($this->getJs(), $integrity));
+ }
+
+ /**
+ * @test
+ */
+ public function it_accepts_options()
+ {
+ $hasher = $this->app->make(Hasher::class);
+
+ rename(
+ $this->getTestFilesDirectory('sri.json'),
+ $this->getTestFilesDirectory('integrity.json')
+ );
+
+ $integrity = integrity('app.css', ['filename' => 'integrity.json']);
+
+ $this->assertTrue($hasher->check($this->getCss(), $integrity));
+ }
+
+ /**
+ * @test
+ * @expectedException \InvalidArgumentException
+ */
+ public function it_fails_if_a_file_does_not_exist()
+ {
+ $hasher = $this->app->make(Hasher::class);
+
+ integrity('app.min.css');
+ }
+}
diff --git a/tests/TestCase.php b/tests/TestCase.php
new file mode 100644
index 0000000..a1d1be2
--- /dev/null
+++ b/tests/TestCase.php
@@ -0,0 +1,82 @@
+setUpTempTestFiles();
+ }
+
+ /**
+ * @param \Illuminate\Foundation\Application $app
+ *
+ * @return array
+ */
+ protected function getPackageProviders($app)
+ {
+ return [
+ SubresourceIntegrityServiceProvider::class,
+ ];
+ }
+
+ /**
+ * @param \Illuminate\Foundation\Application $app
+ */
+ protected function getEnvironmentSetUp($app)
+ {
+ $this->initializeDirectory($this->getTempDirectory());
+
+ $app->setBasePath($this->getTestFilesDirectory());
+
+ $app->singleton('path.public', function () {
+ return $this->getTestFilesDirectory();
+ });
+
+ $config = $app['files']->getRequire(__DIR__ . '/../config/sri.php');
+
+ $app['config']->set('sri', $config);
+ }
+
+ protected function setUpTempTestFiles()
+ {
+ $this->initializeDirectory($this->getTestFilesDirectory());
+ File::copyDirectory(__DIR__.'/testfiles', $this->getTestFilesDirectory());
+ }
+
+ protected function initializeDirectory($directory)
+ {
+ if (File::isDirectory($directory)) {
+ File::deleteDirectory($directory);
+ }
+ File::makeDirectory($directory);
+ }
+
+ public function getTempDirectory($suffix = '')
+ {
+ return __DIR__.'/temp'.($suffix == '' ? '' : '/'.$suffix);
+ }
+
+ public function getTestFilesDirectory($suffix = '')
+ {
+ return $this->getTempDirectory().'/testfiles'.($suffix == '' ? '' : '/'.$suffix);
+ }
+
+ public function getCss()
+ {
+ return $this->getTestFilesDirectory('app.css');
+ }
+
+ public function getJs()
+ {
+ return $this->getTestFilesDirectory('app.js');
+ }
+}
diff --git a/tests/testfiles/app.css b/tests/testfiles/app.css
new file mode 100644
index 0000000..4a0203a
--- /dev/null
+++ b/tests/testfiles/app.css
@@ -0,0 +1,3 @@
+.flex {
+ display: flex;
+}
diff --git a/tests/testfiles/app.js b/tests/testfiles/app.js
new file mode 100644
index 0000000..ec9bc0e
--- /dev/null
+++ b/tests/testfiles/app.js
@@ -0,0 +1 @@
+console.log('SRI');
diff --git a/tests/testfiles/sri.json b/tests/testfiles/sri.json
new file mode 100644
index 0000000..e654f04
--- /dev/null
+++ b/tests/testfiles/sri.json
@@ -0,0 +1,3 @@
+{
+ "app.css": "sha256-LwVRqU6YK4JxEeAofRks4sFR9U/yambtZYgim8JJpXQ="
+}