Skip to content

Commit bc718cf

Browse files
committed
support for frontend user groups and cache tags
1 parent 626957b commit bc718cf

12 files changed

+753
-38
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ composer.lock
22
public/
33
vendor/
44
var/
5+
.phpunit.result.cache
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace AUS\SsiInclude\Cache\Backend;
6+
7+
use AUS\SsiInclude\Utility\FilenameUtility;
8+
use InvalidArgumentException;
9+
use TYPO3\CMS\Core\Cache\Backend\AbstractBackend;
10+
use TYPO3\CMS\Core\Cache\Backend\BackendInterface;
11+
use TYPO3\CMS\Core\Cache\Backend\TaggableBackendInterface;
12+
use TYPO3\CMS\Core\Cache\CacheManager;
13+
use TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException;
14+
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
15+
use TYPO3\CMS\Core\Core\Environment;
16+
use TYPO3\CMS\Core\Utility\GeneralUtility;
17+
use Webimpress\SafeWriter\Exception\ExceptionInterface;
18+
use Webimpress\SafeWriter\FileWriter;
19+
20+
class SsiIncludeCacheBackend extends AbstractBackend implements TaggableBackendInterface
21+
{
22+
private readonly TaggableBackendInterface $concrete;
23+
24+
private string $concreteCache = 'aus_ssi_include_concrete_cache';
25+
26+
private readonly FilenameUtility $filenameUtility;
27+
28+
/**
29+
* path where the include files are saved for use with the webserver
30+
*/
31+
private string $ssiIncludeDir = '/typo3temp/tx_ssiinclude/';
32+
33+
private bool $storeData = true;
34+
35+
/**
36+
* @inheritdoc
37+
* @param array<string, mixed> $options
38+
* @throws NoSuchCacheException
39+
*/
40+
public function __construct($context, array $options = [])
41+
{
42+
parent::__construct($context, $options);
43+
44+
$this->filenameUtility = GeneralUtility::makeInstance(FilenameUtility::class);
45+
46+
$cacheManager = GeneralUtility::makeInstance(CacheManager::class);
47+
assert($cacheManager instanceof CacheManager);
48+
$concrete = $cacheManager->getCache($this->concreteCache)->getBackend();
49+
assert($concrete instanceof BackendInterface);
50+
assert($concrete instanceof TaggableBackendInterface);
51+
$this->concrete = $concrete;
52+
}
53+
54+
/**
55+
* The cache identifier of the concrete cache which is used to save
56+
* the data. If storeData is false, it also creates an empty cache entry
57+
* to include with caching framework and have the identifier tied to the cache tags and lifetime
58+
*/
59+
public function setConcreteCache(string $concreteCache): void
60+
{
61+
$this->concreteCache = $concreteCache;
62+
}
63+
64+
/**
65+
* public accessible directory where the file is saved
66+
*/
67+
public function setSsiIncludeDir(string $ssiIncludeDir): void
68+
{
69+
$this->ssiIncludeDir = '/' . trim($ssiIncludeDir, '/') . '/';
70+
}
71+
72+
/**
73+
* if set, behaves like other cache backends, but if you do not need to receive the stored data
74+
* you can set this to false and save some space
75+
*/
76+
public function setStoreData(bool $storeData): void
77+
{
78+
$this->storeData = $storeData;
79+
}
80+
81+
public function getSsiIncludeDir(): string
82+
{
83+
return $this->ssiIncludeDir;
84+
}
85+
86+
/**
87+
* @inheritdoc
88+
*/
89+
public function setCache(FrontendInterface $cache): void
90+
{
91+
$this->cache = $cache;
92+
}
93+
94+
/**
95+
* @inheritdoc
96+
* @param array<string> $tags
97+
* @throws ExceptionInterface
98+
*/
99+
public function set($entryIdentifier, $data, array $tags = [], $lifetime = null): void
100+
{
101+
if (!is_string($data)) {
102+
throw new InvalidArgumentException('Data must be a string', 1616420133);
103+
}
104+
105+
$absolutePath = $this->filenameUtility->getAbsoluteFilename($entryIdentifier);
106+
107+
GeneralUtility::mkdir_deep(dirname($absolutePath));
108+
GeneralUtility::fixPermissions(dirname($absolutePath));
109+
110+
FileWriter::writeFile($absolutePath, $data);
111+
GeneralUtility::fixPermissions($absolutePath);
112+
113+
$this->concrete->set($entryIdentifier, $this->storeData ? $data : '', $tags, $lifetime);
114+
}
115+
116+
/**
117+
* @inheritdoc
118+
*/
119+
public function get($entryIdentifier): false|string
120+
{
121+
return $this->concrete->get($entryIdentifier);
122+
}
123+
124+
/**
125+
* @inheritdoc
126+
* @throws NoSuchCacheException
127+
*/
128+
public function has($entryIdentifier): bool
129+
{
130+
$data = $this->concrete->has($entryIdentifier);
131+
if (!$data) {
132+
return false;
133+
}
134+
135+
$absoluteFile = $this->filenameUtility->getAbsoluteFilename($entryIdentifier);
136+
return file_exists($absoluteFile);
137+
}
138+
139+
/**
140+
* @inheritdoc
141+
* @throws NoSuchCacheException
142+
*/
143+
public function remove($entryIdentifier): bool
144+
{
145+
$absoluteFile = $this->filenameUtility->getAbsoluteFilename($entryIdentifier);
146+
if (file_exists($absoluteFile)) {
147+
unlink($absoluteFile);
148+
}
149+
150+
return $this->concrete->remove($entryIdentifier);
151+
}
152+
153+
/**
154+
* @inheritdoc
155+
*/
156+
public function flush(): void
157+
{
158+
foreach ($this->getSsiIncludeDirFiles() as $file) {
159+
unlink($file);
160+
}
161+
162+
$this->concrete->flush();
163+
}
164+
165+
/**
166+
* @inheritdoc
167+
* @throws NoSuchCacheException
168+
*/
169+
public function collectGarbage(): void
170+
{
171+
// remove outdated things
172+
$this->concrete->collectGarbage();
173+
174+
// get all files, and the file that has no entry remove them
175+
$files = $this->getSsiIncludeDirFiles();
176+
foreach ($files as $absoluteFilename) {
177+
$file = basename($absoluteFilename);
178+
if (!$this->has($file)) {
179+
$absoluteFilename = $this->filenameUtility->getAbsoluteFilename($file);
180+
unlink($absoluteFilename);
181+
}
182+
}
183+
}
184+
185+
/**
186+
* @inheritdoc
187+
* @throws NoSuchCacheException
188+
*/
189+
public function flushByTag($tag): void
190+
{
191+
$entryIdentifiers = $this->findIdentifiersByTag($tag);
192+
foreach ($entryIdentifiers as $entryIdentifier) {
193+
$this->remove($entryIdentifier);
194+
}
195+
}
196+
197+
/**
198+
* @inheritdoc
199+
* @throws NoSuchCacheException
200+
*/
201+
public function flushByTags(array $tags): void
202+
{
203+
$this->concrete->flushByTags($tags);
204+
foreach ($tags as $tag) {
205+
$this->flushByTag($tag);
206+
}
207+
}
208+
209+
/**
210+
* @inheritdoc
211+
* @param string $tag
212+
* @return list<string>
213+
*/
214+
public function findIdentifiersByTag($tag): array
215+
{
216+
return $this->concrete->findIdentifiersByTag($tag);
217+
}
218+
219+
/**
220+
* @return list<string>
221+
*/
222+
private function getSsiIncludeDirFiles(): array
223+
{
224+
$publicIncludeDir = Environment::getPublicPath() . $this->ssiIncludeDir;
225+
return glob($publicIncludeDir . '*.html') ?: [];
226+
}
227+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace AUS\SsiInclude\Cache\Frontend;
6+
7+
use InvalidArgumentException;
8+
use TYPO3\CMS\Core\Cache\Exception;
9+
use TYPO3\CMS\Core\Cache\Exception\InvalidDataException;
10+
use TYPO3\CMS\Core\Cache\Frontend\AbstractFrontend;
11+
12+
/**
13+
* A cache frontend for SSI include cache entries.
14+
* If you use with another backend, make sure the backend supports cache entries with the pattern defined here
15+
*/
16+
class SsiIncludeCacheFrontend extends AbstractFrontend
17+
{
18+
public const PATTERN_ENTRYIDENTIFIER = '/^[a-zA-Z0-9_-]+\.html$/';
19+
20+
/**
21+
* @inheritdoc
22+
* @throws Exception
23+
* @throws InvalidDataException
24+
* @param list<string> $tags
25+
*/
26+
public function set($entryIdentifier, $data, array $tags = [], $lifetime = null): void
27+
{
28+
/** @noinspection DuplicatedCode */
29+
if (!$this->isValidEntryIdentifier($entryIdentifier)) {
30+
throw new InvalidArgumentException(
31+
'"' . $entryIdentifier . '" is not a valid cache entry identifier.',
32+
1233058264
33+
);
34+
}
35+
36+
foreach ($tags as $tag) {
37+
if (!$this->isValidTag($tag)) {
38+
throw new InvalidArgumentException('"' . $tag . '" is not a valid tag for a cache entry.', 1233058269);
39+
}
40+
}
41+
42+
$this->backend->set($entryIdentifier, $data, $tags, $lifetime);
43+
}
44+
45+
/**
46+
* @inheritdoc
47+
*/
48+
public function get($entryIdentifier)
49+
{
50+
if (!$this->isValidEntryIdentifier($entryIdentifier)) {
51+
throw new InvalidArgumentException(
52+
'"' . $entryIdentifier . '" is not a valid cache entry identifier.',
53+
1233058294
54+
);
55+
}
56+
57+
return $this->backend->get($entryIdentifier);
58+
}
59+
60+
/**
61+
* @inheritdoc
62+
*/
63+
public function isValidEntryIdentifier($identifier): bool
64+
{
65+
return preg_match(static::PATTERN_ENTRYIDENTIFIER, $identifier) === 1;
66+
}
67+
}

Classes/Middleware/InternalSsiRedirectMiddleware.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace AUS\SsiInclude\Middleware;
66

7+
use AUS\SsiInclude\Cache\Frontend\SsiIncludeCacheFrontend;
78
use AUS\SsiInclude\ViewHelpers\RenderIncludeViewHelper;
89
use Psr\Http\Message\ResponseInterface;
910
use Psr\Http\Message\ServerRequestInterface;
@@ -20,8 +21,8 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
2021
if (isset($request->getQueryParams()['ssi_include'])) {
2122
$originalRequestUri = new Uri($request->getQueryParams()['originalRequestUri'] ?? '');
2223
$ssiInclude = $request->getQueryParams()['ssi_include'];
23-
if (!preg_match('/^(\w+)$/', (string) $ssiInclude)) {
24-
return new HtmlResponse('ssi_include invalid', 400);
24+
if (!preg_match(SsiIncludeCacheFrontend::PATTERN_ENTRYIDENTIFIER, (string) $ssiInclude)) {
25+
return new HtmlResponse('ssi_include invalid ' . $ssiInclude, 400);
2526
}
2627

2728
$cacheFileName = RenderIncludeViewHelper::SSI_INCLUDE_DIR . $ssiInclude;

Classes/Utility/FilenameUtility.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace AUS\SsiInclude\Utility;
6+
7+
use AUS\SsiInclude\Cache\Backend\SsiIncludeCacheBackend;
8+
use TYPO3\CMS\Core\Cache\CacheManager;
9+
use TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException;
10+
use TYPO3\CMS\Core\Core\Environment;
11+
use TYPO3\CMS\Core\Utility\GeneralUtility;
12+
13+
class FilenameUtility
14+
{
15+
private string $ssiIncludeDir = '';
16+
17+
/**
18+
* @throws NoSuchCacheException
19+
*/
20+
private function getSsiIncludeDir(): string
21+
{
22+
if ($this->ssiIncludeDir !== '') {
23+
return $this->ssiIncludeDir;
24+
}
25+
26+
$cacheManager = GeneralUtility::makeInstance(CacheManager::class);
27+
assert($cacheManager instanceof CacheManager);
28+
$cache = $cacheManager->getCache('aus_ssi_include_cache');
29+
$cacheBackend = $cache->getBackend();
30+
assert($cacheBackend instanceof SsiIncludeCacheBackend);
31+
$this->ssiIncludeDir = $cacheBackend->getSsiIncludeDir();
32+
return $this->ssiIncludeDir;
33+
}
34+
35+
/**
36+
* @throws NoSuchCacheException
37+
*/
38+
public function getAbsoluteFilename(string $filename): string
39+
{
40+
$basePath = $this->getSsiIncludeDir() . $filename;
41+
return Environment::getPublicPath() . $basePath;
42+
}
43+
44+
/**
45+
* @throws NoSuchCacheException
46+
*/
47+
public function getReqUrl(string $filename): string
48+
{
49+
$reverseProxyPrefix = '/' . trim($GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefix'] ?? '', '/') . '/';
50+
$includePath = rtrim($reverseProxyPrefix, '/') . $this->getSsiIncludeDir();
51+
return $includePath . $filename . '?ssi_include=' . $filename . '&originalRequestUri=' . urlencode((string)GeneralUtility::getIndpEnv('REQUEST_URI'));
52+
}
53+
}

0 commit comments

Comments
 (0)