Skip to content

Commit

Permalink
[PoC][FEATURE] Introduce mixed mode for headless (#578)
Browse files Browse the repository at this point in the history
* [FEATURE] Introduce mixed mode for headless

Patch introduces new way for checking if we are in headless mode, you can set not enabled, mixed mode (fluid & headless at the same time), or full headless mode.

To configure headless mode, please use site configuration flag, for example:
`
   'headless': 0|1|2
`
Legacy flag (true|false) is respected, but please migrate to integer notation

Possible options:
 0 (old: false) = headless disabled for site in TYPO3 instance
 1 (old: true) = headless fully enabled site in TYPO3 instance
 2 = headless in mixed mode (fluid & json API in one site in TYPO3 instance)

Values 0 (old: false) or 1 (old: true) inform extension to fully disable/enable headless for particular site.

To enable mixed mode for chosen site in TYPO3 you need to:
- In typoscript template for site, you have to load "Headless - Mixed mode JSON response" setup file instead of default headless one.
- set `headless` flag to value of `2` site configuration's file or configure flag via editor in Site's management backend

Mixed mode flag (value of `2`) inform EXT:headless extension to additionally check for `Accept` header with value `application/json` when processing request to the particular site in TYPO3 instance.

In case of request without `Accept` header or `Accept` with different value than `application/json` TYPO3 will respond with HTML content (standard TYPO3's response)
In case of request with header `Accept` match value of `application/json`, TYPO3 will respond with JSON response.

resolves #94 #578 #348
  • Loading branch information
twoldanski authored Oct 27, 2023
1 parent 15e981e commit b6a1620
Show file tree
Hide file tree
Showing 35 changed files with 584 additions and 226 deletions.
12 changes: 4 additions & 8 deletions Classes/Hooks/FileOrFolderLinkBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@

namespace FriendsOfTYPO3\Headless\Hooks;

use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
use FriendsOfTYPO3\Headless\Utility\HeadlessMode;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\Typolink\LinkResultInterface;
use TYPO3\CMS\Frontend\Typolink\UnableToLinkException;

Expand All @@ -26,14 +27,9 @@ class FileOrFolderLinkBuilder extends \TYPO3\CMS\Frontend\Typolink\FileOrFolderL
*/
public function build(array &$linkDetails, string $linkText, string $target, array $conf): LinkResultInterface
{
$setup = ($GLOBALS['TSFE'] ?? null) instanceof TypoScriptFrontendController ? $GLOBALS['TSFE']->tmpl->setup : null;
$headlessMode = GeneralUtility::makeInstance(HeadlessMode::class)->withRequest($GLOBALS['TYPO3_REQUEST']);

if (
array_key_exists('type', $linkDetails)
&& $linkDetails['type'] === 'file'
&& isset($setup['plugin.']['tx_headless.']['staticTemplate'])
&& (bool)$setup['plugin.']['tx_headless.']['staticTemplate'] === true
) {
if ($headlessMode->isEnabled()) {
$conf['forceAbsoluteUrl'] = 1;
}

Expand Down
9 changes: 5 additions & 4 deletions Classes/Middleware/ElementBodyResponseMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace FriendsOfTYPO3\Headless\Middleware;

use FriendsOfTYPO3\Headless\Json\JsonEncoder;
use FriendsOfTYPO3\Headless\Utility\HeadlessMode;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
Expand All @@ -26,10 +27,12 @@
class ElementBodyResponseMiddleware implements MiddlewareInterface
{
private JsonEncoder $jsonEncoder;
private HeadlessMode $headlessMode;

public function __construct(JsonEncoder $jsonEncoder = null)
public function __construct(JsonEncoder $jsonEncoder = null, HeadlessMode $headlessMode)
{
$this->jsonEncoder = $jsonEncoder;
$this->headlessMode = $headlessMode;
}

public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
Expand All @@ -45,9 +48,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
return $response;
}

$siteConf = $request->getAttribute('site')->getConfiguration();

if (!($siteConf['headless'] ?? false)) {
if (!$this->headlessMode->withRequest($request)->isEnabled()) {
return $response;
}

Expand Down
43 changes: 43 additions & 0 deletions Classes/Middleware/HeadlessModeSetter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

/*
* This file is part of the "headless" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.md file that was distributed with this source code.
*/

declare(strict_types=1);

namespace FriendsOfTYPO3\Headless\Middleware;

use FriendsOfTYPO3\Headless\Utility\Headless;
use FriendsOfTYPO3\Headless\Utility\HeadlessMode;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use TYPO3\CMS\Core\Site\Entity\Site;

class HeadlessModeSetter implements MiddlewareInterface
{
public function __construct() {}

public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$mode = HeadlessMode::NONE;

/**
* @var Site $site
*/
$site = $request->getAttribute('site');
if ($site) {
$mode = (int)($site->getConfiguration()['headless'] ?? HeadlessMode::NONE);
}

$request = $request->withAttribute('headless', new Headless($mode));

$GLOBALS['TYPO3_REQUEST'] = $request;
return $handler->handle($request);
}
}
6 changes: 3 additions & 3 deletions Classes/Middleware/RedirectHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use FriendsOfTYPO3\Headless\Event\RedirectUrlEvent;
use FriendsOfTYPO3\Headless\Utility\HeadlessFrontendUrlInterface;
use FriendsOfTYPO3\Headless\Utility\HeadlessMode;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
Expand All @@ -31,6 +32,7 @@ final class RedirectHandler extends \TYPO3\CMS\Redirects\Http\Middleware\Redirec
{
private ServerRequestInterface $request;
private HeadlessFrontendUrlInterface $urlUtility;
private HeadlessMode $headlessMode;

public function __construct(
RedirectService $redirectService,
Expand Down Expand Up @@ -66,9 +68,7 @@ protected function buildRedirectResponse(UriInterface $uri, array $redirectRecor
return parent::buildRedirectResponse($uri, $redirectRecord);
}

$siteConf = $this->request->getAttribute('site')->getConfiguration();

if (!($siteConf['headless'] ?? false)) {
if (!$this->headlessMode->withRequest($this->request)->isEnabled()) {
return parent::buildRedirectResponse($uri, $redirectRecord);
}

Expand Down
18 changes: 5 additions & 13 deletions Classes/Middleware/ShortcutAndMountPointRedirect.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace FriendsOfTYPO3\Headless\Middleware;

use FriendsOfTYPO3\Headless\Utility\HeadlessMode;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
Expand All @@ -22,7 +23,6 @@
use TYPO3\CMS\Core\Http\JsonResponse;
use TYPO3\CMS\Core\Http\RedirectResponse;
use TYPO3\CMS\Core\Routing\PageArguments;
use TYPO3\CMS\Core\Site\Entity\Site;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\Controller\ErrorController;

Expand All @@ -34,6 +34,9 @@
class ShortcutAndMountPointRedirect implements MiddlewareInterface, LoggerAwareInterface
{
use LoggerAwareTrait;

public function __construct(private readonly HeadlessMode $headlessMode) {}

public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$queryParams = $request->getQueryParams();
Expand Down Expand Up @@ -137,17 +140,6 @@ protected function prefixExternalPageUrl(string $redirectTo, string $sitePrefix)

private function isHeadlessEnabled(ServerRequestInterface $request): bool
{
/**
* @var Site
*/
$site = $request->getAttribute('site');

if (!($site instanceof Site)) {
return false;
}

$siteConf = $request->getAttribute('site')->getConfiguration();

return $siteConf['headless'] ?? false;
return $this->headlessMode->withRequest($request)->isEnabled();
}
}
25 changes: 7 additions & 18 deletions Classes/Middleware/UserIntMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,44 +11,33 @@

namespace FriendsOfTYPO3\Headless\Middleware;

use FriendsOfTYPO3\Headless\Utility\HeadlessMode;
use FriendsOfTYPO3\Headless\Utility\HeadlessUserInt;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use TYPO3\CMS\Core\Http\Stream;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;

class UserIntMiddleware implements MiddlewareInterface
{
/**
* @var TypoScriptFrontendController
*/
private $tsfe;
/**
* @var HeadlessUserInt
*/
private $headlessUserInt;
private HeadlessUserInt $headlessUserInt;
private HeadlessMode $headlessMode;

public function __construct(
HeadlessUserInt $headlessUserInt = null
HeadlessUserInt $headlessUserInt = null,
HeadlessMode $headlessMode
) {
$this->headlessUserInt = $headlessUserInt ?? GeneralUtility::makeInstance(HeadlessUserInt::class);
$this->headlessMode = $headlessMode;
}

public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$response = $handler->handle($request);

$this->tsfe = $request->getAttribute('frontend.controller');
if ($this->tsfe === null && isset($GLOBALS['TSFE'])) {
$this->tsfe = $GLOBALS['TSFE'];
}

if (!isset($this->tsfe->tmpl->setup['plugin.']['tx_headless.']['staticTemplate'])
|| (bool)$this->tsfe->tmpl->setup['plugin.']['tx_headless.']['staticTemplate'] === false
) {
if (!$this->headlessMode->withRequest($request)->isEnabled()) {
return $response;
}

Expand Down
30 changes: 30 additions & 0 deletions Classes/Utility/Headless.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

/*
* This file is part of the "headless" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.md file that was distributed with this source code.
*/

declare(strict_types=1);

namespace FriendsOfTYPO3\Headless\Utility;

/**
* @codeCoverageIgnore
*/
final class Headless
{
private int $mode;

public function __construct(int $mode = HeadlessMode::NONE)
{
$this->mode = $mode;
}

public function getMode(): int
{
return $this->mode;
}
}
44 changes: 44 additions & 0 deletions Classes/Utility/HeadlessMode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

/*
* This file is part of the "headless" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.md file that was distributed with this source code.
*/

declare(strict_types=1);

namespace FriendsOfTYPO3\Headless\Utility;

use Psr\Http\Message\ServerRequestInterface;

final class HeadlessMode
{
public const NONE = 0;
public const FULL = 1;
public const MIXED = 2;

private ?ServerRequestInterface $request = null;

public function withRequest(ServerRequestInterface $request): self
{
$this->request = $request;
return $this;
}
public function isEnabled(): bool
{
if ($this->request === null) {
return false;
}

$headless = $this->request->getAttribute('headless') ?? new Headless();

if ($headless->getMode() === self::NONE) {
return false;
}

return $headless->getMode() === self::FULL ||
($headless->getMode() === self::MIXED && ($this->request->getHeader('Accept')[0] ?? '') === 'application/json');
}
}
14 changes: 9 additions & 5 deletions Classes/Utility/UrlUtility.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,19 @@ class UrlUtility implements LoggerAwareInterface, HeadlessFrontendUrlInterface
private SiteFinder $siteFinder;
private array $conf = [];
private array $variants = [];
private HeadlessMode $headlessMode;

public function __construct(
?Features $features = null,
?Resolver $resolver = null,
?SiteFinder $siteFinder = null,
?ServerRequestInterface $serverRequest = null
?ServerRequestInterface $serverRequest = null,
?HeadlessMode $headlessMode = null
) {
$this->features = $features ?? GeneralUtility::makeInstance(Features::class);
$this->resolver = $resolver ?? GeneralUtility::makeInstance(Resolver::class, 'site', []);
$this->siteFinder = $siteFinder ?? GeneralUtility::makeInstance(SiteFinder::class);
$this->headlessMode = $headlessMode ?? GeneralUtility::makeInstance(HeadlessMode::class);
$request = $serverRequest ?? ($GLOBALS['TYPO3_REQUEST'] ?? null);

if ($request instanceof ServerRequestInterface) {
Expand All @@ -72,18 +75,17 @@ public function withLanguage(SiteLanguage $language): HeadlessFrontendUrlInterfa

public function getFrontendUrlWithSite($url, SiteInterface $site, string $returnField = 'frontendBase'): string
{
$configuration = $site->getConfiguration();

if (!($configuration['headless'] ?? false)) {
if (!$this->headlessMode->isEnabled()) {
return $url;
}

try {
$base = $site->getBase()->getHost();
$port = $site->getBase()->getPort();
$configuration = $site->getConfiguration();
$frontendBaseUrl = $this->resolveWithVariants(
$configuration[$returnField] ?? '',
$configuration['baseVariants'] ?? [],
$this->variants,
$returnField
);

Expand Down Expand Up @@ -269,6 +271,8 @@ private function extractConfigurationFromRequest(ServerRequestInterface $request
$object->handleLanguageConfiguration($language, $object);
}

$object->headlessMode = $object->headlessMode->withRequest($request);

return $object;
}
}
10 changes: 4 additions & 6 deletions Classes/XClass/Controller/FormFrontendController.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use FriendsOfTYPO3\Headless\Form\Decorator\DefinitionDecoratorInterface;
use FriendsOfTYPO3\Headless\Form\Decorator\FormDefinitionDecorator;
use FriendsOfTYPO3\Headless\Form\Translator;
use FriendsOfTYPO3\Headless\Utility\HeadlessMode;
use FriendsOfTYPO3\Headless\XClass\FormRuntime;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Core\Utility\ArrayUtility;
Expand All @@ -23,7 +24,6 @@
use TYPO3\CMS\Extbase\Security\Cryptography\HashService;
use TYPO3\CMS\Form\Domain\Factory\ArrayFormFactory;
use TYPO3\CMS\Form\Domain\Model\FormDefinition;
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;

use function array_merge;
use function array_pop;
Expand Down Expand Up @@ -62,11 +62,9 @@ public function __construct()
*/
public function renderAction(): ResponseInterface
{
// check if headless not loaded & call original method in case of fluid configuration
$typoScriptSetup = $GLOBALS['TSFE'] instanceof TypoScriptFrontendController ? $GLOBALS['TSFE']->tmpl->setup : [];
if (!isset($typoScriptSetup['plugin.']['tx_headless.']['staticTemplate'])
|| (bool)$typoScriptSetup['plugin.']['tx_headless.']['staticTemplate'] === false
) {
$headlessMode = GeneralUtility::makeInstance(HeadlessMode::class);

if (!$headlessMode->withRequest($GLOBALS['TYPO3_REQUEST'])->isEnabled()) {
return parent::renderAction();
}

Expand Down
Loading

0 comments on commit b6a1620

Please sign in to comment.