Skip to content
Open
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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/node_modules
.php_cs.cache
.php-cs-fixer.cache
.idea/
/node_modules
/composer.lock
/Resources/Private/JavaScript/Backend/TYPO3
30 changes: 30 additions & 0 deletions .htaccess.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Apache CORS configuration for z7_semantilizer
#
# This configuration is ONLY needed if you want to use the Semantilizer
# across different domains (e.g., editing content on domain-a.com while
# viewing from the TYPO3 backend on domain-b.com).
#
# For same-origin usage, this is NOT required.
#
# Copy the relevant parts to your document root's .htaccess if using Apache

<IfModule mod_headers.c>
<IfModule mod_rewrite.c>
RewriteEngine On

# Handle preflight OPTIONS requests for semantilizer
# This must be done at webserver level as OPTIONS requests
# never reach PHP middleware in most setups
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteCond %{HTTP:Access-Control-Request-Headers} (?:^|,\s*)x-semantilizer(?:,|$) [NC]
RewriteCond %{HTTP:Origin} ^https?://(.+)$ [NC]
RewriteRule .* - [R=204,L,E=CORS_ORIGIN:%1]

# Set CORS headers for preflight response
Header always set Access-Control-Allow-Origin "%{HTTP:Origin}e" env=CORS_ORIGIN
Header always set Access-Control-Allow-Credentials "true" env=CORS_ORIGIN
Header always set Access-Control-Allow-Methods "GET, OPTIONS" env=CORS_ORIGIN
Header always set Access-Control-Allow-Headers "X-Semantilizer" env=CORS_ORIGIN
Header always set Access-Control-Max-Age "86400" env=CORS_ORIGIN
</IfModule>
</IfModule>
80 changes: 80 additions & 0 deletions CORS-SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# CORS Setup for Multi-Site Usage

## When is this needed?

This configuration is **ONLY** required if you want to use the Semantilizer across different domains in a multi-site TYPO3 setup. For example:

- TYPO3 Backend running on `backend.example.com`
- Site A running on `site-a.example.com`
- Site B running on `site-b.example.com`
- You want to use Semantilizer in the backend to check content on Site A and Site B

**If you're using Semantilizer only on the same domain, you don't need this setup.**

## Why is webserver configuration needed?

When the browser detects a cross-origin request with custom headers (like `X-Semantilizer`), it automatically sends a **CORS preflight request** using the OPTIONS HTTP method.

This preflight request is handled by your webserver (nginx/Apache) **before** it reaches PHP or TYPO3. By default, most webservers block or reject OPTIONS requests, which causes the "CORS policy" error.

The PHP middleware in this extension handles the CORS response headers for the actual GET requests, but it **cannot** handle the OPTIONS preflight - that must be done at the webserver level.

## Setup Instructions

### For Apache

1. Ensure `mod_headers` and `mod_rewrite` are enabled
2. Add the configuration from `.htaccess.example` to your document root's `.htaccess` file
3. Restart Apache: `sudo service apache2 restart`

### For Nginx

1. Add the configuration from `nginx-cors.conf.example` to your server block
2. Test the configuration: `nginx -t`
3. Reload nginx: `sudo service nginx reload`

### For DDEV

**Option 1: nginx_full (recommended for development)**

1. Copy the nginx configuration to `.ddev/nginx_full/nginx-site.conf`
2. Remove the `#ddev-generated` line at the top
3. Integrate the CORS handling into the `location /` block
4. Run `ddev restart`

**Option 2: Custom nginx snippet**

1. Create `.ddev/nginx/semantilizer-cors.conf` with the CORS configuration
2. Run `ddev restart`

## Testing

After applying the configuration:

1. Clear browser cache and cookies
2. Open Firefox/Chrome DevTools → Network tab
3. Use Semantilizer on a cross-origin site
4. You should see:
- An OPTIONS request with status 204 (success)
- The GET request with status 200 and CORS headers

## Troubleshooting

**Still getting CORS errors?**

1. Verify the webserver configuration is active (check error logs)
2. Ensure `trustedHostsPattern` in TYPO3 includes your domains
3. Check that site configurations have proper `base` URLs defined
4. Clear browser cache completely (CORS responses are often cached)

**OPTIONS request returns 405?**

The webserver configuration is not active or not correctly placed. The OPTIONS request should never reach PHP - it should be answered by the webserver directly with 204.

## Security Considerations

The middleware checks allowed origins against:
1. Site configuration base URLs
2. TYPO3's `trustedHostsPattern` configuration

Make sure your `trustedHostsPattern` is properly configured and not too permissive (avoid wildcards like `.*` in production).
13 changes: 11 additions & 2 deletions Classes/Middleware/AbstractMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,21 @@
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Context\Exception\AspectNotFoundException;
use TYPO3\CMS\Core\Utility\GeneralUtility;

abstract class AbstractMiddleware implements MiddlewareInterface
{
protected function isSemantilizerRequest(ServerRequestInterface $request): bool
protected function isSemantilizerRequest(ServerRequestInterface $request, bool $requireLogin = null): bool
{
return !empty($request->getHeader('X-Semantilizer'));
$headerExists = !empty($request->getHeader('X-Semantilizer'));

try {
return $headerExists && (!$requireLogin || GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('backend.user', 'isLoggedIn', false));
} catch (AspectNotFoundException) {
return false;
}
}

abstract public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface;
Expand Down
24 changes: 24 additions & 0 deletions Classes/Middleware/CacheControl.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Zeroseven\Semantilizer\Middleware;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;

class CacheControl extends AbstractMiddleware
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
// Disable cache on some conditions
$this->isSemantilizerRequest($request, true)
&& $GLOBALS['TSFE'] instanceof TypoScriptFrontendController
&& $GLOBALS['TSFE']->set_no_cache(sprintf('Semantilizer frontend request (%s, line %d)', self::class, __LINE__));

// Go your way …
return $handler->handle($request);
}
}
83 changes: 83 additions & 0 deletions Classes/Middleware/CorsHeaders.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

declare(strict_types=1);

namespace Zeroseven\Semantilizer\Middleware;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use TYPO3\CMS\Core\Middleware\VerifyHostHeader;
use TYPO3\CMS\Core\Site\Entity\Site;
use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Core\Utility\GeneralUtility;

class CorsHeaders extends AbstractMiddleware
{
protected function urlToDomain(string $url): ?string
{
$parts = parse_url($url);
if (!$parts || !isset($parts['scheme'], $parts['host'])) {
return null;
}

$domain = $parts['scheme'] . '://' . $parts['host'];

// Include port if present and not default
if (isset($parts['port'])) {
$isDefaultPort = ($parts['scheme'] === 'https' && $parts['port'] === 443)
|| ($parts['scheme'] === 'http' && $parts['port'] === 80);
if (!$isDefaultPort) {
$domain .= ':' . $parts['port'];
}
}

return $domain;
}

private function isOriginAllowed(string $origin, array $serverParams): bool
{
// 1. Check trustedHostsPattern first (works without SiteFinder)
$trustedHostsPattern = $GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] ?? 'SERVER_NAME';
$verifier = new VerifyHostHeader($trustedHostsPattern);

if ($verifier->isAllowedHostHeaderValue(parse_url($origin, PHP_URL_HOST), $serverParams)) {
return true;
}

// 2. Check urls of site config if SiteFinder is available
try {
$siteUrls = array_filter(array_map(
fn (Site $site) => $this->urlToDomain((string)$site->getBase()),
GeneralUtility::makeInstance(SiteFinder::class)?->getAllSites() ?? []
));

if (in_array($origin, $siteUrls)) {
return true;
}
} catch (\Exception $e) {
// SiteFinder not ready yet (e.g., middleware runs very early)
// Already checked trustedHostsPattern above
}

return false;
}

public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$origin = $this->urlToDomain($request->getHeaderLine('Origin'));

// Only add CORS headers for actual semantilizer requests (not OPTIONS preflight)
// Note: OPTIONS preflight requests need to be handled by webserver configuration
// (nginx/Apache) as they never reach PHP middleware in most setups
if ($origin && $this->isSemantilizerRequest($request)) {
if ($this->isOriginAllowed($origin, $request->getServerParams())) {
return $handler->handle($request)
->withHeader('Access-Control-Allow-Origin', $origin)
->withHeader('Access-Control-Allow-Credentials', 'true');
}
}

return $handler->handle($request);
}
}
31 changes: 0 additions & 31 deletions Classes/Middleware/Request.php

This file was deleted.

2 changes: 1 addition & 1 deletion Classes/Middleware/UserTsConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class UserTsConfig extends AbstractMiddleware
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
// Disable the admin panel
$this->isSemantilizerRequest($request)
$this->isSemantilizerRequest($request, true)
&& ExtensionManagementUtility::isLoaded('adminpanel')
&& ExtensionManagementUtility::addUserTSConfig('admPanel.hide = 1');

Expand Down
27 changes: 27 additions & 0 deletions Configuration/ContentSecurityPolicies.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Mutation;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\UriValue;
use TYPO3\CMS\Core\Site\Entity\Site;
use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Core\Type\Map;
use TYPO3\CMS\Core\Utility\GeneralUtility;

return Map::fromEntries([
Scope::backend(),
new MutationCollection(
...array_map(function (Site $site) {
return new Mutation(
MutationMode::Extend,
Directive::ConnectSrc,
new UriValue(rtrim((string)$site->getBase(), '/')),
);
}, GeneralUtility::makeInstance(SiteFinder::class)?->getAllSites() ?? [])
)
]);
10 changes: 8 additions & 2 deletions Configuration/RequestMiddlewares.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@

return [
'frontend' => [
'zeroseven/z7_semantilizer/request' => [
'target' => \Zeroseven\Semantilizer\Middleware\Request::class,
'zeroseven/z7_semantilizer/cache-control' => [
'target' => \Zeroseven\Semantilizer\Middleware\CacheControl::class,
'after' => [
'typo3/cms-frontend/tsfe'
]
],
'zeroseven/z7_semantilizer/cors-headers' => [
'target' => \Zeroseven\Semantilizer\Middleware\CorsHeaders::class,
'after' => [
'typo3/cms-frontend/site'
]
],
'zeroseven/z7_semantilizer/user-ts-config' => [
'target' => \Zeroseven\Semantilizer\Middleware\UserTsConfig::class,
'before' => [
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ possibly be there.

## Setup

### Multi-Site / Cross-Origin Setup

For multi-site TYPO3 installations where the Semantilizer needs to work across different domains, additional webserver configuration may be required. See **[CORS-SETUP.md](CORS-SETUP.md)** for detailed instructions.

Note: This is only needed for cross-origin scenarios. Same-origin usage works out of the box.

### Render headlines

You’ll need to render your headlines via an extra ViewHelper in order to be able to edit or correct them automatically
Expand Down
1 change: 1 addition & 0 deletions Resources/Private/JavaScript/Backend/Semantilizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export class Semantilizer {
};

request.open('GET', (this.url.indexOf('#') < 0 ? this.url : this.url.substr(0, this.url.indexOf('#'))) + '#' + Math.random().toString(36).slice(2), true);
request.withCredentials = true;
request.setRequestHeader('X-Semantilizer', 'true');
request.send();
}
Expand Down
2 changes: 1 addition & 1 deletion Resources/Public/JavaScript/Backend/Semantilizer.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
"php-cs-fixer fix -v --dry-run --using-cache no --diff"
],
"test-php-cs-fixer-fix": [
"php-cs-fixer fix -v --using-cache false"
"php-cs-fixer fix"
],
"prepare-release": [
"rm .editorconfig",
Expand Down
Loading
Loading