Skip to content

Commit

Permalink
external assets: download or move to public section
Browse files Browse the repository at this point in the history
  • Loading branch information
Milan Matějček committed Jul 7, 2017
1 parent 4ac353e commit 63a579e
Show file tree
Hide file tree
Showing 18 changed files with 270 additions and 18 deletions.
5 changes: 4 additions & 1 deletion composer.json
Expand Up @@ -20,7 +20,10 @@
"autoload": {
"psr-4": {
"h4kuna\\Assets\\": "src/"
}
},
"files": [
"src/exceptions.php"
]
},
"require-dev": {
"salamium/testinium": "^0.1"
Expand Down
9 changes: 4 additions & 5 deletions src/Assets.php
Expand Up @@ -24,7 +24,7 @@ public function __construct(File $file)
public function addCss($filename, array $attributes = [])
{
if ($this->css === NULL) {
throw new \RuntimeException('You try add file after renderCss().');
throw new InvalidStateException('You try add file after renderCss().');
}
$this->css[$filename] = $attributes;
return $this;
Expand All @@ -33,7 +33,7 @@ public function addCss($filename, array $attributes = [])
public function addJs($filename, array $attributes = [])
{
if ($this->js === NULL) {
throw new \RuntimeException('You try add file after renderJs().');
throw new InvalidStateException('You try add file after renderJs().');
}
$this->js[$filename] = $attributes;
return $this;
Expand All @@ -43,7 +43,7 @@ public function addJs($filename, array $attributes = [])
public function renderCss()
{
if ($this->css === NULL) {
throw new \RuntimeException('renderCss() call onetime per life.');
throw new InvalidStateException('renderCss() call onetime per life.');
}
$out = new Utils\Html;
foreach ($this->css as $filename => $attributes) {
Expand All @@ -61,12 +61,11 @@ public function renderCss()
public function renderJs()
{
if ($this->js === NULL) {
throw new \RuntimeException('renderJs() call onetime per life.');
throw new InvalidStateException('renderJs() call onetime per life.');
}
$out = new Utils\Html;
foreach ($this->js as $filename => $attributes) {
$out[] = Utils\Html::el('script', [
'type' => 'text/javascript',
'src' => $this->createUrl($filename)
] + $attributes);
}
Expand Down
115 changes: 111 additions & 4 deletions src/DI/AssetsExtension.php
Expand Up @@ -3,21 +3,34 @@
namespace h4kuna\Assets\DI;

use h4kuna\Assets,
Nette\DI as NDI;
Nette\DI as NDI,
Nette\Utils;

class AssetsExtension extends \Nette\DI\CompilerExtension
{

/** @var array */
private $duplicity = [];

/** @var array */
private $defaults = [
'debugMode' => '%debugMode%',
'wwwDir' => '%wwwDir%',
'tempDir' => '%tempDir%/cache',
'cacheBuilder' => NULL
'cacheBuilder' => NULL,
'wwwTempDir' => '%wwwDir%/temp',
'externalAssets' => []
];

public function loadConfiguration()
{
$config = $this->getConfig($this->defaults);
$parameters = $this->getContainerBuilder()->parameters;
$this->defaults['debugMode'] = $parameters['debugMode'];
$this->defaults['tempDir'] = $parameters['tempDir'] . '/cache';
$this->defaults['wwwTempDir'] = $parameters['wwwDir'] . '/temp';

$config = $this->validateConfig($this->defaults);
$config['wwwDir'] = $parameters['wwwDir'];
$builder = $this->getContainerBuilder();

$cacheAssets = $builder->addDefinition($this->prefix('cache'))
Expand All @@ -33,6 +46,10 @@ public function loadConfiguration()
$builder->getDefinition('latte.latteFactory')
->addSetup('addFilter', ['asset', new NDI\Statement("[?, 'createUrl']", [$assetFile])]);

if ($config['externalAssets']) {
$this->saveExternalAssets($config['externalAssets'], $config['wwwTempDir']);
}

// build own cache
if ($config['cacheBuilder'] && $config['debugMode'] === FALSE) {
$this->createAssetsCache($config);
Expand All @@ -44,12 +61,102 @@ private function createAssetsCache(array $config)
/* @var $cacheBuilder ICacheBuilder */
$cacheBuilder = new $config['cacheBuilder'];
if (!$cacheBuilder instanceof ICacheBuilder) {
throw new \InvalidArgumentException('Option cacheBuilder must be class instance of ' . __NAMESPACE__ . '\\ICacheBuilder');
throw new Assets\InvalidArgumentException('Option cacheBuilder must be class instance of ' . __NAMESPACE__ . '\\ICacheBuilder');
}

$cache = new Assets\CacheAssets($config['debugMode'], $config['tempDir']);
$cache->clear();
$cacheBuilder->create($cache, $config['wwwDir']);
}

private function saveExternalAssets(array $files, $destination)
{
$duplicity = [];
foreach ($files as $key => $file) {
if (self::isHttp($file)) {
$path = $this->fromHttp($file, $key, $destination);
if ($path === NULL) {
continue;
}
$mtime = self::mtimeHttp($file);
} else {
$path = $this->fromFs($file, $key, $destination);
$mtime = filemtime($file);
}

touch($path, $mtime);
}
}

private function fromFs($file, $newName, $destination)
{
if (is_numeric($newName)) {
$newName = basename($file);
}
$path = $destination . DIRECTORY_SEPARATOR . $newName;
if (!is_file($file)) {
throw new Assets\FileNotFoundException($file);
}
Utils\FileSystem::createDir(dirname($path));
$this->checkDuplicity($path);
if (!copy($file, $path)) {
throw new Assets\DirectoryIsNotWriteableException(dirname($path));
}
return $path;
}

private static function isHttp($file)
{
return preg_match('~^http~', $file);
}

private function fromHttp($url, $hash, $destination)
{
$name = basename($url);
$filename = $destination . DIRECTORY_SEPARATOR . $name;
$this->checkDuplicity($filename);
if (is_file($filename)) {
return NULL;
}

$content = @file_get_contents($url);
if (!$content) {
throw new Assets\DownloadFaildFromExternalUrlException($url);
}

if (!file_put_contents($filename, $content)) {
throw new Assets\DirectoryIsNotWriteableException(dirname($filename));
}

if (!is_numeric($hash)) {
list($function, $token) = explode('-', $hash, 2);
$secureToken = base64_encode(hash($function, $content, TRUE));
if ($secureToken !== $token) {
throw new Assets\CompareTokensException('Expected token: ' . $token . ' and actual is: ' . $secureToken . '. Hash function is: "' . $function . '".');
}
}

return $filename;
}

private function checkDuplicity($filename)
{
if (isset($this->duplicity[$filename])) {
throw new Assets\DuplicityAssetNameException($filename);
}
$this->duplicity[$filename] = TRUE;
}

private static function mtimeHttp($url)
{
foreach (get_headers($url) as $header) {
if (!preg_match('~Last-Modified: (?P<date>.*)~', $header, $find)) {
continue;
}

return \DateTime::createFromFormat('D, d M Y H:i:s T', $find['date'])->format('U');
}
throw new Assets\HeaderLastModifyException('Header Last-Modified not found for url: "' . $url . '". You can\'t use this automaticaly download. Let\'s save manualy.');
}

}
21 changes: 21 additions & 0 deletions src/exceptions.php
@@ -0,0 +1,21 @@
<?php

namespace h4kuna\Assets;

abstract class AssetException extends \Exception {}

class DirectoryIsNotWriteableException extends AssetException {}

class DownloadFaildFromExternalUrlException extends AssetException {}

class CompareTokensException extends AssetException {}

class HeaderLastModifyException extends AssetException {}

class InvalidArgumentException extends AssetException {}

class InvalidStateException extends AssetException {}

class DuplicityAssetNameException extends AssetException {}

class FileNotFoundException extends AssetException {}
1 change: 1 addition & 0 deletions tests/bootsrap.php
Expand Up @@ -9,6 +9,7 @@
$tmp = __DIR__ . '/temp';
$configurator->enableDebugger($tmp);
$configurator->setTempDirectory($tmp);
$configurator->addParameters(['wwwDir' => __DIR__]);
$configurator->setDebugMode(FALSE);
$configurator->addConfig(__DIR__ . '/config/test.neon');
$local = __DIR__ . '/config/test.local.neon';
Expand Down
5 changes: 4 additions & 1 deletion tests/config/test.neon
Expand Up @@ -12,7 +12,10 @@ extensions:
assetsExtension: h4kuna\Assets\DI\AssetsExtension

assetsExtension:
wwwDir: %appDir%
wwwTempDir: %appDir%/temp
externalAssets:
'sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=': https://code.jquery.com/jquery-3.2.1.min.js
- %appDir%/../vendor/tracy/tracy/src/Tracy/assets/BlueScreen/bluescreen.js
# optional
debugMode: FALSE
# tempDir: %tempDir%
Expand Down
12 changes: 6 additions & 6 deletions tests/src/AssetsTest.phpt
Expand Up @@ -11,7 +11,7 @@ function test(\Closure $closure)

$container = require __DIR__ . '/../bootsrap.php';

$time = 536284800;
$time = 1490036475;
touch(__DIR__ . '/../config/php-unix.ini', $time);

test(function() use ($container, $time) {
Expand All @@ -20,15 +20,15 @@ test(function() use ($container, $time) {
$assets->addJs('//example.com/foo.js');
$assets->addJs('http://example.com/foo.js');
$assets->addJs('config/php-unix.ini');
Assert::same('<script type="text/javascript" src="//example.com/foo.js"></script><script type="text/javascript" src="http://example.com/foo.js"></script><script type="text/javascript" src="/config/php-unix.ini?' . $time . '"></script>', (string) $assets->renderJs());
Assert::same('<script src="//example.com/foo.js"></script><script src="http://example.com/foo.js"></script><script src="/config/php-unix.ini?' . $time . '"></script>', (string) $assets->renderJs());

Assert::exception(function() use ($assets) {
Assert::same('', (string) $assets->renderJs());
}, \RuntimeException::class);
}, InvalidStateException::class);

Assert::exception(function() use ($assets) {
$assets->addJs('config/php-unix.ini');
}, \RuntimeException::class);
}, InvalidStateException::class);
});


Expand All @@ -39,10 +39,10 @@ test(function() use ($container, $time) {

Assert::exception(function() use ($assets) {
Assert::same('', (string) $assets->renderCss());
}, \RuntimeException::class);
}, InvalidStateException::class);


Assert::exception(function() use ($assets) {
$assets->addCss('foo.css');
}, \RuntimeException::class);
}, InvalidStateException::class);
});
4 changes: 3 additions & 1 deletion tests/src/BasicRunTest.phpt
Expand Up @@ -5,7 +5,7 @@ use h4kuna\Assets,

$container = require __DIR__ . '/../bootsrap.php';

$time = 536284800;
$time = 1490036475; // mtime jsquery
touch(__DIR__ . '/../config/php-unix.ini', $time);

/* @var $file Assets\File */
Expand All @@ -16,3 +16,5 @@ Assert::type(Assets\File::class, $file);
Assert::same('/config/php-unix.ini?' . $time, $file->createUrl('config/php-unix.ini'));

Assert::same('//www.example.com/config/test.neon', preg_replace('~\?.*~', '', $file->createUrl('//config/test.neon')));

Assert::same('/temp/jquery-3.2.1.min.js?' . $time, $file->createUrl('temp/jquery-3.2.1.min.js'));
83 changes: 83 additions & 0 deletions tests/src/DI/AssetsExtensionTest.phpt
@@ -0,0 +1,83 @@
<?php

namespace h4kuna\Assets\DI;

use h4kuna\Assets,
Nette\Utils,
Tester\Assert;

$container = require __DIR__ . '/../../bootsrap.php';

function configuratorFactory($neon, $subDirOff = FALSE)
{
$configurator = new \Nette\Configurator;
$tmp = __DIR__ . '/../../temp/test';

Utils\FileSystem::delete($tmp);
Utils\FileSystem::createDir($tmp);

$wwwDir = $tmp . '/../www';
$subWww = $wwwDir . '/temp';

@chmod($subWww, 0755);
Utils\FileSystem::delete($subWww);
Utils\FileSystem::createDir($subWww);
if ($subDirOff) {
chmod($subWww, 0000);
}

$configurator->enableDebugger($tmp);
$configurator->addParameters([
'wwwDir' => $wwwDir
]);
$configurator->setTempDirectory($tmp);
$configurator->addConfig(__DIR__ . '/assets/assets.neon');
$configurator->addConfig(__DIR__ . '/assets/' . $neon);
return $configurator->createContainer();
}

Assert::exception(function() {
configuratorFactory('external-download-faild.neon');
}, Assets\DownloadFaildFromExternalUrlException::class);


Assert::exception(function() {
configuratorFactory('permission-denied.neon', TRUE);
}, Assets\DirectoryIsNotWriteableException::class);


Assert::exception(function() {
configuratorFactory('bad-token.neon');
}, Assets\CompareTokensException::class);


Assert::exception(function() {
configuratorFactory('file-not-found.neon');
}, Assets\FileNotFoundException::class);

Assert::exception(function() {
configuratorFactory('fs-main.neon', TRUE);
}, Assets\DirectoryIsNotWriteableException::class);

$x = function() {
touch(__DIR__ . '/assets/main.js', 123456789);
$container = configuratorFactory('fs-main.neon');
$file = $container->getByType(Assets\File::class);
/* @var $file \h4kuna\Assets\File */
Assert::same('/temp/main.js?123456789', $file->createUrl('temp/main.js'));
};
$x();

$x = function() {
touch(__DIR__ . '/assets/main.js', 123456789);
$container = configuratorFactory('fs-main-alias.neon');
$file = $container->getByType(Assets\File::class);
/* @var $file \h4kuna\Assets\File */
Assert::same('/temp/app/index.js?123456789', $file->createUrl('temp/app/index.js'));
};
$x();


Assert::exception(function() {
configuratorFactory('duplicity.neon');
}, Assets\DuplicityAssetNameException::class);
8 changes: 8 additions & 0 deletions tests/src/DI/assets/assets.neon
@@ -0,0 +1,8 @@
php:
date.timezone: Europe/Prague

application:
scanDirs: FALSE

extensions:
assetsExtension: h4kuna\Assets\DI\AssetsExtension

0 comments on commit 63a579e

Please sign in to comment.