Skip to content

Commit

Permalink
[FEATURE] Add TYPO3 v13 compatibility
Browse files Browse the repository at this point in the history
- adjust tests for T3v13
  • Loading branch information
twoldanski committed Sep 23, 2024
1 parent 9dfec77 commit cdde24b
Show file tree
Hide file tree
Showing 22 changed files with 343 additions and 294 deletions.
70 changes: 65 additions & 5 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,67 @@ jobs:
- "php:cs-fixer"
php-version:
- 8.2
tests:
testV12:
runs-on: ubuntu-latest
strategy:
max-parallel: 2
matrix:
php-versions: [8.1]
php-versions: [ 8.1, 8.2 ]
typo3-versions:
- {typo3: 12, testing: ^7.0@dev}
- { typo3: 12, testing: ^7.0, phpunit: ^11, yaml: ^6 }

name: "Run tests with PHP ${{ matrix.php-versions }}
using TYPO3 ${{ matrix.typo3-versions.typo3 }}
with testing framework version ${{ matrix.typo3-versions.testing }}"
steps:
- uses: actions/checkout@v4

- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
extensions: intl, mbstring, pdo_sqlite

- name: "Cache composer dependencies"
uses: actions/cache@v4
with:
path: ~/.composer/cache
key: php-${{ matrix.php-versions }}
-typo3-${{ matrix.typo3-versions.typo3 }}
-phpunit-${{ matrix.typo3-versions.phpunit }}
-yaml-${{ matrix.typo3-versions.yaml }}
-testing-${{ matrix.typo3-versions.testing }}
-composer-${{ hashFiles('composer.json') }}
restore-keys: |
php-${{ matrix.php-versions }}
-typo3-${{ matrix.typo3-versions.typo3 }}
-phpunit-${{ matrix.typo3-versions.phpunit }}
-phpcov-${{ matrix.typo3-versions.phpcov }}
-yaml-${{ matrix.typo3-versions.yaml }}
-testing-${{ matrix.typo3-versions.testing }}composer-
php-${{ matrix.php-versions }}-typo3-
- name: "Install composer dependencies"
run: composer require typo3/minimal
"^${{ matrix.typo3-versions.typo3 }}"
typo3/testing-framework "${{ matrix.typo3-versions.testing }}"
phpunit/phpunit "${{ matrix.typo3-versions.phpunit }}"
symfony/yaml "${{ matrix.typo3-versions.yaml }}"
--prefer-dist --no-progress --no-suggest

- name: "Run Unit tests"
run: composer ci:test:unit

- name: "Functional tests"
run: composer ci:test:functional

testsV13:
runs-on: ubuntu-latest
strategy:
max-parallel: 2
matrix:
php-versions: [8.2, 8.3]
typo3-versions:
- {typo3: 13, testing: ^8.0, phpunit: ^11, yaml: ^7 }

name: "Run tests with PHP ${{ matrix.php-versions }}
using TYPO3 ${{ matrix.typo3-versions.typo3 }}
Expand All @@ -89,19 +142,26 @@ jobs:
path: ~/.composer/cache
key: php-${{ matrix.php-versions }}
-typo3-${{ matrix.typo3-versions.typo3 }}
-phpunit-${{ matrix.typo3-versions.phpunit }}
-phpcov-${{ matrix.typo3-versions.phpcov }}
-yaml-${{ matrix.typo3-versions.yaml }}
-testing-${{ matrix.typo3-versions.testing }}
-composer-${{ hashFiles('composer.json') }}
restore-keys: |
php-${{ matrix.php-versions }}
-typo3-${{ matrix.typo3-versions.typo3 }}
-phpunit-${{ matrix.typo3-versions.phpunit }}
-yaml-${{ matrix.typo3-versions.yaml }}
-testing-${{ matrix.typo3-versions.testing }}composer-
php-${{ matrix.php-versions }}-typo3-
- name: "Install composer dependencies"
run: composer require typo3/minimal
run: composer remove php-coveralls/php-coveralls --dev --no-progress && composer require typo3/minimal
"^${{ matrix.typo3-versions.typo3 }}"
typo3/testing-framework "${{ matrix.typo3-versions.testing }}"
--prefer-dist --no-progress --no-suggest
phpunit/phpunit "${{ matrix.typo3-versions.phpunit }}"
symfony/yaml "${{ matrix.typo3-versions.yaml }}"
--prefer-dist --no-progress

- name: "Run Unit tests"
run: composer ci:test:unit
Expand Down
51 changes: 44 additions & 7 deletions Classes/ContentObject/JsonContentContentObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@
use FriendsOfTYPO3\Headless\Json\JsonEncoder;
use FriendsOfTYPO3\Headless\Json\JsonEncoderInterface;
use FriendsOfTYPO3\Headless\Utility\HeadlessUserInt;
use Psr\EventDispatcher\EventDispatcherInterface;
use TYPO3\CMS\Backend\View\BackendLayoutView;
use TYPO3\CMS\Core\Information\Typo3Version;
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentContentObject;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
Expand All @@ -23,6 +26,7 @@
use function count;
use function is_array;
use function json_decode;
use function json_encode;
use function str_contains;
use function trim;

Expand Down Expand Up @@ -102,11 +106,18 @@ class JsonContentContentObject extends ContentContentObject
{
private HeadlessUserInt $headlessUserInt;
private JsonEncoderInterface $jsonEncoder;
/**
* @var mixed|object|\Psr\Log\LoggerAwareInterface|\TYPO3\CMS\Core\SingletonInterface|TimeTracker|(TimeTracker&\Psr\Log\LoggerAwareInterface)|(TimeTracker&\TYPO3\CMS\Core\SingletonInterface)|null
*/
private TimeTracker $timeTracker;
private EventDispatcherInterface $eventDispatcher;

public function __construct()
{
$this->headlessUserInt = GeneralUtility::makeInstance(HeadlessUserInt::class);
$this->jsonEncoder = GeneralUtility::makeInstance(JsonEncoder::class);
$this->timeTracker = GeneralUtility::makeInstance(TimeTracker::class);
$this->eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class);
}

/**
Expand Down Expand Up @@ -199,6 +210,8 @@ protected function groupContentElementsByColPos(array $contentElements, array $c
*/
private function prepareValue(array $conf): array
{
$t3v13andAbove = (new Typo3Version())->getMajorVersion() >= 13;

$frontendController = $this->getTypoScriptFrontendController();
$theValue = [];
$originalRec = $frontendController->currentRecord;
Expand Down Expand Up @@ -233,20 +246,44 @@ private function prepareValue(array $conf): array
$tmpValue = '';

do {
$records = $this->cObj->getRecords($conf['table'], $conf['select.']);
if ($t3v13andAbove) {
$modifyRecordsEvent = $this->eventDispatcher->dispatch(
new \TYPO3\CMS\Frontend\ContentObject\Event\ModifyRecordsAfterFetchingContentEvent(
$this->cObj->getRecords($conf['table'], $conf['select.']),
json_encode($theValue, JSON_THROW_ON_ERROR),
$slide,
$slideCollect,
$slideCollectReverse,
$slideCollectFuzzy,
$conf
)
);

$records = $modifyRecordsEvent->getRecords();
$theValue = json_decode($modifyRecordsEvent->getFinalContent(), true, 512, JSON_THROW_ON_ERROR);
$slide = $modifyRecordsEvent->getSlide();
$slideCollect = $modifyRecordsEvent->getSlideCollect();
$slideCollectReverse = $modifyRecordsEvent->getSlideCollectReverse();
$slideCollectFuzzy = $modifyRecordsEvent->getSlideCollectFuzzy();
$conf = $modifyRecordsEvent->getConfiguration();
} else {
$records = $this->cObj->getRecords($conf['table'], $conf['select.']);
}
$cobjValue = [];
if (!empty($records)) {
$this->getTimeTracker()->setTSlogMessage('NUMROWS: ' . count($records));
$this->timeTracker->setTSlogMessage('NUMROWS: ' . count($records));

$cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class, $frontendController);
$cObj->setParent($this->cObj->data, $this->cObj->currentRecord);
$this->cObj->currentRecordNumber = 0;

foreach ($records as $row) {
// Call hook for possible manipulation of database row for cObj->data
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content_content.php']['modifyDBRow'] ?? [] as $className) {
$_procObj = GeneralUtility::makeInstance($className);
$_procObj->modifyDBRow($row, $conf['table']);
if (!$t3v13andAbove) {
// Call hook for possible manipulation of database row for cObj->data
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content_content.php']['modifyDBRow'] ?? [] as $className) {
$_procObj = GeneralUtility::makeInstance($className);
$_procObj->modifyDBRow($row, $conf['table']);
}
}
$registerField = $conf['table'] . ':' . ($row['uid'] ?? 0);
if (!($frontendController->recordRegister[$registerField] ?? false)) {
Expand Down Expand Up @@ -282,7 +319,7 @@ private function prepareValue(array $conf): array
}
$again = (string)$conf['select.']['pidInList'] !== '';
}
} while ($again && $slide && ((string)$tmpValue === '' && $slideCollectFuzzy || $slideCollect));
} while ($again && $slide && (((string)$tmpValue === '' && $slideCollectFuzzy) || $slideCollect));

$theValue = $this->groupContentElementsByColPos($theValue, $conf);
// Restore
Expand Down
113 changes: 11 additions & 102 deletions Classes/Middleware/ShortcutAndMountPointRedirect.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,16 @@
namespace FriendsOfTYPO3\Headless\Middleware;

use FriendsOfTYPO3\Headless\Utility\HeadlessMode;
use FriendsOfTYPO3\Headless\Utility\UrlUtility;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
use TYPO3\CMS\Core\Http\ImmediateResponseException;
use TYPO3\CMS\Core\Http\JsonResponse;
use TYPO3\CMS\Core\Http\RedirectResponse;
use TYPO3\CMS\Core\Routing\PageArguments;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\Controller\ErrorController;

use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons;

use function is_array;
use function parse_url;

class ShortcutAndMountPointRedirect implements MiddlewareInterface, LoggerAwareInterface
class ShortcutAndMountPointRedirect extends \TYPO3\CMS\Frontend\Middleware\ShortcutAndMountPointRedirect
{
use LoggerAwareTrait;

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

public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
Expand All @@ -46,96 +33,18 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
return $handler->handle($request);
}

$exposeInformation = $GLOBALS['TYPO3_CONF_VARS']['FE']['exposeRedirectInformation'] ?? false;

// Check for shortcut page and mount point redirect
try {
$redirectToUri = $this->getRedirectUri($request);
} catch (ImmediateResponseException $e) {
return $e->getResponse();
}
if ($redirectToUri !== null && $redirectToUri !== (string)$request->getUri()) {
/** @var PageArguments $pageArguments */
$pageArguments = $request->getAttribute('routing', null);
$message = 'TYPO3 Shortcut/Mountpoint' . ($exposeInformation ? ' at page with ID ' . $pageArguments->getPageId() : '');

if ($this->isHeadlessEnabled($request)) {
$parsed = parse_url($redirectToUri);
if (is_array($parsed)) {
$path = $parsed['path'] ?? '/';
return new JsonResponse(['redirectUrl' => $path, 'statusCode' => 307]);
}
}

return new RedirectResponse(
$redirectToUri,
307,
['X-Redirect-By' => $message]
);
}

// See if the current page is of doktype "External URL", if so, do a redirect as well.
$controller = $request->getAttribute('frontend.controller');
if ((int)$controller->page['doktype'] === PageRepository::DOKTYPE_LINK) {
$externalUrl = $this->prefixExternalPageUrl(
$controller->page['url'],
$request->getAttribute('normalizedParams')->getSiteUrl()
);
$message = 'TYPO3 External URL' . ($exposeInformation ? ' at page with ID ' . $controller->page['uid'] : '');
if (!empty($externalUrl)) {
if ($this->isHeadlessEnabled($request)) {
return new JsonResponse(['redirectUrl' => $externalUrl, 'statusCode' => 303]);
}
$coreResponse = parent::process($request, $handler);

return new RedirectResponse(
$externalUrl,
303,
['X-Redirect-By' => $message]
);
}
$this->logger->error(
'Page of type "External URL" could not be resolved properly',
[
'page' => $controller->page,
]
);
return GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
$request,
'Page of type "External URL" could not be resolved properly',
$controller->getPageAccessFailureReasons(PageAccessFailureReasons::INVALID_EXTERNAL_URL)
);
if ($coreResponse instanceof RedirectResponse && $this->isHeadlessEnabled($request)) {
return new JsonResponse([
'redirectUrl' => GeneralUtility::makeInstance(UrlUtility::class)
->withRequest($request)
->prepareRelativeUrlIfPossible($coreResponse->getHeader('location')[0] ?? ''),
'statusCode' => $coreResponse->getStatusCode(),
]);
}

return $handler->handle($request);
}

protected function getRedirectUri(ServerRequestInterface $request): ?string
{
$controller = $request->getAttribute('frontend.controller');
$redirectToUri = $controller->getRedirectUriForShortcut($request);
return $redirectToUri ?? $controller->getRedirectUriForMountPoint($request);
}

/**
* Returns the redirect URL for the input page row IF the doktype is set to 3.
*
* @param string $redirectTo The page row to return URL type for
* @param string $sitePrefix if no protocol or relative path given, the site prefix is added
* @return string The URL from based on the external page URL given with a prefix.
*/
protected function prefixExternalPageUrl(string $redirectTo, string $sitePrefix): string
{
$uI = parse_url($redirectTo);
// If relative path, prefix Site URL
// If it's a valid email without protocol, add "mailto:"
if (!($uI['scheme'] ?? false)) {
if (GeneralUtility::validEmail($redirectTo)) {
$redirectTo = 'mailto:' . $redirectTo;
} elseif (!str_starts_with($redirectTo, '/')) {
$redirectTo = $sitePrefix . $redirectTo;
}
}
return $redirectTo;
return $coreResponse;
}

private function isHeadlessEnabled(ServerRequestInterface $request): bool
Expand Down
Loading

0 comments on commit cdde24b

Please sign in to comment.