├── vendor ├── composer │ ├── autoload_namespaces.php │ ├── autoload_psr4.php │ ├── autoload_classmap.php │ ├── platform_check.php │ ├── installed.php │ ├── autoload_real.php │ ├── autoload_static.php │ ├── InstalledVersions.php │ └── ClassLoader.php └── autoload.php ├── LICENSE ├── index.php ├── composer.json ├── classes ├── FingerprintFile.php └── Fingerprint.php └── readme.md /vendor/composer/autoload_namespaces.php: -------------------------------------------------------------------------------- 1 | array($vendorDir . '/getkirby/composer-installer/src'), 10 | 'Bnomei\\' => array($baseDir . '/classes'), 11 | ); 12 | -------------------------------------------------------------------------------- /vendor/autoload.php: -------------------------------------------------------------------------------- 1 | $baseDir . '/classes/Fingerprint.php', 10 | 'Bnomei\\FingerprintFile' => $baseDir . '/classes/FingerprintFile.php', 11 | 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', 12 | 'Kirby\\ComposerInstaller\\CmsInstaller' => $vendorDir . '/getkirby/composer-installer/src/ComposerInstaller/CmsInstaller.php', 13 | 'Kirby\\ComposerInstaller\\Installer' => $vendorDir . '/getkirby/composer-installer/src/ComposerInstaller/Installer.php', 14 | 'Kirby\\ComposerInstaller\\Plugin' => $vendorDir . '/getkirby/composer-installer/src/ComposerInstaller/Plugin.php', 15 | 'Kirby\\ComposerInstaller\\PluginInstaller' => $vendorDir . '/getkirby/composer-installer/src/ComposerInstaller/PluginInstaller.php', 16 | ); 17 | -------------------------------------------------------------------------------- /vendor/composer/platform_check.php: -------------------------------------------------------------------------------- 1 | = 80200)) { 8 | $issues[] = 'Your Composer dependencies require a PHP version ">= 8.2.0". You are running ' . PHP_VERSION . '.'; 9 | } 10 | 11 | if ($issues) { 12 | if (!headers_sent()) { 13 | header('HTTP/1.1 500 Internal Server Error'); 14 | } 15 | if (!ini_get('display_errors')) { 16 | if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { 17 | fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL); 18 | } elseif (!headers_sent()) { 19 | echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; 20 | } 21 | } 22 | trigger_error( 23 | 'Composer detected issues in your platform: ' . implode(' ', $issues), 24 | E_USER_ERROR 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Bruno Meilick 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /vendor/composer/installed.php: -------------------------------------------------------------------------------- 1 | array( 3 | 'name' => 'bnomei/kirby3-fingerprint', 4 | 'pretty_version' => '5.2.1', 5 | 'version' => '5.2.1.0', 6 | 'reference' => null, 7 | 'type' => 'kirby-plugin', 8 | 'install_path' => __DIR__ . '/../../', 9 | 'aliases' => array(), 10 | 'dev' => false, 11 | ), 12 | 'versions' => array( 13 | 'bnomei/kirby3-fingerprint' => array( 14 | 'pretty_version' => '5.2.1', 15 | 'version' => '5.2.1.0', 16 | 'reference' => null, 17 | 'type' => 'kirby-plugin', 18 | 'install_path' => __DIR__ . '/../../', 19 | 'aliases' => array(), 20 | 'dev_requirement' => false, 21 | ), 22 | 'getkirby/composer-installer' => array( 23 | 'pretty_version' => '1.2.1', 24 | 'version' => '1.2.1.0', 25 | 'reference' => 'c98ece30bfba45be7ce457e1102d1b169d922f3d', 26 | 'type' => 'composer-plugin', 27 | 'install_path' => __DIR__ . '/../getkirby/composer-installer', 28 | 'aliases' => array(), 29 | 'dev_requirement' => false, 30 | ), 31 | ), 32 | ); 33 | -------------------------------------------------------------------------------- /vendor/composer/autoload_real.php: -------------------------------------------------------------------------------- 1 | register(true); 35 | 36 | return $loader; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | css($url, $attrs); 12 | } 13 | } 14 | 15 | if (! function_exists('js_f')) { 16 | function js_f(File|FileVersion|string $url, array $attrs = []): ?string 17 | { 18 | return (new \Bnomei\Fingerprint)->js($url, $attrs); 19 | } 20 | } 21 | 22 | if (! function_exists('url_f')) { 23 | function url_f(File|FileVersion|string $url): string 24 | { 25 | return (new \Bnomei\Fingerprint)->url($url); 26 | } 27 | } 28 | 29 | Kirby::plugin('bnomei/fingerprint', [ 30 | 'options' => [ 31 | 'cache' => true, 32 | 'query' => true, 33 | 'digest' => 'sha384', 34 | 'https' => function () { 35 | return kirby()->system()->isLocal() === false; 36 | }, 37 | 'hash' => function ($file, $query = true) { 38 | return (new \Bnomei\FingerprintFile($file))->hash($query); 39 | }, 40 | 'integrity' => function ($file, ?string $digest = null, ?string $manifest = null) { 41 | return (new \Bnomei\FingerprintFile($file))->integrity($digest, $manifest); 42 | }, 43 | 'ignore-missing-auto' => true, 44 | 'absolute' => true, 45 | ], 46 | 'fileMethods' => [ 47 | 'fingerprint' => function () { 48 | return (new \Bnomei\Fingerprint)->process($this)['hash']; 49 | }, 50 | 'integrity' => function () { 51 | return (new \Bnomei\Fingerprint)->process($this)['integrity']; 52 | }, 53 | ], 54 | ]); 55 | -------------------------------------------------------------------------------- /vendor/composer/autoload_static.php: -------------------------------------------------------------------------------- 1 | 11 | array ( 12 | 'Kirby\\' => 6, 13 | ), 14 | 'B' => 15 | array ( 16 | 'Bnomei\\' => 7, 17 | ), 18 | ); 19 | 20 | public static $prefixDirsPsr4 = array ( 21 | 'Kirby\\' => 22 | array ( 23 | 0 => __DIR__ . '/..' . '/getkirby/composer-installer/src', 24 | ), 25 | 'Bnomei\\' => 26 | array ( 27 | 0 => __DIR__ . '/../..' . '/classes', 28 | ), 29 | ); 30 | 31 | public static $classMap = array ( 32 | 'Bnomei\\Fingerprint' => __DIR__ . '/../..' . '/classes/Fingerprint.php', 33 | 'Bnomei\\FingerprintFile' => __DIR__ . '/../..' . '/classes/FingerprintFile.php', 34 | 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', 35 | 'Kirby\\ComposerInstaller\\CmsInstaller' => __DIR__ . '/..' . '/getkirby/composer-installer/src/ComposerInstaller/CmsInstaller.php', 36 | 'Kirby\\ComposerInstaller\\Installer' => __DIR__ . '/..' . '/getkirby/composer-installer/src/ComposerInstaller/Installer.php', 37 | 'Kirby\\ComposerInstaller\\Plugin' => __DIR__ . '/..' . '/getkirby/composer-installer/src/ComposerInstaller/Plugin.php', 38 | 'Kirby\\ComposerInstaller\\PluginInstaller' => __DIR__ . '/..' . '/getkirby/composer-installer/src/ComposerInstaller/PluginInstaller.php', 39 | ); 40 | 41 | public static function getInitializer(ClassLoader $loader) 42 | { 43 | return \Closure::bind(function () use ($loader) { 44 | $loader->prefixLengthsPsr4 = ComposerStaticInitbae460cdf0be72ff2112fe50496f281e::$prefixLengthsPsr4; 45 | $loader->prefixDirsPsr4 = ComposerStaticInitbae460cdf0be72ff2112fe50496f281e::$prefixDirsPsr4; 46 | $loader->classMap = ComposerStaticInitbae460cdf0be72ff2112fe50496f281e::$classMap; 47 | 48 | }, null, ClassLoader::class); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bnomei/kirby3-fingerprint", 3 | "type": "kirby-plugin", 4 | "version": "5.2.1", 5 | "description": "File Method and css/js helper to add cache-busting hash and optional Subresource Integrity to file", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Bruno Meilick", 10 | "email": "b@bnomei.com" 11 | } 12 | ], 13 | "keywords": [ 14 | "kirby", 15 | "kirby-cms", 16 | "kirby-plugin", 17 | "fingerprint", 18 | "hash", 19 | "cache-buster", 20 | "subresource-integrity", 21 | "manifest", 22 | "manifest-file", 23 | "json", 24 | "assets" 25 | ], 26 | "config": { 27 | "optimize-autoloader": true, 28 | "sort-packages": true, 29 | "allow-plugins": { 30 | "getkirby/composer-installer": true, 31 | "pestphp/pest-plugin": true 32 | } 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "Bnomei\\": "classes/" 37 | } 38 | }, 39 | "require": { 40 | "php": ">=8.2", 41 | "getkirby/composer-installer": "^1.2" 42 | }, 43 | "require-dev": { 44 | "getkirby/cms": "^5.0.0-alpha.4", 45 | "larastan/larastan": "^v3.0.0", 46 | "laravel/pint": "^1.13", 47 | "pestphp/pest": "^v3.5.1", 48 | "spatie/ray": "^1.39" 49 | }, 50 | "scripts": { 51 | "stan": "./vendor/bin/phpstan", 52 | "fix": "./vendor/bin/pint", 53 | "test": "./vendor/bin/pest --profile", 54 | "dist": [ 55 | "composer fix", 56 | "composer install --no-dev --optimize-autoloader", 57 | "git rm -rf --cached .; git add .;" 58 | ], 59 | "kirby": [ 60 | "composer install", 61 | "composer update", 62 | "composer install --working-dir=tests/kirby --no-dev --optimize-autoloader", 63 | "composer update --working-dir=tests/kirby", 64 | "sed -i.bak 's/function dump(/function xdump(/g' tests/kirby/config/helpers.php", 65 | "sed -i.bak 's/function e(/function xe(/g' tests/kirby/config/helpers.php" 66 | ] 67 | }, 68 | "extra": { 69 | "kirby-cms-path": "tests/kirby" 70 | }, 71 | "suggest": { 72 | "bnomei/kirby3-security-headers": "Let's make the web a saver place – sensible defaults included.", 73 | "diverently/laravel-mix-kirby": "Consider using this plugin instead if all your assets are versioned by laravel mix." 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /classes/FingerprintFile.php: -------------------------------------------------------------------------------- 1 | original(); 30 | $this->kirbyFile = $o; 31 | $this->file = $this->kirbyFile->url(); 32 | } elseif ($file instanceof File) { 33 | $this->kirbyFile = $file; 34 | $this->file = $this->kirbyFile->url(); 35 | } else { 36 | $this->file = url($file); 37 | } 38 | } 39 | 40 | public function id(): string 41 | { 42 | if ($this->kirbyFile) { 43 | return $this->kirbyFile->root() ?? ''; 44 | } 45 | 46 | return ltrim($this->file, '/'); 47 | } 48 | 49 | public function modified(): ?int 50 | { 51 | $root = $this->fileRoot(); 52 | if (! F::exists($root)) { 53 | return null; 54 | } 55 | 56 | $modified = null; 57 | if ($this->kirbyFile && function_exists('modified')) { 58 | // @codeCoverageIgnoreStart 59 | $modified = \modified($this->kirbyFile); 60 | if (! $modified) { 61 | $modified = $this->kirbyFile->modified(); 62 | } 63 | // @codeCoverageIgnoreEnd 64 | } else { 65 | $modified = F::modified($root); 66 | } 67 | 68 | return $modified; 69 | } 70 | 71 | public function hash(bool|string $query = true): string 72 | { 73 | $root = $this->fileRoot(); 74 | 75 | $filename = null; 76 | if (is_string($query) && F::exists($query)) { 77 | $filename = $this->filenameFromQuery($query, $root); 78 | } elseif (is_bool($query)) { 79 | if (! F::exists($root)) { 80 | return $this->file; 81 | } 82 | 83 | // Determine file suffix, either .. 84 | $suffix = $query 85 | // ... query string 86 | ? F::extension($root).'?v='.filemtime($root) 87 | // ... MD5 file hash 88 | : md5_file($root).'.'.F::extension($root); 89 | 90 | $filename = implode('.', [F::name($root), $suffix]); 91 | } 92 | 93 | if (! $filename) { 94 | throw new \Exception("File not found: $root"); 95 | } 96 | 97 | $url = null; 98 | if ($this->kirbyFile) { 99 | $url = str_replace($this->kirbyFile->filename(), $filename, $this->kirbyFile->url()); 100 | } else { 101 | $dirname = str_replace(kirby()->roots()->index(), '', dirname($root)); 102 | $url = ($dirname === '.') ? $filename : ($dirname.'/'.$filename); 103 | } 104 | 105 | return url($url); 106 | } 107 | 108 | public function integrity(?string $digest = null, ?string $manifest = null): ?string 109 | { 110 | $root = $this->fileRoot(); 111 | 112 | if (is_string($manifest) && F::exists($manifest)) { 113 | $filename = $this->filenameFromQuery($manifest, $root); 114 | $dest = str_replace(basename($root), $filename, $root); 115 | 116 | if (F::exists($dest)) { 117 | $root = $dest; 118 | } 119 | } 120 | 121 | if (! F::exists($root)) { 122 | return null; 123 | } 124 | 125 | // Select hashing algorithm 126 | if (! $digest || ! in_array($digest, ['sha256', 'sha384', 'sha512'])) { 127 | $digest = 'sha384'; 128 | } 129 | 130 | $data = F::read($root); 131 | if (! $data) { 132 | return null; 133 | } 134 | 135 | $hash = hash($digest, $data, true); 136 | // .. encode hash using 'base64' 137 | $b64 = base64_encode($hash); 138 | 139 | // Glue everything together, forming an SRI string 140 | return "{$digest}-{$b64}"; 141 | } 142 | 143 | public function fileRoot(): string 144 | { 145 | if ($this->kirbyFile) { 146 | return $this->kirbyFile->root() ?? ''; 147 | } 148 | 149 | $url = kirby()->site()->url(); 150 | 151 | if ($lang = kirby()->language()) { 152 | $url = preg_replace('/\/'.$lang->code().'$/', '', $url); 153 | } 154 | 155 | if (! $url) { 156 | return ''; 157 | } 158 | 159 | $path = ltrim($url, DIRECTORY_SEPARATOR); 160 | $uri = ltrim(str_replace($path, '', $this->file), DIRECTORY_SEPARATOR); 161 | 162 | return kirby()->roots()->index().DIRECTORY_SEPARATOR.$uri; 163 | } 164 | 165 | public function file(): File|string 166 | { 167 | return $this->kirbyFile ?? $this->file; 168 | } 169 | 170 | public function filenameFromQuery(string $query, string $root): string 171 | { 172 | $manifest = Json::read($query); 173 | $url = ''; 174 | 175 | if (kirby()->language()) { 176 | $url = preg_replace('/\/'.kirby()->language()->code().'$/', '', kirby()->site()->url()); 177 | } 178 | 179 | $url = str_replace($url ?? '', '', $this->id()); 180 | 181 | $hasLeadingSlash = Str::substr(array_keys($manifest)[0], 0, 1) === '/'; 182 | $url = Url::path($url, $hasLeadingSlash); 183 | 184 | return basename(A::get( 185 | $manifest, 186 | $url, 187 | $root 188 | )); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /classes/Fingerprint.php: -------------------------------------------------------------------------------- 1 | option('debug'), 23 | 'query' => option('bnomei.fingerprint.query'), 24 | 'hash' => option('bnomei.fingerprint.hash'), 25 | 'integrity' => option('bnomei.fingerprint.integrity'), 26 | 'digest' => option('bnomei.fingerprint.digest'), 27 | 'https' => option('bnomei.fingerprint.https'), 28 | 'ignore-missing-auto' => option('bnomei.fingerprint.ignore-missing-auto'), 29 | 'absolute' => option('bnomei.fingerprint.absolute'), 30 | ]; 31 | $this->options = array_merge($defaults, $options); 32 | 33 | foreach ($this->options as $key => $call) { 34 | if ($call instanceof \Closure && ! in_array($key, ['hash', 'integrity'])) { 35 | $this->options[$key] = $call(); 36 | } 37 | } 38 | 39 | if ($this->option('debug')) { 40 | try { 41 | kirby()->cache('bnomei.fingerprint')->flush(); 42 | } catch (Exception $e) { 43 | // 44 | } 45 | } 46 | } 47 | 48 | public function option(?string $key = null): mixed 49 | { 50 | if ($key) { 51 | return A::get($this->options, $key); 52 | } 53 | 54 | return $this->options; 55 | } 56 | 57 | public function apply(string $option, File|FileVersion|string $file): mixed 58 | { 59 | $callback = $this->option($option); 60 | 61 | if ($callback && is_callable($callback)) { 62 | if ($option === 'integrity') { 63 | return call_user_func_array($callback, [$file, $this->option('digest'), $this->option('query')]); 64 | } elseif ($option === 'hash') { 65 | return call_user_func_array($callback, [$file, $this->option('query')]); 66 | } 67 | } 68 | 69 | return null; 70 | } 71 | 72 | public function https(string $url): string 73 | { 74 | if ($this->option('https') && ! kirby()->system()->isLocal()) { 75 | $url = str_replace('http://', 'https://', $url); 76 | } 77 | 78 | if ($this->option('absolute') === false && kirby()->url() !== '/') { // in CLI 79 | $url = str_replace(rtrim(kirby()->url(), '/'), '', $url); 80 | } 81 | 82 | return $url; 83 | } 84 | 85 | public function process(File|FileVersion|string $file): array 86 | { 87 | $needsPush = false; 88 | $lookup = $this->read(); 89 | if (! $lookup) { 90 | $lookup = []; 91 | $needsPush = true; 92 | } 93 | 94 | $finFile = new FingerprintFile($file); 95 | $id = $finFile->id(); 96 | $mod = $finFile->modified(); 97 | 98 | if (! array_key_exists($id, $lookup)) { 99 | $needsPush = true; 100 | } elseif ($mod && $lookup[$id]['modified'] < $mod) { 101 | $needsPush = true; 102 | } 103 | 104 | if ($needsPush) { 105 | $lookup[$id] = [ 106 | 'modified' => $mod, 107 | 'root' => $finFile->fileRoot(), 108 | 'integrity' => $this->apply('integrity', $file), 109 | 'hash' => $this->apply('hash', $file), 110 | ]; 111 | 112 | $this->write($lookup); 113 | } 114 | 115 | return A::get($lookup, $id); 116 | } 117 | 118 | public function attrs(array $attrs, array $lookup): array 119 | { 120 | $sri = A::get($attrs, 'integrity', false); 121 | if ($sri === true) { 122 | $sri = A::get($lookup, 'integrity'); 123 | } 124 | if ($sri && strlen($sri) > 0) { 125 | $attrs['integrity'] = $sri; 126 | $attrs['crossorigin'] = A::get($attrs, 'crossorigin', 'anonymous'); 127 | } elseif (! $sri) { 128 | if (array_key_exists('integrity', $attrs)) { 129 | unset($attrs['integrity']); 130 | } 131 | if (array_key_exists('crossorigin', $attrs)) { 132 | unset($attrs['crossorigin']); 133 | } 134 | } 135 | 136 | return $attrs; 137 | } 138 | 139 | public function helper(string $extension, File|FileVersion|string $url, array $attrs = []): ?string 140 | { 141 | if (! is_callable($extension)) { 142 | return null; 143 | } 144 | 145 | if ($url === '@auto') { 146 | $assetUrl = Url::toTemplateAsset($extension.'/templates', $extension); 147 | if ($assetUrl) { 148 | $url = $assetUrl; 149 | } elseif (! $assetUrl && $this->option('ignore-missing-auto')) { 150 | return null; 151 | } 152 | } 153 | 154 | $lookup = $this->process($url); 155 | $attrs = $this->attrs($attrs, $lookup); 156 | 157 | return $this->https($extension($lookup['hash'], $attrs)); 158 | } 159 | 160 | public function cacheKey(): string 161 | { 162 | return implode('-', [ 163 | 'lookup', 164 | str_replace('.', '-', kirby()->plugin('bnomei/fingerprint')?->version() ?? '0.0.0'), 165 | $this->option('query') ? 'query' : 'redirect', 166 | ]); 167 | } 168 | 169 | public function read(): ?array 170 | { 171 | if ($this->option('debug')) { 172 | return null; 173 | } 174 | 175 | return kirby()->cache('bnomei.fingerprint')->get($this->cacheKey()); 176 | } 177 | 178 | private function write(array $lookup): bool 179 | { 180 | if ($this->option('debug')) { 181 | return false; 182 | } 183 | 184 | return kirby()->cache('bnomei.fingerprint')->set($this->cacheKey(), $lookup); 185 | } 186 | 187 | public static function css(File|FileVersion|string $url, string|array $attrs = []): ?string 188 | { 189 | if (is_string($attrs)) { 190 | $attrs = ['media' => $attrs]; 191 | } 192 | 193 | return (new Fingerprint)->helper('css', $url, $attrs); 194 | } 195 | 196 | public static function js(File|FileVersion|string $url, array $attrs = []): ?string 197 | { 198 | return (new Fingerprint)->helper('js', $url, $attrs); 199 | } 200 | 201 | public static function url(File|FileVersion|string $url): string 202 | { 203 | $fingerprint = new Fingerprint; 204 | $url = $fingerprint->process($url)['hash']; 205 | 206 | return $fingerprint->https($url); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Kirby Fingerprint 2 | 3 | [![Kirby 5](https://flat.badgen.net/badge/Kirby/5?color=ECC748)](https://getkirby.com) 4 | ![PHP 8.2](https://flat.badgen.net/badge/PHP/8.2?color=4E5B93&icon=php&label) 5 | ![Release](https://flat.badgen.net/packagist/v/bnomei/kirby3-fingerprint?color=ae81ff&icon=github&label) 6 | ![Downloads](https://flat.badgen.net/packagist/dt/bnomei/kirby3-fingerprint?color=272822&icon=github&label) 7 | [![Coverage](https://flat.badgen.net/codeclimate/coverage/bnomei/kirby3-fingerprint?icon=codeclimate&label)](https://codeclimate.com/github/bnomei/kirby3-fingerprint) 8 | [![Maintainability](https://flat.badgen.net/codeclimate/maintainability/bnomei/kirby3-fingerprint?icon=codeclimate&label)](https://codeclimate.com/github/bnomei/kirby3-fingerprint/issues) 9 | [![Discord](https://flat.badgen.net/badge/discord/bnomei?color=7289da&icon=discord&label)](https://discordapp.com/users/bnomei) 10 | [![Buymecoffee](https://flat.badgen.net/badge/icon/donate?icon=buymeacoffee&color=FF813F&label)](https://www.buymeacoffee.com/bnomei) 11 | 12 | 13 | File Method and css/js helper to add a cache busting hash and optional [Subresource Integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) to files. 14 | 15 | ## Installation 16 | 17 | - unzip [master.zip](https://github.com/bnomei/kirby3-fingerprint/archive/master.zip) as folder `site/plugins/kirby3-fingerprint` or 18 | - `git submodule add https://github.com/bnomei/kirby3-fingerprint.git site/plugins/kirby3-fingerprint` or 19 | - `composer require bnomei/kirby3-fingerprint` 20 | 21 | ## Usage 22 | 23 | > [!WARNING] 24 | > This Plugin does **not** override the build in `js()`/`css()` helpers. Use `css_f`/`Bnomei\Fingerprint::css` and `js_f`/`Bnomei\Fingerprint::js` when you need them. 25 | 26 | ```php 27 | echo css_f('/assets/css/index.css'); 28 | echo Bnomei\Fingerprint::css('/assets/css/index.css'); 29 | //