TYPO3 durch Middleware effizient erweitern

|Daniel Goerz
Beitragsbild für Artikel Efficiently Extending TYPO3 with Middleware

PSR-15-Middlewares wurden mit TYPO3 v9 LTS eingeführt, um die Verarbeitung von HTTP-Anfragen und -Antworten zu verbessern und zu standardisieren. TYPO3 wird bereits mit einer Vielzahl an Middlewares ausgeliefert, da der TYPO3 Core das Verfahren sehr stark nutzt. Entwickler können darüber hinaus ihre eigenen Middlewares hinzufügen. Um einen Eindruck davon zu vermitteln, wie eine solche Middleware aussehen könnte, werden in diesem Artikel zwei reale Anwendungsfälle von Middleware-Implementierungen aus unseren Kundenprojekten vorgestellt.

Disclaimer: Dieser Artikel behandelt nicht die grundlegenden Konzepte von Middlewares oder die generelle Registrierung von eigenen Middlewares und geht auch nicht darauf ein wie und wo man seine eigenen Middlewares registriert. Wenn du dich über die Grundlagen informieren möchtest, schau dir bitte die offizielle Dokumentation sowie den Blog-Post „PSR-15 Middlewares in TYPO3“ an.

Lass dich von den beiden Beispielen inspirieren, passe die Konzepte für deine Anwendungsfälle an oder verwende unsere Extensions sofort. Aber meine Empfehlung vorab: Nutze so oder so die neuen Möglichkeiten, die TYPO3 mit sich bringt.

Schauen wir uns nun die beiden Beispiele an:

Ausgangspunkt der Anwendungsfälle

Beide Beispiele, stammen aus Kundenprojekten. Es mussten jeweils Zugriffsregeln für bestimmte Teile der Website angepasst bzw. implementiert werden. Im ersten Beispiel sollte der Zugriff auf statische HTML-Dateien nur für Website-Benutzer mit einer gültigen Login-Session möglich sein. Im zweiten Beispiel sollten Teile der Website, die normalerweise nicht öffentlich zugänglich sind, durch Verwendung einer speziellen URL einsehbar gemacht werden.

Die Beispiele sind aus dem "echten Leben" gegriffen. Das sei nochmal betont, um deutlich zu machen, dass es sich nicht nur um theoretische Entwürfe handelt, sondern um tatsächliche Lösungen, die bereits im Livebetrieb im Einsatz sind.

Middlewares sind in beiden Fällen die richtige Lösung, da sie minimalinvasive Anpassungen in der Request-Verarbeitung erlauben. Der Aufwand bei zukünftigen TYPO3 Updates wird dadurch gering gehalten. Durch das Platzieren der Middlewares an der richtigen Stelle im Middleware-Stack (d.h. so früh wie möglich) und das direkte Zurückgeben einer HTTP-Antwort sparen wir außerdem die Teile des TYPO3 Bootstrap- und Renderingprozesses ein, die nicht gebraucht werden. Das macht viele Aufrufe wesentlich performanter. Durch den Einsatz von Middlewares konnten wir also mit wenig Aufwand viel erreichen. Aber sehen wir uns die Beispiele einzeln an.

Anwendungsfall 1: Zugriff auf statische Dateien einschränken

Vielleicht kennst du das Szenario: Es gibt ein paar statische HTML-Dateien, die nicht aus dem TYPO3 kommen, aber ein wichtiger Teil der Website sind, wie z.B. ein HTML-basiertes Handbuch zu einem der Kundenprodukte. Die HTML-Dateien sind per Browser aufrufbar, sie sind miteinander verlinkt und für viele Benutzer wichtig.

Nun wollte der Kunde, dass wir den Zugriff auf diese HTML-Dateien auf bestimmte TYPO3-Frontend-Benutzer beschränken. Wenn Benutzer angemeldet sind, können sie wie gewohnt auf die HTML-Dateien zugreifen, und wenn sie nicht angemeldet sind, werden sie stattdessen auf die Anmeldeseite umgeleitet.
Eine Middleware eignet sich hervorragend um eine solche Anforderung zu erfüllen. Eine Implementierung könnte (etwas vereinfacht) so aussehen:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if (!$this->isMatchingRequest($request)) {
return $handler->handle($request);
}

$context = GeneralUtility::makeInstance(Context::class);

$userAspect = $context->getAspect('frontend.user');
if (!$userAspect->isLoggedIn()) {
$targetUrl = $request->getUri()
->withPath('/login/')
->withQuery('referer=' . rawurlencode((string)$request->getUri()));
return new RedirectResponse($targetUrl, 401);
}

return $this->generateResponse($request);
}

Wir wollen kurz zusammenfassen, was hier geschieht. Zunächst prüfen wir, ob die Anfrage von unserer Middleware bearbeitet werden kann. Die Methode prüft, ob es sich um eine Anfrage an eine der statischen HTML-Dateien handelt. Ist dies nicht der Fall, gehen wir zur nächsten Middleware in der Reihe über, da wir für die weitere Bearbeitung dieser Anfrage nicht verantwortlich sind.

Als nächstes fragen wir, ob ein Frontend-Benutzer authentifiziert ist, und aufgrund dieser Information leiten wir entweder auf die Anmeldeseite um (wobei wir die ursprünglich angeforderte URl als Referrer übergeben, so dass der Benutzer nach der Anmeldung wieder dorthin zurückgeleitet wird) oder wir erzeugen die Antwort und geben sie direkt zurück. Die generateResponse()-Methode sieht wie folgt aus:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
protected function generateResponse(ServerRequestInterface $request): ResponseInterface
{
$content = file_get_contents(Environment::getPublicPath() . $request->getUri()->getPath());
switch ($this->fileExtension) {
case 'html':
$contentType = 'text/html; charset=utf-8';
$content = mb_convert_encoding($content, 'HTML-ENTITIES', 'Windows-1252');
break;
case 'pdf':
$contentType = 'application/pdf';
break;
case 'jpg':
$contentType = 'image/jpeg';
break;
case 'png':
case 'gif':
case 'jpg':
$contentType = 'image/' . $this->fileExtension;
break;
default:
$contentType = 'text/plain';
}
$response = new Response();
$response->getBody()->write($content);
return $response->withHeader('Content-Type', $contentType);
}

Damit dieser Ansatz funktioniert, mussten wir sicherstellen, dass jede Anfrage an eine statische HTML-Datei über TYPO3 geleitet wird. Dies haben wir auf Webserver-Ebene - also z.B. mittels Apaches htaccess oder der entsprechenden nginx Konfiguration - erreicht, indem wir jede Anfrage an die Datei /index.php – den Einstiegspunkt von TYPO3 für Frontend-Anfragen - weitergeleitet haben.

Wie bei jedem Middleware-Ansatz ist es wichtig, die richtige Stelle im Stack der TYPO3-Core-Middleware zu finden, um unsere kundenspezifische Middleware einzureihen. Im aktuellen Beispiel muss die Frontend-Benutzerauthentifizierung bereits abgeschlossen (typo3/cms-frontend/authentication) und die passende Site aufgelöst (typo3/cms-frontend/site) sein, denn auf beides müssen wir zugreifen. Da die Auflösung der Site nach der Authentifizierung erfolgt, haben wir unsere Middleware hinter beiden registriert.

Das war’s auch schon. Mit einer eigenen Middleware integrieren wir jetzt also einen Teil einer Website, der nichts mit TYPO3 zu tun hatte in die Zugangskontrolle von TYPO3. Der Ansatz ist sauber und schnell, da er den Großteil der Bearbeitung von TYPO3-Anfragen vermeidet, indem wie oben beschrieben frühzeitig eine HTTP-Antwort generiert wird.

Kommen wir zum zweiten Beispiel.

Anwendungsfall 2: Backend-Benutzerzugriff simulieren

Bei einem anderen Projekt stellte sich die Situation wie folgt dar: Bei einer großen TYPO3-Installation mit mehreren Sprachen sollte eine weitere Übersetzung hinzugefügt werden (und es sollten noch viele weitere folgen). Die Inhalte waren in der neuen Sprache bereits bereitgestellt worden, wobei die Sprache selbst in der Site-Konfiguration deaktiviert war. Dies bedeutete, dass die Backend-Redakteure die neue Sprache der Website über die TYPO3-Vorschau-Funktionalität besuchen konnten.

Dann kam die Anforderung auf, dass die neue Sprachvariante der Website auch Personen ohne TYPO3-Backendzugang zum Korrekturlesen zur Verfügung gestellt werden sollte.

Wir entschieden uns, ein Backend-Modul zu bauen, in dem Vorschau-URLs generiert werden können. Diese URLs können dann an die Korrekturleser gesendet werden. Die URLs enthalten einen Hash-Parameter, der von der Middleware, die wir in diesem Beispiel vorstellen erkannt und validiert wird. Wenn der Hash gültig ist, simuliert die Middleware einen speziellen “read only” Backend-Benutzer, damit wir auf die Vorschaufunktionen von TYPO3 zurückgreifen können, und speichert den Hash in einem Cookie. Der Korrekturleser, der die angegebene URL besucht, kann nun die deaktivierte Sprache so aufrufen, als ob er eine gültige Backend-Sitzung hätte.

Die Middleware sieht wie folgt aus:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if ($this->context->getPropertyFromAspect('backend.user', 'isLoggedIn')) {
return $handler->handle($request);
}
$language = $request->getAttribute('language', null);
if (!$language instanceof SiteLanguage) {
return $handler->handle($request);
}
if ($language->isEnabled()) {
return $handler->handle($request);
}
$hash = $this->findHashInRequest($request);
if (empty($hash)) {
return $handler->handle($request);
}
if (!$this->verifyHash($hash, $language)) {
return $handler->handle($request);
}
// If the GET parameter PreviewUriBuilder::PARAMETER_NAME is set, then a cookie is set for the next request
if ($request->getQueryParams()[PreviewUriBuilder::PARAMETER_NAME] ?? false) {
$this->setCookie($hash, $request->getAttribute('normalizedParams'));
}
$this->initializePreviewUser();
return $handler->handle($request);
}

Zuerst prüfen wir, dass kein Backend-Benutzer angemeldet ist und dass die aktuelle Sprache auch tatsächlich deaktiviert ist. Danach suchen wir nach dem Hash-Parameter in der URL und in den mit der Anfrage gesendeten Cookies. Wenn ein Hash gefunden wird, validieren wir ihn, d.h. wir suchen den Hash in der Datenbank, prüfen, dass er noch nicht abgelaufen ist und dass er auch tatsächlich für die aktuell angeforderte Sprache erzeugt wurde.

All diese Prüfungen sind Teil der Beurteilung, ob die Middleware für die Anfragen zuständig ist. Die meisten Anfragen von Website-Besuchern werden einfach an die nächste Middleware in der Reihe delegiert. Wenn alle Bedingungen erfüllt sind, speichert die Middleware den Hash in einem Cookie und initialisiert einen Vorschaubenutzer.

Der Vorschau-Benutzer ist eine abgespeckte Version der BackendUserAuthentication,die Zugriff auf die aktuelle Sprache ermöglicht, auch wenn sie deaktiviert ist. Wir platzieren unsere Middleware vor der typo3/cms-frontend/page-resolver-Middleware, aber nach der typo3/cms-frontend/site-Middleware, da wir die bereits aufgelöste Site benötigen, um auf die aktuelle Sprache zugreifen zu können aber den Vorschau-Benutzer initialisieren wollen bevor TYPO3 entscheided of wir die aufgerufene Seite sehen können.

Diese Funktionalität wurde von b13 mit der TYPO3 Extension „authorized_preview“ veröffentlicht.

Zusammenfassung

Verwende Middlewares, wenn du dich in die Verarbeitung der Webserver-Anfrage einklinken musst, um die Funktionalitäten des TYPO3 Core zu erweitern, zu ändern oder sogar einige davon zu entfernen. Wenn deine Middleware nur in bestimmten Fällen etwas tun muss, dann stelle die performantesten Checks dazu ganz nach oben.

Wirf einen Blick auf die Middleware-Stacks für Backend und Frontend im Konfigurationsmodul des TYPO3-Backends (dazu muss die System-Extension typo3/cms-lowlevel installiert sein) um zu bestimmen, welche Middleware vor deiner eigenen Implementierung ausgeführt werden soll und registriere deine Middleware entsprechend.

Gerne kannst du unseren Code verwenden und anpassen und auch deine eigene Implementierung weitergeben. Schließlich können die Middlewares in jedes (TYPO3-) System integriert werden, das die PSR-15-Request/Response-Verarbeitung unterstützt. Lasst uns gemeinsam coole Sachen bauen.

Hilfreiche TYPO3-Extensions, von b13 für Dich!

Schau Dir unsere Extensions an

Hier findest Du die offiziellen PSR-15 Spezifikationen:

https://www.php-fig.org/psr/psr-15/