Efficiently Extending TYPO3 with Middleware

Daniel Goerz

At b13, we get excited when clients come to us with interesting challenges. Have you ever been asked to add functionality to your website that is not currently available in your CMS? Building middleware can be a useful approach in these cases. To give you an idea of what can be accomplished with a middleware, I will share two use cases from customer projects at b13. I hope these two examples provide you some inspiration and help you embrace all the new tools and possibilities that TYPO3 CMS brings to the table.

The PHP Standard Recommendation 15 (PSR-15), which improves and standardizes request and response processing by defining request handlers and middleware, was initially introduced to TYPO3 with version 9 LTS. Since then, TYPO3 ships with many types of middleware that are heavily used by the core, like the middleware “NormalizedParamsAttribute” that enriches the Request object or the “RedirectHandler” middleware that resolves and performs redirects. In addition, developers can create their own middleware and extend the core. 

Middleware Examples: b13 Client Use Cases

The use cases that follow originated from client projects. I’d like to emphasize that these are real world requirements that we had to solve. They are not theoretical proofs of concept, but actual solutions now running in production. Both involve implementing custom access rules for specific sections of client websites. In the first example, access to static HTML files had to be restricted to only those website users with a valid login session. In the second, some sections of the client website not usually accessible to site visitors needed to be temporarily accessible through a publicly shareable URL.

We chose middleware-based resolutions to these challenges because we wanted to be as technically unobtrusive as possible and not disrupt other functionality within the CMS. Intercepting the middleware stack at the right point also allowed us to achieve the required results with minimal effort. 

Use Case 1: Restrict Access to Static Files

You might be familiar with this scenario: Some parts of a TYPO3 website consist only of mere static HTML files that are not generated by TYPO3 but uploaded and updated independently. Those files are accessible for everyone, they are linked to each other and quite important for some users.

In this case, the customer needed to restrict the access to those HTML files for some TYPO3 frontend users. Users who are logged in are allowed to access the HTML files in question. Site visitors who are not logged in, however, should be redirected to the login page.
A middleware solution would be an appropriate approach for such a requirement. A simplified implementation might look like this:

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);
}

First, we check to see if the request is eligible to be handled by our middleware. Then, we check to see if the request is to one of the restricted, static HTML files. If this is not the case, we pass the request on to the regular CMS processes.

Next, we simply ask if the frontend user is authenticated and based on that information, we either redirect the not-yet-authenticated user to the login page (passing the originally requested URl as referrer so the user is redirected back there after login) or we generate the response and return it directly for the user to consume. The generateResponse() method looks like this:

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);
}

To make this approach work, we had to make sure that every request to a static HTML file was routed through TYPO3. We achieved this on the web server level by redirecting every request to TYPO3’s index.php file.

After this, it is important to find the right place in TYPO3s core middleware stack to insert our custom middleware. We needed the front-end user authentication processed (typo3/cms-frontend/authentication) and the matching site resolved (typo3/cms-frontend/site). As the site resolving happens after the authentication, we registered our middleware after site resolution.

That is pretty much it! With a custom middleware we embedded files that lived outside of TYPO3 into TYPO3s access control mechanisms. The approach is clean and fast since it creates the response itself very early in the middleware stack. This avoids the execution of most of TYPO3’s regular request processing by and leads to a very quick response time.

Use Case 2: Simulate Backend User Access

In this project, the situation was as follows: A reasonably large TYPO3 installation with multiple languages was going to add another language—and the client had plans to add many more. Content in the new language was already in place, while the language itself was still disabled in the site configuration (preventing it from being displayed in the frontend) so that
backend editors could visit the website in the newly added language through TYPO3’s standard preview functionality. However, an additional requirement then came up. The new language variant needed to be proofread, but by proofreaders who did not (and should not) have backend accounts. 

We needed to generate temporary access to the new language of the site for selected visitors. For that purpose we created a backend module that generates preview URLs that can be sent out to the proofreaders. The URLs contained a hash parameter recognized and validated by the middleware. Given a valid hash, the middleware stores the hash in a cookie, and gives the user preview access to the relevant content in the new, yet-to-be activated language. Proofreaders visiting the site via the specially generated URLs can browse the site in the disabled language as if they had an authenticated backend session.

The middleware looks like this:

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);
}

First, we check that no backend user is currently logged in and that the current language is set and disabled. Next, we look for the hash parameter in the URL and in the cookies. If a hash is found we validate it in the database to make sure it hasn’t expired, and that the hash was generated for the currently requested language.

All these checks determine whether the current request is eligible for the middleware to do it’s thing. Requests not meeting all the specified conditions will simply be passed along to the next middleware in line. The few that do fulfil its requirements will be picked up by our middleware, which stores the validated hash in a cookie and initializes a preview user.

The preview user is initialized by a stripped-down version of the BackendUserAuthentication, with access only to the specified language, so that TYPO3 will render the content despite the language in questions being disabled for all other users. Therefore, we place our middleware in line before the typo3/cms-frontend/page-resolver middleware and after the typo3/cms-frontend/site middleware because we need the Site being already resolved to access the current language.

If you need to do something similar, we open sourced this functionality through b13’s GitHub account as the TYPO3 “authorized_preview” extension.

Conclusion

If you need to intercept and modify request processing to add to or modify core functionality, middleware may be the way to go.

Final tips:

  • If your middleware is only responsible in some cases, put the least expensive checks (in terms of computing time) first to decide whether the current request should be modified or passed on to the rest of the middleware stack.
  • Install the sysext typo3/cms-lowlevel system extension to give yourself access to the overview of TYPO3’s middleware stacks in the configuration module.
  • Determine where your middleware needs to be located in the backend or frontend middleware stacks. What needs to be executed before your custom implementation kicks in?
  • Register your new middleware to fire off accordingly. 

Standardized middleware can be plugged into every system that supports PSR-15 request and response processing—like TYPO3. Feel free to use and adapt our implementation and also share your own. We’d love to hear from you about how you adapted our solutions or what you did to come up with your own! Let’s build cool stuff - together!

If you want to learn about the basics of middleware, please refer to the official TYPO3 documentation as well as to my blog post PSR-15 middleware in TYPO3.

Useful TYPO3 Extensions, from b13 to You!

Take a look

Find the official PSR-15 specifications here:

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