Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.75% covered (success)
93.75%
30 / 32
75.00% covered (warning)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
Assets
93.75% covered (success)
93.75%
30 / 32
75.00% covered (warning)
75.00%
3 / 4
11.03
0.00% covered (danger)
0.00%
0 / 1
 asset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 staticAsset
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 vendor
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 serve
89.47% covered (warning)
89.47%
17 / 19
0.00% covered (danger)
0.00%
0 / 1
7.06
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\Controller\Panel;
6
7use Celema\Core\Exception\HttpNotFound;
8use Celema\Core\Factory\Factory;
9use Celema\Core\Request;
10use Celema\Core\Response;
11use Cosray\Exception\RuntimeException;
12use Cosray\Plugin\Assets as PluginAssets;
13use Cosray\Util\Path;
14
15final class Assets extends Panel
16{
17    private const array PUBLIC_EXTENSIONS = [
18        'css',
19        'js',
20        'mjs',
21        'svg',
22        'png',
23        'jpg',
24        'jpeg',
25        'webp',
26        'woff2',
27        'json',
28        'map',
29    ];
30
31    public function asset(Request $request, Factory $factory, string $slug): Response
32    {
33        return $this->serve($request, $factory, $this->panelDir, $slug);
34    }
35
36    public function staticAsset(Request $request, Factory $factory, string $slug): Response
37    {
38        return $this->serve(
39            $request,
40            $factory,
41            $this->panelAssetsDir(),
42            $slug,
43            self::PUBLIC_EXTENSIONS,
44            cacheControl: 'private, no-cache',
45        );
46    }
47
48    public function vendor(Request $request, Factory $factory, string $plugin, string $slug): Response
49    {
50        $dir = $this->container->get(PluginAssets::class)->dir($plugin);
51
52        if ($dir === null) {
53            throw new HttpNotFound($request);
54        }
55
56        return $this->serve($request, $factory, $dir, $slug, self::PUBLIC_EXTENSIONS);
57    }
58
59    private function serve(
60        Request $request,
61        Factory $factory,
62        string $root,
63        string $slug,
64        array $extensions = ['css', 'js', 'svg'],
65        string $cacheControl = 'private, max-age=3600',
66    ): Response {
67        try {
68            $file = Path::inside($root, $slug, checkIsFile: true);
69        } catch (RuntimeException $e) {
70            throw new HttpNotFound($request, previous: $e);
71        }
72
73        $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
74
75        if (!in_array($ext, $extensions, true)) {
76            throw new HttpNotFound($request);
77        }
78
79        $etag = md5_file($file);
80        $lastModified = filemtime($file);
81
82        if ($etag === false || $lastModified === false) {
83            throw new HttpNotFound($request);
84        }
85
86        $etag = '"' . $etag . '"';
87        $response = Response::create($factory)
88            ->header('Cache-Control', $cacheControl)
89            ->header('ETag', $etag)
90            ->header('Last-Modified', gmdate('D, d M Y H:i:s', $lastModified) . ' GMT');
91        $ifNoneMatch = array_map('trim', explode(',', $request->header('If-None-Match')));
92
93        // Return 304 when the client already has this asset revision cached.
94        if (in_array('*', $ifNoneMatch, true) || in_array($etag, $ifNoneMatch, true)) {
95            return $response->status(304);
96        }
97
98        return $response->file($file);
99    }
100}