Skip to content

Commit b3be306

Browse files
committed
[TASK] Modernize GoogleFontService for TYPO3 v13/v14 compatibility
This change modernizes the GoogleFontService and improves its robustness. The URL validation has been improved to use proper URL parsing instead of strpos(). Downloaded font files are now validated against expected MIME types. The service uses Environment::getPublicPath() for absolute path resolution and HashService::hmac() for cache identifiers.
1 parent efae650 commit b3be306

File tree

2 files changed

+368
-57
lines changed

2 files changed

+368
-57
lines changed

Classes/Service/GoogleFontService.php

Lines changed: 96 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
namespace BK2K\BootstrapPackage\Service;
1212

1313
use Psr\Http\Message\ResponseInterface;
14+
use TYPO3\CMS\Core\Core\Environment;
15+
use TYPO3\CMS\Core\Crypto\HashService;
1416
use TYPO3\CMS\Core\Http\RequestFactory;
1517
use TYPO3\CMS\Core\Utility\GeneralUtility;
1618

@@ -20,138 +22,175 @@
2022
class GoogleFontService
2123
{
2224
/**
23-
* @var string
25+
* Relative path from public directory for cached font files (must be web-accessible)
2426
*/
25-
protected $tempDirectory = 'typo3temp/assets/bootstrappackage/fonts';
27+
protected const CACHE_DIRECTORY = 'typo3temp/assets/bootstrappackage/fonts';
2628

27-
/**
28-
* @var string
29-
*/
30-
protected $googleFontApiUrl = 'fonts.googleapis.com/css';
29+
protected const GOOGLE_FONTS_HOST = 'fonts.googleapis.com';
3130

3231
/**
33-
* @param string $file
34-
* @return string|null
35-
* @throws \Exception
32+
* Allowed MIME types for font files
33+
* @var list<string>
3634
*/
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
3850
{
39-
if (!$this->supports($file)) {
51+
if (!$this->supports($url)) {
4052
return null;
4153
}
42-
if ($this->isCached($file)) {
43-
return $this->getCssFileCacheName($file);
54+
if ($this->isCached($url)) {
55+
return $this->getCssFileCacheName($url);
4456
}
45-
return $this->cacheFile($file) ? $this->getCssFileCacheName($file) : null;
57+
return $this->cacheUrl($url) ? $this->getCssFileCacheName($url) : null;
4658
}
4759

48-
/**
49-
* @param string $file
50-
* @return bool
51-
*/
52-
protected function cacheFile(string $file): bool
60+
protected function cacheUrl(string $url): bool
5361
{
5462
/** @var RequestFactory */
5563
$requestFactory = GeneralUtility::makeInstance(RequestFactory::class);
5664

5765
/** @var ResponseInterface */
58-
$response = $requestFactory->request($file, 'GET', [
66+
$response = $requestFactory->request($url, 'GET', [
5967
'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'],
6068
]);
6169

6270
if ($response->getStatusCode() >= 300) {
6371
return false;
64-
} else {
65-
$content = $response->getBody()->getContents();
6672
}
6773

74+
$content = $response->getBody()->getContents();
75+
6876
// Ensure cache directory exists
69-
GeneralUtility::mkdir_deep($this->tempDirectory . '/' . $this->getCacheIdentifier($file));
77+
GeneralUtility::mkdir_deep($this->getAbsoluteCacheDirectory($url));
7078

71-
// Find and Download font files
79+
// Find and download font files
7280
$pattern = '%url\((.*?)\)%';
7381
preg_match_all($pattern, $content, $matches, PREG_SET_ORDER);
74-
$fontFiles = [];
82+
$fontUrls = [];
7583
foreach ($matches as $match) {
76-
$fontFiles[$match[1]] = $match[1];
84+
$fontUrls[$match[1]] = $match[1];
7785
}
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+
8091
/** @var ResponseInterface */
81-
$response = $requestFactory->request($fontFile);
92+
$response = $requestFactory->request($fontUrl);
8293
if ($response->getStatusCode() >= 300) {
8394
continue;
84-
} else {
85-
$fontFileContent = $response->getBody()->getContents();
8695
}
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);
87104
file_put_contents($localFontFile, $fontFileContent);
88105
GeneralUtility::fixPermissions($localFontFile);
89-
$content = str_replace($fontFile, basename($fontFile), $content);
106+
$content = str_replace($fontUrl, basename($fontUrl), $content);
90107
}
91108

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);
95112
GeneralUtility::fixPermissions($cacheFile);
96113

97114
return true;
98115
}
99116

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+
100123
/**
101-
* @param string $file
102-
* @return bool
124+
* Check if the response has an allowed font MIME type
103125
*/
104-
protected function supports(string $file): bool
126+
protected function isAllowedFontMimeType(ResponseInterface $response): bool
105127
{
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);
107137
}
108138

109139
/**
110-
* @param string $file
111-
* @return bool
140+
* Check if the URL is a valid Google Fonts CSS API URL
112141
*/
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
114152
{
115-
$cacheFile = $this->getCssFileCacheName($file);
116-
$absoluteFile = GeneralUtility::getFileAbsFileName($cacheFile);
153+
$absoluteFile = $this->getAbsoluteCacheDirectory($url) . '/webfont.css';
117154

118155
if (!file_exists($absoluteFile)) {
119156
return false;
120157
}
121158

122159
// Discard cache after 24 hours
123160
if ((time() - (int) filemtime($absoluteFile)) > 86400) {
124-
GeneralUtility::rmdir($this->getCacheDirectory($file));
161+
GeneralUtility::rmdir($this->getAbsoluteCacheDirectory($url));
125162
return false;
126163
}
127164

128165
return true;
129166
}
130167

131168
/**
132-
* @param string $file
133-
* @return string
169+
* Returns the relative path to the cached CSS file (for use in frontend)
134170
*/
135-
protected function getCssFileCacheName(string $file): string
171+
protected function getCssFileCacheName(string $url): string
136172
{
137-
return $this->getCacheDirectory($file) . '/' . 'webfont.css';
173+
return $this->getCacheDirectory($url) . '/webfont.css';
138174
}
139175

140176
/**
141-
* @param string $file
142-
* @return string
177+
* Returns the relative cache directory path (for use in frontend URLs)
143178
*/
144-
protected function getCacheDirectory(string $file): string
179+
protected function getCacheDirectory(string $url): string
145180
{
146-
return $this->tempDirectory . '/' . $this->getCacheIdentifier($file);
181+
return self::CACHE_DIRECTORY . '/' . $this->getCacheIdentifier($url);
147182
}
148183

149184
/**
150-
* @param string $file
151-
* @return string
185+
* Returns the absolute cache directory path (for file system operations)
152186
*/
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
154193
{
155-
return hash('sha256', $file);
194+
return GeneralUtility::makeInstance(HashService::class)->hmac($url, 'GoogleFontService');
156195
}
157196
}

0 commit comments

Comments
 (0)