|
11 | 11 | namespace BK2K\BootstrapPackage\Service; |
12 | 12 |
|
13 | 13 | use Psr\Http\Message\ResponseInterface; |
| 14 | +use TYPO3\CMS\Core\Core\Environment; |
| 15 | +use TYPO3\CMS\Core\Crypto\HashService; |
14 | 16 | use TYPO3\CMS\Core\Http\RequestFactory; |
15 | 17 | use TYPO3\CMS\Core\Utility\GeneralUtility; |
16 | 18 |
|
|
20 | 22 | class GoogleFontService |
21 | 23 | { |
22 | 24 | /** |
23 | | - * @var string |
| 25 | + * Relative path from public directory for cached font files (must be web-accessible) |
24 | 26 | */ |
25 | | - protected $tempDirectory = 'typo3temp/assets/bootstrappackage/fonts'; |
| 27 | + protected const CACHE_DIRECTORY = 'typo3temp/assets/bootstrappackage/fonts'; |
26 | 28 |
|
27 | | - /** |
28 | | - * @var string |
29 | | - */ |
30 | | - protected $googleFontApiUrl = 'fonts.googleapis.com/css'; |
| 29 | + protected const GOOGLE_FONTS_HOST = 'fonts.googleapis.com'; |
31 | 30 |
|
32 | 31 | /** |
33 | | - * @param string $file |
34 | | - * @return string|null |
35 | | - * @throws \Exception |
| 32 | + * Allowed MIME types for font files |
| 33 | + * @var list<string> |
36 | 34 | */ |
37 | | - public function getCachedFile(string $file): ?string |
| 35 | + protected array $allowedFontMimeTypes = [ |
| 36 | + 'font/woff', |
| 37 | + 'font/woff2', |
| 38 | + 'font/ttf', |
| 39 | + 'font/otf', |
| 40 | + 'application/font-woff', |
| 41 | + 'application/font-woff2', |
| 42 | + 'application/x-font-woff', |
| 43 | + 'application/x-font-ttf', |
| 44 | + 'application/x-font-otf', |
| 45 | + 'application/vnd.ms-fontobject', |
| 46 | + 'font/sfnt', |
| 47 | + ]; |
| 48 | + |
| 49 | + public function getCachedFile(string $url): ?string |
38 | 50 | { |
39 | | - if (!$this->supports($file)) { |
| 51 | + if (!$this->supports($url)) { |
40 | 52 | return null; |
41 | 53 | } |
42 | | - if ($this->isCached($file)) { |
43 | | - return $this->getCssFileCacheName($file); |
| 54 | + if ($this->isCached($url)) { |
| 55 | + return $this->getCssFileCacheName($url); |
44 | 56 | } |
45 | | - return $this->cacheFile($file) ? $this->getCssFileCacheName($file) : null; |
| 57 | + return $this->cacheUrl($url) ? $this->getCssFileCacheName($url) : null; |
46 | 58 | } |
47 | 59 |
|
48 | | - /** |
49 | | - * @param string $file |
50 | | - * @return bool |
51 | | - */ |
52 | | - protected function cacheFile(string $file): bool |
| 60 | + protected function cacheUrl(string $url): bool |
53 | 61 | { |
54 | 62 | /** @var RequestFactory */ |
55 | 63 | $requestFactory = GeneralUtility::makeInstance(RequestFactory::class); |
56 | 64 |
|
57 | 65 | /** @var ResponseInterface */ |
58 | | - $response = $requestFactory->request($file, 'GET', [ |
| 66 | + $response = $requestFactory->request($url, 'GET', [ |
59 | 67 | 'headers' => ['User-Agent' => 'Mozilla/5.0 Gecko/20100101 Firefox/94.0 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36 Edg/97.0.1072.55'], |
60 | 68 | ]); |
61 | 69 |
|
62 | 70 | if ($response->getStatusCode() >= 300) { |
63 | 71 | return false; |
64 | | - } else { |
65 | | - $content = $response->getBody()->getContents(); |
66 | 72 | } |
67 | 73 |
|
| 74 | + $content = $response->getBody()->getContents(); |
| 75 | + |
68 | 76 | // Ensure cache directory exists |
69 | | - GeneralUtility::mkdir_deep($this->tempDirectory . '/' . $this->getCacheIdentifier($file)); |
| 77 | + GeneralUtility::mkdir_deep($this->getAbsoluteCacheDirectory($url)); |
70 | 78 |
|
71 | | - // Find and Download font files |
| 79 | + // Find and download font files |
72 | 80 | $pattern = '%url\((.*?)\)%'; |
73 | 81 | preg_match_all($pattern, $content, $matches, PREG_SET_ORDER); |
74 | | - $fontFiles = []; |
| 82 | + $fontUrls = []; |
75 | 83 | foreach ($matches as $match) { |
76 | | - $fontFiles[$match[1]] = $match[1]; |
| 84 | + $fontUrls[$match[1]] = $match[1]; |
77 | 85 | } |
78 | | - foreach ($fontFiles as $fontFile) { |
79 | | - $localFontFile = $this->getCacheDirectory($file) . '/' . basename($fontFile); |
| 86 | + foreach ($fontUrls as $fontUrl) { |
| 87 | + if (!$this->isValidHttpsUrl($fontUrl)) { |
| 88 | + continue; |
| 89 | + } |
| 90 | + |
80 | 91 | /** @var ResponseInterface */ |
81 | | - $response = $requestFactory->request($fontFile); |
| 92 | + $response = $requestFactory->request($fontUrl); |
82 | 93 | if ($response->getStatusCode() >= 300) { |
83 | 94 | continue; |
84 | | - } else { |
85 | | - $fontFileContent = $response->getBody()->getContents(); |
86 | 95 | } |
| 96 | + |
| 97 | + // Validate MIME type of the response |
| 98 | + if (!$this->isAllowedFontMimeType($response)) { |
| 99 | + continue; |
| 100 | + } |
| 101 | + |
| 102 | + $fontFileContent = $response->getBody()->getContents(); |
| 103 | + $localFontFile = $this->getAbsoluteCacheDirectory($url) . '/' . basename($fontUrl); |
87 | 104 | file_put_contents($localFontFile, $fontFileContent); |
88 | 105 | GeneralUtility::fixPermissions($localFontFile); |
89 | | - $content = str_replace($fontFile, basename($fontFile), $content); |
| 106 | + $content = str_replace($fontUrl, basename($fontUrl), $content); |
90 | 107 | } |
91 | 108 |
|
92 | | - // Save CSS File |
93 | | - $cacheFile = $this->getCacheDirectory($file) . '/' . 'webfont.css'; |
94 | | - file_put_contents(GeneralUtility::getFileAbsFileName($cacheFile), $content); |
| 109 | + // Save CSS file |
| 110 | + $cacheFile = $this->getAbsoluteCacheDirectory($url) . '/webfont.css'; |
| 111 | + file_put_contents($cacheFile, $content); |
95 | 112 | GeneralUtility::fixPermissions($cacheFile); |
96 | 113 |
|
97 | 114 | return true; |
98 | 115 | } |
99 | 116 |
|
| 117 | + protected function isValidHttpsUrl(string $url): bool |
| 118 | + { |
| 119 | + return filter_var($url, FILTER_VALIDATE_URL) !== false |
| 120 | + && str_starts_with($url, 'https://'); |
| 121 | + } |
| 122 | + |
100 | 123 | /** |
101 | | - * @param string $file |
102 | | - * @return bool |
| 124 | + * Check if the response has an allowed font MIME type |
103 | 125 | */ |
104 | | - protected function supports(string $file): bool |
| 126 | + protected function isAllowedFontMimeType(ResponseInterface $response): bool |
105 | 127 | { |
106 | | - return (bool) strpos($file, $this->googleFontApiUrl); |
| 128 | + $contentType = $response->getHeaderLine('Content-Type'); |
| 129 | + if ($contentType === '') { |
| 130 | + return false; |
| 131 | + } |
| 132 | + |
| 133 | + // Extract MIME type without charset or other parameters |
| 134 | + $mimeType = strtolower(trim(explode(';', $contentType)[0])); |
| 135 | + |
| 136 | + return in_array($mimeType, $this->allowedFontMimeTypes, true); |
107 | 137 | } |
108 | 138 |
|
109 | 139 | /** |
110 | | - * @param string $file |
111 | | - * @return bool |
| 140 | + * Check if the URL is a valid Google Fonts CSS API URL |
112 | 141 | */ |
113 | | - protected function isCached(string $file): bool |
| 142 | + protected function supports(string $url): bool |
| 143 | + { |
| 144 | + if (!$this->isValidHttpsUrl($url)) { |
| 145 | + return false; |
| 146 | + } |
| 147 | + |
| 148 | + return parse_url($url, PHP_URL_HOST) === self::GOOGLE_FONTS_HOST; |
| 149 | + } |
| 150 | + |
| 151 | + protected function isCached(string $url): bool |
114 | 152 | { |
115 | | - $cacheFile = $this->getCssFileCacheName($file); |
116 | | - $absoluteFile = GeneralUtility::getFileAbsFileName($cacheFile); |
| 153 | + $absoluteFile = $this->getAbsoluteCacheDirectory($url) . '/webfont.css'; |
117 | 154 |
|
118 | 155 | if (!file_exists($absoluteFile)) { |
119 | 156 | return false; |
120 | 157 | } |
121 | 158 |
|
122 | 159 | // Discard cache after 24 hours |
123 | 160 | if ((time() - (int) filemtime($absoluteFile)) > 86400) { |
124 | | - GeneralUtility::rmdir($this->getCacheDirectory($file)); |
| 161 | + GeneralUtility::rmdir($this->getAbsoluteCacheDirectory($url)); |
125 | 162 | return false; |
126 | 163 | } |
127 | 164 |
|
128 | 165 | return true; |
129 | 166 | } |
130 | 167 |
|
131 | 168 | /** |
132 | | - * @param string $file |
133 | | - * @return string |
| 169 | + * Returns the relative path to the cached CSS file (for use in frontend) |
134 | 170 | */ |
135 | | - protected function getCssFileCacheName(string $file): string |
| 171 | + protected function getCssFileCacheName(string $url): string |
136 | 172 | { |
137 | | - return $this->getCacheDirectory($file) . '/' . 'webfont.css'; |
| 173 | + return $this->getCacheDirectory($url) . '/webfont.css'; |
138 | 174 | } |
139 | 175 |
|
140 | 176 | /** |
141 | | - * @param string $file |
142 | | - * @return string |
| 177 | + * Returns the relative cache directory path (for use in frontend URLs) |
143 | 178 | */ |
144 | | - protected function getCacheDirectory(string $file): string |
| 179 | + protected function getCacheDirectory(string $url): string |
145 | 180 | { |
146 | | - return $this->tempDirectory . '/' . $this->getCacheIdentifier($file); |
| 181 | + return self::CACHE_DIRECTORY . '/' . $this->getCacheIdentifier($url); |
147 | 182 | } |
148 | 183 |
|
149 | 184 | /** |
150 | | - * @param string $file |
151 | | - * @return string |
| 185 | + * Returns the absolute cache directory path (for file system operations) |
152 | 186 | */ |
153 | | - protected function getCacheIdentifier(string $file): string |
| 187 | + protected function getAbsoluteCacheDirectory(string $url): string |
| 188 | + { |
| 189 | + return Environment::getPublicPath() . '/' . $this->getCacheDirectory($url); |
| 190 | + } |
| 191 | + |
| 192 | + protected function getCacheIdentifier(string $url): string |
154 | 193 | { |
155 | | - return hash('sha256', $file); |
| 194 | + return GeneralUtility::makeInstance(HashService::class)->hmac($url, 'GoogleFontService'); |
156 | 195 | } |
157 | 196 | } |
0 commit comments