Skip to content

Commit

Permalink
Merge pull request #32 from rubanooo/add-nonce-option
Browse files Browse the repository at this point in the history
add nonce option
  • Loading branch information
freekmurze authored Jan 17, 2023
2 parents 816f04c + f8c0d5f commit 593a74d
Show file tree
Hide file tree
Showing 11 changed files with 621 additions and 31 deletions.
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ return [
<head>
{{-- Loads Inter --}}
@googlefonts
{{-- Loads IBM Plex Mono --}}
@googlefonts('code')
</head>
Expand All @@ -155,6 +155,22 @@ If you want to make sure fonts are ready to go before anyone visits your site, y
php artisan google-fonts:fetch
```

## Usage with spatie/laravel-csp

If you're using [spatie/laravel-csp](https://github.com/spatie/laravel-csp) to manage your Content Security Policy, you can pass an array to the blade directive and add the `nonce` option.

```blade
{{-- resources/views/layouts/app.blade.php --}}
<head>
{{-- Loads Inter --}}
@googlefonts(['nonce' => csp_nonce()])
{{-- Loads IBM Plex Mono --}}
@googlefonts(['font' => 'code', 'nonce' => csp_nonce()])
</head>
```

### Caveats for legacy browsers

Google Fonts' servers sniff the visitor's user agent header to determine which font format to serve. This means fonts work in all modern and legacy browsers.
Expand Down
2 changes: 1 addition & 1 deletion src/Commands/FetchGoogleFontsCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public function handle()
->each(function (string $font) {
$this->info("Fetching `{$font}`...");

app(GoogleFonts::class)->load($font, forceDownload: true);
app(GoogleFonts::class)->load(compact('font'), forceDownload: true);
});

$this->info('All done!');
Expand Down
48 changes: 39 additions & 9 deletions src/Fonts.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,50 +3,71 @@
namespace Spatie\GoogleFonts;

use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Support\Collection;
use Illuminate\Support\HtmlString;

class Fonts implements Htmlable
{
public function __construct(
protected string $googleFontsUrl,
protected string $googleFontsUrl,
protected ?string $localizedUrl = null,
protected ?string $localizedCss = null,
protected bool $preferInline = false,
) {
protected ?string $nonce = null,
protected bool $preferInline = false,
)
{
}

public function inline(): HtmlString
{
if (! $this->localizedCss) {
if (!$this->localizedCss) {
return $this->fallback();
}

$attributes = $this->parseAttributes([
'nonce' => $this->nonce ?? false,
]);

return new HtmlString(<<<HTML
<style>{$this->localizedCss}</style>
<style {$attributes->implode(' ')}>{$this->localizedCss}</style>
HTML);
}

public function link(): HtmlString
{
if (! $this->localizedUrl) {
if (!$this->localizedUrl) {
return $this->fallback();
}

$attributes = $this->parseAttributes([
'href' => $this->localizedUrl,
'rel' => 'stylesheet',
'type' => 'text/css',
'nonce' => $this->nonce ?? false,
]);

return new HtmlString(<<<HTML
<link href="{$this->localizedUrl}" rel="stylesheet" type="text/css">
<link {$attributes->implode(' ')}>
HTML);
}

public function fallback(): HtmlString
{
$attributes = $this->parseAttributes([
'href' => $this->googleFontsUrl,
'rel' => 'stylesheet',
'type' => 'text/css',
'nonce' => $this->nonce ?? false,
]);

return new HtmlString(<<<HTML
<link href="{$this->googleFontsUrl}" rel="stylesheet" type="text/css">
<link {$attributes->implode(' ')}>
HTML);
}

public function url(): string
{
if (! $this->localizedUrl) {
if (!$this->localizedUrl) {
return $this->googleFontsUrl;
}

Expand All @@ -57,4 +78,13 @@ public function toHtml(): HtmlString
{
return $this->preferInline ? $this->inline() : $this->link();
}

protected function parseAttributes($attributes): Collection
{
return Collection::make($attributes)
->reject(fn ($value, $key) => in_array($value, [false, null], true))
->flatMap(fn ($value, $key) => $value === true ? [$key] : [$key => $value])
->map(fn ($value, $key) => is_int($key) ? $value : $key . '="' . $value . '"')
->values();
}
}
38 changes: 27 additions & 11 deletions src/GoogleFonts.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,38 +20,40 @@ public function __construct(
) {
}

public function load(string $font = 'default', bool $forceDownload = false): Fonts
public function load(string|array $options = [], bool $forceDownload = false): Fonts
{
if (! isset($this->fonts[$font])) {
['font' => $font, 'nonce' => $nonce] = $this->parseOptions($options);

if (!isset($this->fonts[$font])) {
throw new RuntimeException("Font `{$font}` doesn't exist");
}

$url = $this->fonts[$font];

try {
if ($forceDownload) {
return $this->fetch($url);
return $this->fetch($url, $nonce);
}

$fonts = $this->loadLocal($url);
$fonts = $this->loadLocal($url, $nonce);

if (! $fonts) {
return $this->fetch($url);
if (!$fonts) {
return $this->fetch($url, $nonce);
}

return $fonts;
} catch (Exception $exception) {
if (! $this->fallback) {
if (!$this->fallback) {
throw $exception;
}

return new Fonts(googleFontsUrl: $url);
return new Fonts(googleFontsUrl: $url, nonce: $nonce);
}
}

protected function loadLocal(string $url): ?Fonts
protected function loadLocal(string $url, ?string $nonce): ?Fonts
{
if (! $this->filesystem->exists($this->path($url, 'fonts.css'))) {
if (!$this->filesystem->exists($this->path($url, 'fonts.css'))) {
return null;
}

Expand All @@ -61,11 +63,12 @@ protected function loadLocal(string $url): ?Fonts
googleFontsUrl: $url,
localizedUrl: $this->filesystem->url($this->path($url, 'fonts.css')),
localizedCss: $localizedCss,
nonce: $nonce,
preferInline: $this->inline,
);
}

protected function fetch(string $url): Fonts
protected function fetch(string $url, ?string $nonce): Fonts
{
$css = Http::withHeaders(['User-Agent' => $this->userAgent])
->get($url)
Expand Down Expand Up @@ -94,6 +97,7 @@ protected function fetch(string $url): Fonts
googleFontsUrl: $url,
localizedUrl: $this->filesystem->url($this->path($url, 'fonts.css')),
localizedCss: $localizedCss,
nonce: $nonce,
preferInline: $this->inline,
);
}
Expand Down Expand Up @@ -123,4 +127,16 @@ protected function path(string $url, string $path = ''): string

return $segments->filter()->join('/');
}

protected function parseOptions(string|array $options): array
{
if (is_string($options)) {
$options = ['font' => $options, 'nonce' => null];
}

return [
'font' => $options['font'] ?? 'default',
'nonce' => $options['nonce'] ?? null,
];
}
}
38 changes: 29 additions & 9 deletions tests/GoogleFontsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
use function Spatie\Snapshots\assertMatchesFileSnapshot;
use function Spatie\Snapshots\assertMatchesHtmlSnapshot;

it('loads google fonts', function () {
$fonts = app(GoogleFonts::class)->load('inter', forceDownload: true);

it('loads google fonts', function ($arguments) {
$fonts = app(GoogleFonts::class)->load($arguments, forceDownload: true);

$expectedFileName = '952ee985ef/fonts.css';

Expand All @@ -23,18 +24,21 @@

expect($woff2FileCount)->toBeGreaterThan(0);

assertMatchesHtmlSnapshot((string)$fonts->link());
assertMatchesHtmlSnapshot((string)$fonts->inline());
assertMatchesHtmlSnapshot((string) $fonts->link());
assertMatchesHtmlSnapshot((string) $fonts->inline());

$expectedUrl = $this->disk()->url($expectedFileName);
expect($fonts->url())->toEqual($expectedUrl);
});
})->with([
'font_as_string' => 'inter',
'font_as_array' => ['font' => 'inter'],
]);

it('falls back to google fonts', function () {
it('falls back to google fonts', function ($arguments) {
config()->set('google-fonts.fonts', ['cow' => 'moo']);
config()->set('google-fonts.fallback', true);

$fonts = app(GoogleFonts::class)->load('cow', forceDownload: true);
$fonts = app(GoogleFonts::class)->load($arguments, forceDownload: true);

$allFiles = $this->disk()->allFiles();

Expand All @@ -45,8 +49,24 @@
HTML;

expect([
(string)$fonts->link(),
(string)$fonts->inline(),
(string) $fonts->link(),
(string) $fonts->inline(),
])->each->toEqual($fallback)
->and($fonts->url())->toEqual('moo');
})
->with([
'font_as_string' => 'cow',
'font_as_array' => ['font' => 'cow'],
]);;

it('adds the nonce attribute when specified', function () {
config()->set('google-fonts.fonts', ['cow' => 'moo']);
config()->set('google-fonts.fallback', true);

$fonts = app(GoogleFonts::class)->load(['font' => 'cow', 'nonce' => 'chicken'], forceDownload: true);

expect([
(string) $fonts->link(),
(string) $fonts->inline(),
])->each->toContain('nonce="chicken"');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<html><head><link href="/storage/952ee985ef/fonts.css" rel="stylesheet" type="text/css"></head></html>
Loading

0 comments on commit 593a74d

Please sign in to comment.