Skip to content

[FEATURE] TYPO3 v14 compatibility - #282

Open
staempfli-webteam wants to merge 2 commits into
beechit:masterfrom
staempfli-webteam:feature/typo3-v14-compatibility
Open

[FEATURE] TYPO3 v14 compatibility#282
staempfli-webteam wants to merge 2 commits into
beechit:masterfrom
staempfli-webteam:feature/typo3-v14-compatibility

Conversation

@staempfli-webteam

Copy link
Copy Markdown

Summary

This PR makes the extension compatible with TYPO3 v14 (tested on 14.3.4) and PHP 8.4.

Besides migrating all APIs that were removed in v13/v14, it fixes two functional bugs that
already exist on master (details under "Bug fixes"): the public-URL event listener never
writes the generated URL back to the event, and — once that is fixed — the backend
ajax_dump_file URL leaks into frontend output because the old TYPO3_MODE === 'BE' guard
was lost in the PSR-14 migration.

Compatibility

TYPO3 ^14.3 (see "Open questions": ^14.0 probably works, only tested on 14.3.4)
PHP ^8.4
ke_search / solrfal integrations unchanged (hook/event interfaces still exist)

1. Migration of removed core APIs

Page module preview hook → PageContentPreviewRenderingEvent

$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['list_type_Info']
was removed in v12 (Breaking: #98375),
so the plugin info in the page module has silently not been rendered since v12.
Hooks/CmsLayout.php is replaced by EventListener/PageContentPreviewRenderingEventListener.php,
which matches on the falsecuredownload_filetree CType (plugins are CType-based since
Deprecation: #105076;
the existing BeechItFalSecuredownloadCTypeMigration upgrade wizard covers existing records).
The listener reads pi_flexform from the event's raw record and uses
StorageRepository::findByUid() instead of ResourceFactory::getStorageObject(), which was
removed in v14 (Important: #107735).

ButtonBar hook → ModifyButtonBarEvent

$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['Backend\Template\Components\ButtonBar']['getButtonsHook']
was removed in v12 (Breaking: #96806),
so the "folder permissions" button in the file list doc header has been missing since v12.
Hooks/DocHeaderButtonsHook.php is replaced by EventListener/ModifyButtonBarEventListener.php
(still based on Hooks/AbstractBeButtons). Buttons are created via the new
ComponentFactory since ButtonBar::makeLinkButton() is deprecated in v14
(Deprecation: #107823).

Context menu item provider

  • Removed the dead $GLOBALS['TYPO3_CONF_VARS']['BE']['ContextMenu']['ItemProviders']
    registration and the v11-only constructor arguments; providers are auto-configured via DI
    since v12 (Breaking: #96333).
    In v14 AbstractProvider has an argument-less constructor and receives its context via
    setContext().
  • Removed the v11 RequireJS branch in getAdditionalAttributes() and deleted the AMD module
    Resources/Public/JavaScript/ContextMenuActions.js (the ES6 module
    context-menu-actions.js is used since v12).

Fluid 5 / ViewHelpers

TYPO3 v14 ships Fluid 5 (Breaking: #108148).
In DownloadLinkViewHelper:

  • registerUniversalTagAttributes() / registerTagAttribute() were removed
    (Deprecation: #104223);
    unregistered arguments are applied as tag attributes automatically in Fluid ≥ 4.
  • The constructor now calls parent::__construct() (initializes the TagBuilder).
  • $GLOBALS['TSFE']->absRefPrefix was removed together with TypoScriptFrontendController
    (Deprecation: #105230,
    removed in Breaking: #105377).
    config.absRefPrefix is now read from the frontend.typoscript request attribute of the
    Fluid rendering context.

Icon API

ModifyIconForResourcePropertiesEvent::getSize() was replaced by getIconSize() returning
the IconSize enum (Deprecation: #101475,
removed in Breaking: #105377).
IconFactoryAspect::buildIconForResource() now type-hints IconSize and accepts
?string $iconIdentifier (the event getter is nullable).

ContentObjectRenderer

GeneralUtility::makeInstance(ContentObjectRenderer::class, null) fatals in v14 (the
constructor requires DI services). ModifyFileDumpEventListener::resolveUrl() now resolves
the instance from the container and passes the PSR-7 request via setRequest() so
typolink works in the eID context.

Upgrade wizard

The UpgradeWizard attribute and AbstractListTypeToCTypeUpdate moved from EXT:install to
EXT:core in v14 (Deprecation: #106947).
Imports were updated and the typo3/cms-install requirement was dropped from
composer.json — the extension no longer references any EXT:install class.

Misc

  • ext_localconf.php: removed a leftover GeneralUtility::makeInstance(IconRegistry::class)
    call — instantiating IconRegistry in ext_localconf.php throws a RuntimeException on
    every request in v14 (icons are registered via Configuration/Icons.php).
  • Configuration/FlexForms/FileTree.xml: removed the invalid <internal_type>db</internal_type>
    option from the settings.storage select field.
  • TCA: removed dead TYPO3 v11 conditionals (Typo3Version checks) in
    tx_falsecuredownload_folder.php and Overrides/sys_file_metadata.php.
  • Deleted Classes/Service/UserFileMountService.php: its parent class
    TYPO3\CMS\Core\Resource\Service\UserFileMountService no longer exists in v14 and nothing
    references it (the flexform folder selector uses type=folder since v12).
  • ext_emconf.php: depends raised to 14.3.0 - 14.99.99; removed the obsolete
    clearCacheOnLoad option.
  • Added rector.php with the TYPO3 14 rule set (optional, see PR notes).

2. Bug fixes

Public URLs of secured files pointed to the backend route in the frontend (or were lost)

Historically the ResourceStorage PreGeneratePublicUrl slot was registered only in
backend mode
(if (TYPO3_MODE === 'BE') in ext_localconf.php) so that backend editors
get working preview links (/typo3/ajax/fal_securedownloads/dump_file) in the file list.
The migration to the PSR-14 GeneratePublicUrlForResourceEvent introduced two bugs that
mask each other on current master:

  1. The listener passes ['publicUrl' => ...] by value to
    PublicUrlAspect::generatePublicUrl() and never calls $event->setPublicUrl(), so the
    generated URL is silently discarded. (In the signal/slot era this worked because the
    dispatcher passed an array containing a reference.)
  2. The TYPO3_MODE === 'BE' guard was reduced to !Environment::isCli(), so once bug 1 is
    fixed, every frontend rendering of a file from a non-public storage (file links,
    images, processed files) points to the backend AJAX route and returns 401 for website
    visitors.

Fix in this PR:

  • PublicUrlAspect::generatePublicUrl() takes $urlData by reference and the listener
    writes the result back via $event->setPublicUrl().
  • The listener only acts when ApplicationType::fromRequest(...)->isBackend(). In frontend
    context it sets no URL, which lets TYPO3 core generate its standard
    index.php?eID=dumpFile&...&token=... URL — and downloads through that endpoint are
    permission-checked by this extension's ModifyFileDumpEventListener, exactly as intended.

DownloadLinkViewHelper generated invalid tokens for TYPO3 v14

TYPO3 v14 hardened HMAC generation to SHA3-256
(Breaking: #106307).
FileDumpController validates the eID=dumpFile token with
HashAlgo::SHA3_256, while the ViewHelper still signed with the SHA-1 default — every link
produced by <sd:downloadLink> was rejected with 403. The ViewHelper now signs with
HashAlgo::SHA3_256.

(The extension-internal pair PublicUrlAspect / BePublicUrlController uses its own
BeResourceStorageDumpFile token on both sides and is unaffected.)

3. File overview

Change File
Renamed + migrated to event Classes/Hooks/CmsLayout.phpClasses/EventListener/PageContentPreviewRenderingEventListener.php
Renamed + migrated to event Classes/Hooks/DocHeaderButtonsHook.phpClasses/EventListener/ModifyButtonBarEventListener.php
Deleted (parent class removed in v14, unused) Classes/Service/UserFileMountService.php
Deleted (v11 RequireJS module) Resources/Public/JavaScript/ContextMenuActions.js
Modified ext_localconf.php, ext_emconf.php, composer.json, Configuration/Services.yaml, Configuration/FlexForms/FileTree.xml, Configuration/TCA/*, Classes/Aspects/*, Classes/ContextMenu/ItemProvider.php, Classes/EventListener/*, Classes/ViewHelpers/DownloadLinkViewHelper.php, Classes/Updates/BeechItFalSecuredownloadCTypeMigration.php, Resources/Public/JavaScript/context-menu-actions.js

4. How this was tested

Manually on TYPO3 14.3.4 / PHP 8.4 (composer mode, ddev):

  • Frontend: page containing file links and images (incl. processed images) from a
    non-public storage secured via folder permissions.
    • Anonymous visitor: URLs are core eID=dumpFile links; download attempt returns
      403 "Authentication required!".
    • Logged-in frontend user with the required group: all files (PDF, DOCX, XLSX) and the
      processed image download/stream correctly (200, correct MIME types).
  • Backend: file list (Media module) renders secured folders/files with permission
    overlay icons; folder context menu and doc-header "folder permissions" button work.
  • CLI: cache:flush, cache:warmup, scheduler runs without errors (DI container compiles,
    ext_localconf/TCA load cleanly).

Not covered: automated tests (the repository currently has none), ke_search / solrfal
integrations (interfaces unchanged).

5. Open questions for maintainers

  • Version: this should probably be released as 7.0.0 (v14-only). If you want to keep
    a v13-compatible line, the two bug fixes in section 2 apply to master as well (bug 1
    verbatim; bug 2's guard is also correct for v12/v13).
  • Constraint: composer.json currently requires ^14.3 because that is what this was
    tested against; all migrated APIs exist since 14.0, so ^14.0 is likely fine.
  • ext_emconf.php constraints.depends.typo3 was set to 14.3.0 - 14.99.99 accordingly.

@Kephson

Kephson commented Jul 7, 2026

Copy link
Copy Markdown

Hi, one mark from my side: the PHP version constraint in the composer.json should be "^8.2" instead of "^8.4", because TYPO3 14.3 allows PHP 8.2 until PHP 8.5, see https://packagist.org/packages/typo3/cms-core

@Kephson

Kephson commented Jul 7, 2026

Copy link
Copy Markdown

Found another issue in PageContentPreviewRenderingEventListener.php:
You should switch in line 129 the isset() and is_array() function, because if $key is accessed before you will get an PHP error.
Switch it like this:
&& isset($flexform[$sheet]['lDEF'][$key]['vDEF']) && is_array($flexform[$sheet]['lDEF'][$key]

@Kephson Kephson mentioned this pull request Jul 9, 2026
Review feedback (thanks @Kephson):
- Lower PHP requirement to ^8.2 to match typo3/cms-core ^14.3 and
  remove PHP 8.4-only constructs (new without parentheses in
  FileTreeStateController, array_any() in CheckPermissions)
- Point rector.php at PHP 8.2 so the 8.4-only syntax is not
  re-introduced on future rector runs
- getFieldFromFlexform(): replace the is_array()/isset() chain with a
  single null-coalescing lookup to avoid undefined array key warnings
- Guard against xml2array() returning an error string for empty or
  invalid pi_flexform values (TypeError on the typed array property)

Additionally, add two changes that were described in the PR summary
but missing from the initial commit:
- Restrict the public url rewrite to the BE dump_file route to
  backend context, so frontend urls are generated by the TYPO3 core
  (eID=dumpFile) and secured downloads work for website visitors
- Sign eID download tokens in DownloadLinkViewHelper with SHA3-256
  as required by the TYPO3 v14 FileDumpController
@staempfli-webteam

Copy link
Copy Markdown
Author

Hi @Kephson, thanks a lot for the review — both points were valid and are fixed in the latest commit.

PHP constraint: You're right, typo3/cms-core ^14.3 allows PHP 8.2–8.5, so I lowered the requirement to "php": "^8.2". Note this needed two code changes as well, since the branch contained PHP 8.4-only constructs that would fail on 8.2/8.3:

  • new Response()->withStatus(404) (parenthesis-free new is 8.4 syntax) → (new Response())->withStatus(404)
  • array_any() in CheckPermissions::matchFeGroupsWithFeUser() (function only exists since PHP 8.4) → replaced with a plain foreach

I also pointed rector.php at PHP 8.2 so a future rector run doesn't re-introduce them.

getFieldFromFlexform(): Good catch. I went one step further than swapping isset()/is_array(), because the earlier is_array($flexform[$sheet]) / is_array(...['lDEF']) checks would still emit "Undefined array key" warnings when the sheet is missing. The whole condition is now a single warning-free lookup:

$value = $this->flexformData['data'][$sheet]['lDEF'][$key]['vDEF'] ?? null;
return is_string($value) ? $value : null;

The is_string() check also protects the ?string return type in case vDEF ever holds an array. While touching this, I fixed one more latent issue in the same listener: GeneralUtility::xml2array() returns an error string for empty/invalid pi_flexform (e.g. a freshly inserted, never-saved plugin element), which would have caused a TypeError on the typed array $flexformData property — the result is now guarded with is_array().

One more thing: while preparing this update I noticed two changes that were described in the PR summary but accidentally missing from the initial commit — the backend-context restriction of the dump_file URL rewrite and the SHA3-256 signing of the eID download tokens. Both are included in this commit now, so the code matches the PR description. Sorry for the confusion, and thanks again for taking the time to review!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants