Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ webloader:
watchFiles: # only watch modify file
- {files: ["*.css", "*.less"], from: css}
- {files: ["*.css", "*.less"], in: css}
sriHashingAlgorithms: # allowed values are sha256, sha384, and sha512, multiple can be specified
- sha256

js:
default:
Expand All @@ -81,6 +83,8 @@ webloader:
files:
- %appDir%/../libs/nette/nette/client-side/netteForms.js
- web.js
sriHashingAlgorithms: # allowed values are sha256, sha384, and sha512, multiple can be specified
- sha256
```

For older versions of Nette, you have to register the extension in `app/bootstrap.php`:
Expand Down
22 changes: 22 additions & 0 deletions WebLoader/Compiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ class Compiler
/** @var bool */
private $debugging = FALSE;

/** @var array */
private $sriHashingAlgorithms = array();

public function __construct(IFileCollection $files, IOutputNamingConvention $convention, $outputDir)
{
$this->collection = $files;
Expand Down Expand Up @@ -303,6 +306,25 @@ public function addFileFilter($filter)
$this->fileFilters[] = $filter;
}

/**
* Add hashing algorithm for Subresource Integrity checksum
*
* @link https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
* @param string $algorithm
*/
public function addSriHashingAlgorithm($algorithm)
{
$this->sriHashingAlgorithms[] = $algorithm;
}

/**
* @return array
*/
public function getSriHashingAlgorithms()
{
return $this->sriHashingAlgorithms;
}

/**
* @return array
*/
Expand Down
18 changes: 11 additions & 7 deletions WebLoader/Nette/CssLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,17 @@ public function setAlternate($alternate)
*/
public function getElement($source)
{
if ($this->alternate) {
$alternate = ' alternate';
} else {
$alternate = '';
}

return Html::el("link")->rel("stylesheet".$alternate)->type($this->type)->media($this->media)->title($this->title)->href($source);
$alternate = $this->alternate ? ' alternate' : '';
$content = $this->getCompiledFileContent($source);
$sriChecksum = $this->getSriChecksums($content) ?: false;

return Html::el('link')
->integrity($sriChecksum)
->rel('stylesheet' . $alternate)
->type($this->type)
->media($this->media)
->title($this->title)
->href($source);
}

}
10 changes: 10 additions & 0 deletions WebLoader/Nette/Extension.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ public function getDefaultConfig()
'fileFilters' => array(),
'joinFiles' => TRUE,
'namingConvention' => '@' . $this->prefix('jsNamingConvention'),
'sriHashingAlgorithms' => array(
'sha256',
),
),
'cssDefaults' => array(
'checkLastModified' => TRUE,
Expand All @@ -52,6 +55,9 @@ public function getDefaultConfig()
'fileFilters' => array(),
'joinFiles' => TRUE,
'namingConvention' => '@' . $this->prefix('cssNamingConvention'),
'sriHashingAlgorithms' => array(
'sha256',
),
),
'js' => array(

Expand Down Expand Up @@ -149,6 +155,10 @@ private function addWebLoader(ContainerBuilder $builder, $name, $config)

$compiler->addSetup('setCheckLastModified', array($config['checkLastModified']));

foreach ($config['sriHashingAlgorithms'] as $algorithm) {
$compiler->addSetup('addSriHashingAlgorithm', array($algorithm));
}

// todo css media
}

Expand Down
8 changes: 7 additions & 1 deletion WebLoader/Nette/JavaScriptLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,13 @@ class JavaScriptLoader extends WebLoader
*/
public function getElement($source)
{
return Html::el("script")->type("text/javascript")->src($source);
$content = $this->getCompiledFileContent($source);
$sriChecksum = $this->getSriChecksums($content) ?: false;

return Html::el("script")
->integrity($sriChecksum)
->type("text/javascript")
->src($source);
}

}
50 changes: 50 additions & 0 deletions WebLoader/Nette/WebLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,59 @@ public function render()
}
}

/**
* Get content of a compiled file by its URL path
*
* @param string $source
* @return string
*/
protected function getCompiledFileContent($source)
{
$outputDir = $this->compiler->getOutputDir();
$urlPath = parse_url($source, PHP_URL_PATH);
$fileName = basename($urlPath);
$filePath = $outputDir . '/' . $fileName;
$content = file_get_contents($filePath);

return $content;
}

protected function getGeneratedFilePath($file)
{
return $this->tempPath . '/' . $file->file . '?' . $file->lastModified;
}

/**
* Generate Subresource Integrity checksums for all set hashing algorithms
*
* @param string $fileContent
* @return string
*/
protected function getSriChecksums($fileContent)
{
$checksums = [];

foreach ($this->compiler->getSriHashingAlgorithms() as $algorithm) {
$checksums[] = $this->getOneSriChecksum($algorithm, $fileContent);
}

return implode(' ', $checksums);
}

/**
* Generate Subresource Integrity checksum
*
* @link https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
* @param string $hashingAlgorithm
* @param string $fileContent
* @return string
*/
private function getOneSriChecksum($hashingAlgorithm, $fileContent)
{
$hash = hash($hashingAlgorithm, $fileContent, true);
$hashBase64 = base64_encode($hash);

return $hashingAlgorithm . '-' . $hashBase64;
}

}
146 changes: 146 additions & 0 deletions tests/Nette/WebLoaderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
<?php

namespace WebLoader\Test\Nette;

use WebLoader\Compiler;
use WebLoader\DefaultOutputNamingConvention;
use WebLoader\FileCollection;
use WebLoader\Nette\WebLoader;

class WebLoaderTest extends \PHPUnit_Framework_TestCase
{
const HASHES_SHA256 = 'sha256';

const HASHES_SHA384 = 'sha384';

const HASHES_SHA512 = 'sha512';

const HASHES_TEST_STRING = 'testString';

const SOURCE_FILE_NAME = 'one.css';

private $hashes = [
self::HASHES_SHA256 => 'sha256-Ss8LOdnEdmcJo2ifVTrAGrVQVF/6RUTfwLLOqC+6AqM=',
self::HASHES_SHA384 => 'sha384-OZ7wmy2rB2wregDCOAvEmnrP7wUiSrCbaFEn6r86mq6oPm8oqDrZMRy2GnFPUyxm',
self::HASHES_SHA512 => 'sha512-xIr1p/bUqFH8ikNO7WOKsabvaOGdvK6JSsZ8n7xbywGCuOcSOz3zyeTct2kMIxA/A9wX9UNSBxzrKk6yBLJrkQ==',
];

private $fileCollectionRootPath;

private $sourceFileDirPath;

private $tempPath;

public function setUp()
{
$this->fileCollectionRootPath = __DIR__ . '/../fixtures';
$this->sourceFileDirPath = __DIR__ . '/../fixtures/dir';
$this->tempPath = __DIR__ . '/../temp';

@mkdir($this->tempPath);
copy(
$this->sourceFileDirPath . '/' . self::SOURCE_FILE_NAME,
$this->tempPath . '/' . self::SOURCE_FILE_NAME
);
}

/**
* @dataProvider provideTestGetSriChecksums
* @param $hashingAlgorithms
* @param $fileContent
* @param $expected
*/
public function testGetSriChecksums($hashingAlgorithms, $fileContent, $expected)
{
$compiler = $this->getCompiler($hashingAlgorithms);
$webloader = $this->getWebLoader($compiler);
$sriChecksumsResult = $webloader->getSriChecksumsResult($fileContent);

$this->assertSame($expected, $sriChecksumsResult);
}

public function provideTestGetSriChecksums()
{
return [
[
[],
self::HASHES_TEST_STRING,
'',
],
[
[
self::HASHES_SHA256,
],
self::HASHES_TEST_STRING,
$this->hashes[self::HASHES_SHA256],
],
[
[
self::HASHES_SHA256,
self::HASHES_SHA512,
],
self::HASHES_TEST_STRING,
implode(' ', [
$this->hashes[self::HASHES_SHA256],
$this->hashes[self::HASHES_SHA512],
]),
],
];
}

public function testGetCompiledFileContent()
{
$compiler = $this->getCompiler();
$webloader = $this->getWebLoader($compiler);
$compiledFileContentResult = $webloader->getCompiledFileContentResult(
$this->sourceFileDirPath . '/' . self::SOURCE_FILE_NAME
);
$expected = file_get_contents($this->sourceFileDirPath . '/' . self::SOURCE_FILE_NAME);

$this->assertSame($expected, $compiledFileContentResult);
}

/**
* @param array $hashingAlgorithms
* @return Compiler
*/
private function getCompiler($hashingAlgorithms = [])
{
$files = new FileCollection($this->fileCollectionRootPath);
$compiler = new Compiler($files, new DefaultOutputNamingConvention(), $this->tempPath);

foreach ($hashingAlgorithms as $alhorithm) {
$compiler->addSriHashingAlgorithm($alhorithm);
}

return $compiler;
}

/**
* @param Compiler $compiler
* @return WebLoaderTestImplementation
*/
private function getWebLoader(Compiler $compiler)
{
return new WebLoaderTestImplementation($compiler, $this->tempPath);
}
}


class WebLoaderTestImplementation extends WebLoader
{
public function getCompiledFileContentResult($source)
{
return $this->getCompiledFileContent($source);
}

public function getSriChecksumsResult($fileContent)
{
return $this->getSriChecksums($fileContent);
}

public function getElement($source)
{
// not important now
}
}