diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ed6080..a33e32f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ ### Unreleased +### v1.3.0 (2020-01-16) + +* Add StaticAssetUrlProvider to provide simple cache-busted local URLs for CSS etc in local + dev or remote (e.g. cloud storage / s3) urls in production. + ### v1.2.1 (2019-11-15) * Allow DeploymentConfig->map() to return values in standalone environment diff --git a/src/Assets/StaticAssetUrlProvider.php b/src/Assets/StaticAssetUrlProvider.php new file mode 100644 index 0000000..c393d06 --- /dev/null +++ b/src/Assets/StaticAssetUrlProvider.php @@ -0,0 +1,128 @@ +mode = $mode; + $this->local_asset_path = rtrim($local_asset_path, '/'); + + if ($this->mode === static::MODE_REMOTE) { + $this->remote_asset_url = $this->loadAssetBaseUrl($asset_base_url_file); + } + } + + protected function loadAssetBaseUrl(string $asset_base_url_file): string + { + if ( ! file_exists($asset_base_url_file)) { + throw new InvalidArgumentException('No asset base url file at '.$asset_base_url_file); + } + + $url = require $asset_base_url_file; + + if (empty($url) OR ! is_string($url)) { + throw new RuntimeException('Invalid content in asset base url file '.$asset_base_url_file); + } + + return $url; + } + + /** + * Generates a URL for a static asset, given its relative path. + * + * @param string $rel_path the path of the asset within the docroot / uploaded asset files + * + * @return string the full URL to render to the client + */ + public function getUrl(string $rel_path): string + { + if ($rel_path[0] !== '/') { + $rel_path = '/'.$rel_path; + } + + if ($this->mode === static::MODE_LOCAL) { + return $this->getLocalTimestampedUrl($rel_path); + } else { + return $this->remote_asset_url.$rel_path; + } + } + + /** + * @param string $rel_path + * + * @return string + */ + protected function getLocalTimestampedUrl(string $rel_path): string + { + $local_path = $this->local_asset_path.$rel_path; + if ( ! file_exists($local_path)) { + throw new RuntimeException('Undefined asset file '.$local_path); + } + + return $rel_path.'?v='.filemtime($local_path); + } +} diff --git a/test/unit/Assets/StaticAssetUrlProviderTest.php b/test/unit/Assets/StaticAssetUrlProviderTest.php new file mode 100644 index 0000000..39d2e79 --- /dev/null +++ b/test/unit/Assets/StaticAssetUrlProviderTest.php @@ -0,0 +1,132 @@ + StaticAssetUrlProvider::MODE_LOCAL, + 'local_asset_path' => __DIR__, + 'asset_base_url_file' => __FILE__, + ]; + + /** + * @var vfsStreamDirectory + */ + protected $vfs; + + public function test_it_is_initialisable() + { + $this->assertInstanceOf(StaticAssetUrlProvider::class, $this->newSubject()); + } + + public function test_it_throws_in_invalid_mode() + { + $this->options['mode'] = 'some-junk'; + $this->expectException(\InvalidArgumentException::class); + $this->newSubject(); + } + + public function test_in_local_mode_get_url_throws_if_file_does_not_exist() + { + $subject = $this->newSubject(); + $this->expectException(\RuntimeException::class); + $subject->getUrl('assets/some-file.css'); + } + + /** + * @testWith ["assets/my-file.css"] + * ["/assets/my-file.css"] + */ + public function test_in_local_mode_get_url_returns_absolute_url_with_mtime_suffix($rel_path) + { + vfsStream::create( + [ + 'assets' => [ + 'my-file.css' => 'some-content', + ], + ], + $this->vfs->getChild('docroot') + ); + $this->vfs->getChild('docroot/assets/my-file.css')->lastModified(123456); + $this->assertSame( + '/assets/my-file.css?v=123456', + $this->newSubject()->getUrl($rel_path) + ); + } + + public function test_in_remote_mode_get_url_throws_if_asset_base_url_file_does_not_exist() + { + $this->options['asset_base_url_file'] = $this->vfs->url().'/no-such-file.php'; + $this->options['mode'] = StaticAssetUrlProvider::MODE_REMOTE; + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('no-such-file.php'); + $this->newSubject(); + } + + /** + * @testWith [""] + * ["some content that is not php"] + * [" $file_content], + $this->vfs + ); + + $this->options['asset_base_url_file'] = $this->vfs->getChild('asset-base-url.php')->url(); + $this->options['mode'] = StaticAssetUrlProvider::MODE_REMOTE; + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Invalid content in asset base url'); + $this->newSubject(); + } + + /** + * @testWith ["assets/my-file.css"] + * ["/assets/my-file.css"] + */ + public function test_it_remote_mode_get_url_returns_url_prefixed_with_base_url($rel_path) + { + vfsStream::create( + ['asset-base-url.php' => 'vfs + ); + + $this->options['asset_base_url_file'] = $this->vfs->getChild('asset-base-url.php')->url(); + $this->options['mode'] = StaticAssetUrlProvider::MODE_REMOTE; + + $this->assertSame( + 'https://i.am.the.walrus/branch/sha/assets/my-file.css', + $this->newSubject()->getUrl($rel_path) + ); + } + + protected function setUp() + { + parent::setUp(); + $this->vfs = vfsStream::setup('vfs', NULL, ['docroot' => []]); + $this->options['local_asset_path'] = $this->vfs->getChild('docroot')->url(); + $this->options['asset_base_url_file'] = $this->vfs->url().'/asset-base-url.php'; + } + + protected function newSubject() + { + return new StaticAssetUrlProvider( + $this->options['mode'], + $this->options['local_asset_path'], + $this->options['asset_base_url_file'] + ); + } + +} diff --git a/test/unit/DeploymentConfig/ConfigValueDecrypterTest.php b/test/unit/DeploymentConfig/ConfigValueDecrypterTest.php index 0a46352..034e85a 100644 --- a/test/unit/DeploymentConfig/ConfigValueDecrypterTest.php +++ b/test/unit/DeploymentConfig/ConfigValueDecrypterTest.php @@ -145,10 +145,11 @@ public function test_it_throws_if_required_decryption_key_is_not_present() $valid_encrypted_val = sodium_crypto_box_seal('whoops', sodium_crypto_box_publickey($kp)); // The actual encrypted value is fine, but the problem is that the keypair it specifies isn't present with the // correct name. - $subject = $this->newSubject(); + $other_keypair_name = uniqid('very'); + $subject = $this->newSubject(); $this->expectException(InvalidConfigException::class); $this->expectExceptionMessage('Unknown config decryption key'); - $subject->decrypt('#SECRET-very#'.$valid_encrypted_val); + $subject->decrypt("#SECRET-$other_keypair_name#$valid_encrypted_val"); } public function provider_failed_decrypt()