Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support for frontend user groups and cache tags #12

Merged
merged 3 commits into from
Mar 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tasks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
with:
php-version: ${{ matrix.php }}
- uses: actions/checkout@v2
- uses: actions/cache@v2
- uses: actions/cache@v3
with:
path: ~/.composer/cache/files
key: ${{ runner.os }}-${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }}
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ composer.lock
public/
vendor/
var/
.phpunit.result.cache
185 changes: 185 additions & 0 deletions Classes/Cache/Backend/SsiIncludeCacheBackend.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
<?php

declare(strict_types=1);

namespace AUS\SsiInclude\Cache\Backend;

use AUS\SsiInclude\Utility\FilenameUtility;
use InvalidArgumentException;
use TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend;
use TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use Webimpress\SafeWriter\Exception\ExceptionInterface;
use Webimpress\SafeWriter\FileWriter;

class SsiIncludeCacheBackend extends Typo3DatabaseBackend
{
private readonly FilenameUtility $filenameUtility;

/**
* path where the include files are saved for use with the webserver
*/
private string $ssiIncludeDir = '/typo3temp/tx_ssiinclude/';

private bool $storeData = true;

/**
* @inheritdoc
* @param array<string, mixed> $options
*/
public function __construct($context, array $options = [])
{
parent::__construct($context, $options);

$this->filenameUtility = GeneralUtility::makeInstance(FilenameUtility::class);
}

public function getSsiIncludeDir(): string
{
return $this->ssiIncludeDir;
}

/**
* @return list<string>
*/
private function getSsiIncludeDirFiles(): array
{
$publicIncludeDir = Environment::getPublicPath() . $this->ssiIncludeDir;
return glob($publicIncludeDir . '*.html') ?: [];
}

/**
* public accessible directory where the file is saved
*/
public function setSsiIncludeDir(string $ssiIncludeDir): void
{
$this->ssiIncludeDir = '/' . trim($ssiIncludeDir, '/') . '/';
}

/**
* if set, behaves like other cache backends, but if you do not need to receive the stored data
* you can set this to false and save some space
*/
public function setStoreData(bool $storeData): void
{
$this->storeData = $storeData;
}

/**
* @inheritdoc
* @param array<string> $tags
* @throws ExceptionInterface
*/
public function set($entryIdentifier, $data, array $tags = [], $lifetime = null): void
{
if (!is_string($data)) {
throw new InvalidArgumentException('Data must be a string', 1616420133);
}

parent::set($entryIdentifier, $this->storeData ? $data : '', $tags, $lifetime);

$absolutePath = $this->filenameUtility->getAbsoluteFilename($entryIdentifier);

GeneralUtility::mkdir_deep(dirname($absolutePath));
GeneralUtility::fixPermissions(dirname($absolutePath));

FileWriter::writeFile($absolutePath, $data);
GeneralUtility::fixPermissions($absolutePath);
}

/**
* @inheritdoc
* @throws NoSuchCacheException
*/
public function has($entryIdentifier): bool
{
$data = parent::has($entryIdentifier);
if (!$data) {
return false;
}

$absoluteFile = $this->filenameUtility->getAbsoluteFilename($entryIdentifier);
return file_exists($absoluteFile);
}

/**
* @inheritdoc
* @throws NoSuchCacheException
*/
public function remove($entryIdentifier): bool
{
$absoluteFile = $this->filenameUtility->getAbsoluteFilename($entryIdentifier);
if (file_exists($absoluteFile)) {
unlink($absoluteFile);
}

return parent::remove($entryIdentifier);
}

/**
* @inheritdoc
*/
public function flush(): void
{
foreach ($this->getSsiIncludeDirFiles() as $file) {
unlink($file);
}

parent::flush();
}

/**
* @inheritdoc
* @throws NoSuchCacheException
*/
public function collectGarbage(): void
{
// remove outdated things
parent::collectGarbage();

// get all files, and the file that has no entry remove them
$files = $this->getSsiIncludeDirFiles();
foreach ($files as $absoluteFilename) {
$file = basename($absoluteFilename);
if (!$this->has($file)) {
$absoluteFilename = $this->filenameUtility->getAbsoluteFilename($file);
unlink($absoluteFilename);
}
}
}

/**
* @inheritdoc
* @throws NoSuchCacheException
*/
public function flushByTag($tag): void
{
$entryIdentifiers = $this->findIdentifiersByTag($tag);
foreach ($entryIdentifiers as $entryIdentifier) {
$this->remove($entryIdentifier);
}
}

/**
* @inheritdoc
* @throws NoSuchCacheException
*/
public function flushByTags(array $tags): void
{
parent::flushByTags($tags);
foreach ($tags as $tag) {
$this->flushByTag($tag);
}
}

/**
* @inheritdoc
* @param string $tag
* @return list<string>
*/
public function findIdentifiersByTag($tag): array
{
return parent::findIdentifiersByTag($tag);
}
}
67 changes: 67 additions & 0 deletions Classes/Cache/Frontend/SsiIncludeCacheFrontend.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

declare(strict_types=1);

namespace AUS\SsiInclude\Cache\Frontend;

use InvalidArgumentException;
use TYPO3\CMS\Core\Cache\Exception;
use TYPO3\CMS\Core\Cache\Exception\InvalidDataException;
use TYPO3\CMS\Core\Cache\Frontend\AbstractFrontend;

/**
* A cache frontend for SSI include cache entries.
* If you use with another backend, make sure the backend supports cache entries with the pattern defined here
*/
class SsiIncludeCacheFrontend extends AbstractFrontend
{
public const PATTERN_ENTRYIDENTIFIER = '/^[a-zA-Z0-9_-]+\.html$/';

/**
* @inheritdoc
* @throws Exception
* @throws InvalidDataException
* @param list<string> $tags
*/
public function set($entryIdentifier, $data, array $tags = [], $lifetime = null): void
{
/** @noinspection DuplicatedCode */
if (!$this->isValidEntryIdentifier($entryIdentifier)) {
throw new InvalidArgumentException(
'"' . $entryIdentifier . '" is not a valid cache entry identifier.',
1233058264
);
}

foreach ($tags as $tag) {
if (!$this->isValidTag($tag)) {
throw new InvalidArgumentException('"' . $tag . '" is not a valid tag for a cache entry.', 1233058269);
}
}

$this->backend->set($entryIdentifier, $data, $tags, $lifetime);
}

/**
* @inheritdoc
*/
public function get($entryIdentifier)
{
if (!$this->isValidEntryIdentifier($entryIdentifier)) {
throw new InvalidArgumentException(
'"' . $entryIdentifier . '" is not a valid cache entry identifier.',
1233058294
);
}

return $this->backend->get($entryIdentifier);
}

/**
* @inheritdoc
*/
public function isValidEntryIdentifier($identifier): bool
{
return preg_match(static::PATTERN_ENTRYIDENTIFIER, $identifier) === 1;
}
}
5 changes: 3 additions & 2 deletions Classes/Middleware/InternalSsiRedirectMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace AUS\SsiInclude\Middleware;

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

$cacheFileName = RenderIncludeViewHelper::SSI_INCLUDE_DIR . $ssiInclude;
Expand Down
53 changes: 53 additions & 0 deletions Classes/Utility/FilenameUtility.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

namespace AUS\SsiInclude\Utility;

use AUS\SsiInclude\Cache\Backend\SsiIncludeCacheBackend;
use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Utility\GeneralUtility;

class FilenameUtility
{
private string $ssiIncludeDir = '';

/**
* @throws NoSuchCacheException
*/
private function getSsiIncludeDir(): string
{
if ($this->ssiIncludeDir !== '') {
return $this->ssiIncludeDir;
}

$cacheManager = GeneralUtility::makeInstance(CacheManager::class);
assert($cacheManager instanceof CacheManager);
$cache = $cacheManager->getCache('aus_ssi_include_cache');
$cacheBackend = $cache->getBackend();
assert($cacheBackend instanceof SsiIncludeCacheBackend);
$this->ssiIncludeDir = $cacheBackend->getSsiIncludeDir();
return $this->ssiIncludeDir;
}

/**
* @throws NoSuchCacheException
*/
public function getAbsoluteFilename(string $filename): string
{
$basePath = $this->getSsiIncludeDir() . $filename;
return Environment::getPublicPath() . $basePath;
}

/**
* @throws NoSuchCacheException
*/
public function getReqUrl(string $filename): string
{
$reverseProxyPrefix = '/' . trim($GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefix'] ?? '', '/') . '/';
$includePath = rtrim($reverseProxyPrefix, '/') . $this->getSsiIncludeDir();
return $includePath . $filename . '?ssi_include=' . $filename . '&originalRequestUri=' . urlencode((string)GeneralUtility::getIndpEnv('REQUEST_URI'));
}
}
Loading