TYPO3 Core Changelog 

Extension key

core

Package name

typo3/cms-core

Version

main

Language

en

Author

TYPO3 contributors

License

This document is published under the Creative Commons BY 4.0 license.

Rendered

Wed, 11 Mar 2026 08:40:27 +0000


This extension provides the TYPO3 API, i.e. the core functionalities, which can be used and extended in any other TYPO3 extension.

Note: Every change to the TYPO3 Core which might affect your site is documented here.


Table of Contents:

ChangeLog v14 

Every change to the TYPO3 Core which might affect your site is documented here.

Also available 

14.2 Changes 

Table of contents

Breaking Changes 

None since TYPO3 v14.0 release.

Features 

Deprecation 

Important 

Feature: #32051 - Extbase Query expression builder for orderings 

See forge#32051

Description 

Extbase queries now support SQL function expressions in ORDER BY clauses through a new fluent API. This enables sorting results by computed values using functions like CONCAT, TRIM, and COALESCE.

New methods have been added to \TYPO3\CMS\Extbase\Persistence\QueryInterface :

  • orderBy() - Sets a single ordering, replacing any existing orderings
  • addOrderBy() - Adds an ordering to existing orderings
  • concat() - Creates a CONCAT expression
  • trim() - Creates a TRIM expression
  • coalesce() - Creates a COALESCE expression

Examples 

Order by concatenated fields:

$query = $this->myRepository->createQuery();
$query->orderBy(
    $query->concat('firstName', 'lastName'),
    QueryInterface::ORDER_ASCENDING
);
Copied!

Order by trimmed field:

$query->orderBy(
    $query->trim('title'),
    QueryInterface::ORDER_DESCENDING
);
Copied!

Order by first non-null value (fallback pattern):

$query->orderBy(
    $query->coalesce('nickname', 'firstName'),
    QueryInterface::ORDER_ASCENDING
);
Copied!

Chain multiple orderings:

$query
    ->orderBy($query->concat('firstName', 'lastName'))
    ->addOrderBy('createdAt', QueryInterface::ORDER_DESCENDING);
Copied!

Nest expressions:

$query->orderBy(
    $query->concat(
        $query->trim('firstName'),
        $query->trim('lastName')
    )
);
Copied!

Backwards Compatibility 

The existing setOrderings() method with array syntax continues to work:

$query->setOrderings([
    'title' => QueryInterface::ORDER_ASCENDING,
    'date' => QueryInterface::ORDER_DESCENDING,
]);
Copied!

Impact 

Developers can now use SQL functions in Extbase query orderings without resorting to raw SQL statements. This enables more flexible sorting logic while maintaining the abstraction and security benefits of the Extbase persistence layer.

Feature: #69190 - Add password generator "wizard" 

See forge#69190

Description 

Password generation in the backend is now driven by password policies. Each password policy can define a generator section with a class implementing \TYPO3\CMS\Core\PasswordPolicy\Generator\PasswordGeneratorInterface .

The passwordGenerator field control references a policy by name via the passwordPolicy option. The dice icon button next to the field generates a password using the configured generator.

The field control can be added to any password field via TCA configuration, making it available for extension developers as well.

Password policies 

The policy to use is determined by context:

  • Backend users: $GLOBALS['TYPO3_CONF_VARS']['BE']['passwordPolicy']
  • Frontend users: $GLOBALS['TYPO3_CONF_VARS']['FE']['passwordPolicy']

All password policies are registered under $GLOBALS['TYPO3_CONF_VARS']['SYS']['passwordPolicies'] .

TYPO3 ships with three pre-configured policies:

  • default — Used for backend and frontend users
  • installTool — Used for install tool passwords
  • secretToken — Used for secret token fields (e.g. webhooks, reactions)

Each policy contains both a generator and validators section. The generator is responsible for creating passwords, while validators enforce password requirements. They are configured independently within the same policy.

Example 

The TYPO3 core ships a PasswordGenerator implementation which is configured like this:

<?php

$GLOBALS['TYPO3_CONF_VARS']['SYS']['passwordPolicies']['default']['generator'] = [
    'className' => \TYPO3\CMS\Core\PasswordPolicy\Generator\PasswordGenerator::class,
    'options' => [
        'length' => 12,
        'upperCaseCharacters' => true,
        'lowerCaseCharacters' => true,
        'digitCharacters' => true,
        'specialCharacters' => true,
    ],
];
Copied!

The PasswordGenerator supports the following options:

  • length: Length of the generated password
  • upperCaseCharacters: Whether to include upper case characters
  • lowerCaseCharacters: Whether to include lower case characters
  • digitCharacters: Whether to include digits
  • specialCharacters: Whether to include special characters

Adjusting an existing policy:

config/system/additional.php OR typo3conf/system/additional.php
<?php

$GLOBALS['TYPO3_CONF_VARS']['SYS']['passwordPolicies']['default']['generator']['options']['length'] = 20;
$GLOBALS['TYPO3_CONF_VARS']['SYS']['passwordPolicies']['default']['generator']['options']['specialCharacters'] = false;
Copied!

Registering a custom password policy with a custom generator:

config/system/additional.php OR typo3conf/system/additional.php
<?php

$GLOBALS['TYPO3_CONF_VARS']['SYS']['passwordPolicies']['customPolicy'] = [
    'generator' => [
        'className' => \Vendor\MyPackage\PasswordPolicy\Generator\MyPasswordGenerator::class,
        'options' => [
            'length' => 12,
            'myCustomOption' => 'my custom Value',
        ],
    ],
    'validators' => [
        // your custom validators
    ],
];

$GLOBALS['TYPO3_CONF_VARS']['BE']['passwordPolicy'] = 'customPolicy';
$GLOBALS['TYPO3_CONF_VARS']['FE']['passwordPolicy'] = 'customPolicy';
Copied!

Impact 

Password generation for backend and frontend users is now configurable through password policies. The Install Tool command vendor/bin/typo3 install:password:set also respects the configured policy.

Feature: #78412 - Provide static tsconfig includes for be_users & be_groups 

See forge#78412

Description 

The tables be_users and be_groups are extended by an additional field which allows to select static Tsconfig defined by extensions. Following the syntax for the field tsconfig_includes in the table pages there are the following methods available:

For Backend users:

<?php

// in Extension/Configuration/TCA/Overrides/be_users.php
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::registerUserTSConfigFile(
    'extensionKey',
    'Configuration/Tsconfig/Static/example1.tsconfig',
    'Example 1'
);
Copied!

For Backend usergroups:

<?php

// in Extension/Configuration/TCA/Overrides/be_groups.php
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::registerUserGroupTSConfigFile(
    'extensionKey',
    'Configuration/Tsconfig/Static/example2.tsconfig',
    'Example 2'
);
Copied!

Impact 

The new fields can be used to define User Tsconfig code for specific users and usergroups which can be provided by extensions.

By using this approach instead of writing TSconfig directly to the TSconfig database field of be_users or be_groups reduces the amount of configuration saved in the database. This has many advantages in following scenarios:

  • Running a TYPO3 instance with automated deployment and GIT version control makes it easily possible to create/modify such an includable TSconfig snippet via a file change. It also helps keeping configuration streamlined for multiple environments like staging or production. Once the file is included you have the same user/group configuration without manually changing that in the database for each affected user or group.
  • Extension authors can ship predefined User TSconfig files which can be included by the TYPO3 backend user. That also applies to local (site) packages or your agency's base package.
  • Possible breaking changes in major upgrades can be automatically upgraded with tools like TYPO3 Fractor (enhancement for TYPO3 Rector). If a breaking change occurs within User TSconfig, such a tool can automatically upgrade the configuration ensuring that you don't forget to search for them in the depths of the database. This kind of approach could reduce the amount of recurring manual work. Particularly affected by this are large TYPO3 instances with a big amount of be_users or be_groups records.
  • Searching within existing User TSconfig which is stored in the database otherwise is possible in your IDE now. All your TSconfig is present in your codebase. This can also improve your daily productivity and greatly simplifies major upgrades. You can also go further and disable/ hide the TSconfig database field in projects to prevent saving User TSconfig in the database for users/groups at all.

Feature: #87435 - Make new content element wizard items sort order configurable 

See forge#87435

Description 

It is now possible to influence the order of content elements within wizard tabs in the new content element wizard by setting before and after values in Page TSconfig.

Previously, only the order of tabs could be configured, but not the order of individual content elements within a tab. This feature extends the existing ordering mechanism to individual content elements, following the same pattern as tab ordering.

This eliminates the need for workarounds like creating custom wizard groups to reorder elements.

Example 

mod.wizards.newContentElement.wizardItems {
    default.elements {
        textmedia {
            after = header
        }
        mask_article_card {
            after = textmedia
        }
        # Multiple elements can be specified (comma-separated)
        mask_article_list {
            after = header,textmedia
        }
        # Or use before
        header {
            before = textmedia
        }
    }
}
Copied!

Impact 

Integrators and developers can now control the exact order of content elements within each wizard tab using Page TSconfig, without needing to create custom wizard groups. The ordering mechanism follows the same syntax and behavior as the existing tab ordering feature (forge#71876), ensuring consistency and familiarity.

The feature works alongside the existing tab ordering: tabs can be ordered using before and after at the group level, and content elements within each tab can be ordered using before and after at the element level.

If no before or after configuration is specified for elements, they will retain their default order (typically the order defined in TCA).

Feature: #88470 - Custom message in form email finisher 

See forge#88470

Description 

The email finishers (EmailToSender and EmailToReceiver) of the TYPO3 form framework now support an optional custom message field. This allows editors to add a personalized text to the email sent by the form, either before or after the submitted form values.

The message field supports rich text editing using the form-content RTE preset, which provides formatting options like bold, italic, links, and lists.

A special placeholder {formValues} can be used within the message to control where the submitted form data table is rendered. If the placeholder is omitted, only the custom message is shown and the form values table is hidden.

The message field is available in:

  • The form editor backend module
  • The form finisher override settings

Impact 

Editors can now configure a custom message for email finishers directly in the form editor or via finisher overrides in the form plugin. This provides more flexibility in crafting email notifications without the need for custom Fluid templates.

Feature: #89951 - Cleanup command for form file upload folders 

See forge#89951

Description 

A new CLI command form:cleanup:uploads has been introduced to clean up old file upload folders created by the TYPO3 Form Framework.

When users upload files via FileUpload or ImageUpload form elements, the files are stored in form_<hash> sub-folders inside the configured upload directory. Over time these folders accumulate — both from completed and incomplete form submissions.

Since uploaded files are not moved upon form submission, there is no way to distinguish between folders from completed and abandoned submissions. The command identifies form upload folders by their naming pattern (form_ followed by exactly 40 hex characters) and their modification time. Folders older than a configurable retention period (default: 2 weeks) can be removed.

You must specify at least one upload folder to scan. Since each form element can configure a different upload folder via the saveToFileMount property (e.g. 1:/user_upload/, 2:/custom_uploads/), pass all relevant folders as arguments.

Usage 

# Dry-run: list form upload folders older than 2 weeks (default)
bin/typo3 form:cleanup:uploads 1:/user_upload/ --dry-run

# Delete folders older than 48 hours
bin/typo3 form:cleanup:uploads 1:/user_upload/ --retention-period=48

# Scan multiple upload folders
bin/typo3 form:cleanup:uploads 1:/user_upload/ 2:/custom_uploads/

# Force deletion without confirmation (useful for scheduler tasks)
bin/typo3 form:cleanup:uploads 1:/user_upload/ --force

# Verbose output shows details about each folder
bin/typo3 form:cleanup:uploads 1:/user_upload/ --dry-run -v
Copied!

Arguments 

upload-folder
Combined folder identifier(s) to scan (required). Multiple folders can be specified as separate arguments.

Options 

--retention-period / -r
Minimum age in hours before a folder is considered for removal (default: 336, i.e. 2 weeks).
--dry-run
Only list expired folders without deleting them.
--force / -f
Skip the interactive confirmation prompt. Automatically set when running with --no-interaction (e.g. in the TYPO3 Scheduler).

Scheduler integration 

The command is automatically available as a Scheduler task since it is registered via the #[AsCommand] attribute. Configure it to run periodically (e.g. once per day or once per week) to keep the upload folders clean.

Impact 

The new command provides a safe and configurable way to reclaim disk space from accumulated form upload folders. The conservative default retention period of 2 weeks ensures that files belonging to forms still being actively worked on are not accidentally removed.

Feature: #91924 - Add form element selection buttons to property grid 

See forge#91924

Description 

A form element selection button has been added to the property grid editors of the finishers EmailToSender and EmailToRecipient.

Impact 

A button is now displayed in the property grid that allows direct selection of form element identifiers without requiring manual input.

Feature: #97898 - TCA option isViewable for Page Types 

See forge#97898

Description 

A new TCA option isViewable is introduced for page types ("doktype") to configure whether a specific page type ("doktype") can be linked to in the page browser and in frontend TypoLink generation.

EXT:my_extension/Configuration/TCA/Overrides/pages.php
// Disable linking for custom page type
$GLOBALS['TCA']['pages']['types']['116']['isViewable'] = false;
Copied!

By default, all page types are viewable unless explicitly set to false.

TYPO3 core now marks the following page types as non-viewable in TCA:

  • Spacer (doktype 199)
  • SysFolder (doktype 254)

The existing TSconfig option TCEMAIN.preview.disableButtonForDokType is also respected when determining viewability in the backend page browser. If a page type is disabled for preview via TSconfig, it will also be non-viewable.

Impact 

The viewability of pages can now be configured in TCA, following the same pattern as the allowedRecordTypes option introduced in TYPO3 v14.1. This provides a centralized way to control which page types can be linked.

Extensions with custom page types that should not be viewable can now configure this directly in TCA:

EXT:my_extension/Configuration/TCA/Overrides/pages.php
$GLOBALS['TCA']['pages']['types'][(string)\MyVendor\MyExtension\Domain\PageType::MY_NON_VIEWABLE_TYPE] = [
    'isViewable' => false,
    'showitem' => '...',
];
Copied!

Feature: #102079 - Introduce BeforePersistingReportEvent for CSP violations 

See forge#102079

Description 

When a Content-Security-Policy violation report needs to be persisted, the \TYPO3\CMS\Core\Security\ContentSecurityPolicy\Event\BeforePersistingReportEvent can be used to provide an alternative report or to prevent a particular report from being persisted at all.

Example 

<?php
declare(strict_types=1);

namespace Example\Demo\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Event\BeforePersistingReportEvent;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Reporting\Report;

final class BeforePersistingReportEventListener
{
    private const BROWSER_PREFIXES = [
        'chrome-extension://',
        'moz-extension://',
        'safari-extension://',
    ];

    #[AsEventListener('example/security/before-persisting-csp-report')]
    public function __invoke(BeforePersistingReportEvent $event): void
    {
        // avoid persisting CSP violations that were caused by browser extensions
        $blockedUri = $event->originalReport->details['blocked-uri'] ?? null;
        if (is_string($blockedUri) && $this->isBrowserExtensions($blockedUri)) {
            $event->report = null;
            return;
        }
        // otherwise adjust report and provide custom meta-data
        $event->report = new Report(
            $event->originalReport->scope,
            $event->originalReport->status,
            $event->originalReport->requestTime,
            array_merge(
                $event->originalReport->meta,
                ['x-example' => '... additional meta-data ...']
            ),
            $event->originalReport->details,
            $event->originalReport->summary,
            $event->originalReport->uuid,
            $event->originalReport->created,
            $event->originalReport->changed
        );
    }

    private function isBrowserExtensions(string $blockedUri): bool
    {
        foreach (self::BROWSER_PREFIXES as $prefix) {
            if (str_starts_with($blockedUri, $prefix)) {
                return true;
            }
        }
        return false;
    }
}
Copied!

Impact 

The new BeforePersistingReportEvent allows custom control over whether and how Content-Security-Policy violation reports are persisted in TYPO3.

Feature: #102159 - Support additional parameters for TCA slug prefix userFunc 

See forge#102159

Description 

TCA slug prefix user functions now receive the full TCA field configuration and the field name as additional parameters.

User Function Implementation 

The prefix user function receives two additional keys alongside the existing parameters:

  • fieldName - The name of the slug field
  • config - The full TCA configuration array of the slug field
EXT:my_extension/Classes/Utility/SlugUtility.php
namespace MyExtension\Utility;

class SlugUtility
{
    public function generatePrefix(array $parameters): string
    {
        // Standard parameters (always available)
        $site = $parameters['site'];
        $languageId = $parameters['languageId'];
        $table = $parameters['table'];
        $row = $parameters['row'];

        $fieldName = $parameters['fieldName'];
        $config = $parameters['config'];

        return '/default/';
    }
}
Copied!

Available Parameters 

The user function receives an array with the following keys:

  • site - The current site object
  • languageId - The current language ID (int)
  • table - The table name (string)
  • row - The current record data (array)
  • fieldName - The name of the slug field (string)
  • config - The full TCA configuration array of the slug field

Impact 

Extension developers can access the complete TCA field configuration and the field name directly within prefix user functions.

Feature: #102194 - Introduce QueryBuilderPaginator 

See forge#102194

Description 

A new \TYPO3\CMS\Core\Pagination\QueryBuilderPaginator is introduced to enable pagination of \TYPO3\CMS\Core\Database\Query\QueryBuilder instances directly.

The paginator implements the existing \TYPO3\CMS\Core\Pagination\PaginatorInterface and integrates seamlessly with the existing \TYPO3\CMS\Core\Pagination\SimplePagination and \TYPO3\CMS\Core\Pagination\SlidingWindowPagination classes.

The paginated items are fetched only once per page request by storing the result internally, avoiding double execution of the database statement.

The total item count is determined robustly using a common table expression (CTE) wrapping the passed QueryBuilder instance. This approach correctly handles advanced queries involving UNION, nested CTEs, windowing functions, or grouping.

Impact 

A new QueryBuilderPaginator is available to paginate QueryBuilder result sets using the standard TYPO3 pagination API.

Example 

EXT:my_extension/Classes/Controller/MyController.php
use TYPO3\CMS\Core\Pagination\QueryBuilderPaginator;
use TYPO3\CMS\Core\Pagination\SimplePagination;

$paginator = new QueryBuilderPaginator(
    queryBuilder: $queryBuilder,
    currentPageNumber: $currentPage,
    itemsPerPage: 10,
);
$pagination = new SimplePagination($paginator);

// Retrieve the items for the current page
$items = $paginator->getPaginatedItems();
Copied!

Feature: #102430 - Flush cache tags for file and folder operations 

See forge#102430

Description 

This feature is guarded with toggle "frontend.cache.autoTagging" and experimental for now: The core flushes cache tags for all kind of records automatically when they are created, changed or deleted. For files and folders this is not the case. This feature adds cache tag handling for file or folder operations when they are created, changed or deleted. Also the file metadata changes are handled correctly now. This leads to a better editor experience when the cache tags are used correctly.

Impact 

Integrators and extension developers can now add sys_file_${uid} and sys_file_metadata_${uid} as cache tags and they are flushed correctly from the TYPO3 core when an editor interacts with them in the filelist module.

Feature: #102790 - Line wrapping option for code editor 

See forge#102790

Description 

A new TCA appearance option lineWrapping has been added for the codeEditor render type. When enabled, long lines are wrapped within the editor instead of requiring horizontal scrolling.

Example:

'config' => [
    'type' => 'text',
    'renderType' => 'codeEditor',
    'format' => 'html',
    'appearance' => [
        'lineWrapping' => true,
    ],
],
Copied!

Impact 

Code editor fields can now be configured to wrap long lines by setting lineWrapping in the appearance array.

Feature: #104546 - Support ICU MessageFormat for plural forms 

See forge#104546

Description 

TYPO3 now supports ICU MessageFormat for translations, enabling proper handling of plural forms, gender-based selections, and other locale-aware formatting directly in language labels.

ICU MessageFormat is an internationalization standard that allows messages to contain placeholders that can vary based on parameters like quantity, gender, or other conditions. This is particularly useful for proper pluralization in languages with complex plural rules.

The format is automatically detected when using named arguments (associative arrays) in translation calls. If the message contains ICU patterns like {count, plural, ...} or {name}, and named arguments are provided, the ICU MessageFormatter will be used automatically.

Language file format 

ICU MessageFormat strings are stored as regular translation strings in XLIFF files:

EXT:my_extension/Resources/Private/Language/locallang.xlf
<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
    <file source-language="en" datatype="plaintext" original="locallang.xlf">
        <body>
            <!-- Simple plural form -->
            <trans-unit id="file_count">
                <source>{count, plural, one {# file} other {# files}}</source>
            </trans-unit>

            <!-- Plural with zero case -->
            <trans-unit id="item_count">
                <source>{count, plural, =0 {no items} one {# item} other {# items}}</source>
            </trans-unit>

            <!-- Combined placeholder and plural -->
            <trans-unit id="greeting">
                <source>Hello {name}, you have {count, plural, one {# message} other {# messages}}.</source>
            </trans-unit>

            <!-- Gender selection -->
            <trans-unit id="profile_update">
                <source>{gender, select, male {He} female {She} other {They}} updated the profile.</source>
            </trans-unit>

            <!-- Simple named placeholder -->
            <trans-unit id="welcome">
                <source>Welcome, {name}!</source>
            </trans-unit>
        </body>
    </file>
</xliff>
Copied!

PHP usage 

Use named arguments (associative array) to trigger ICU MessageFormat processing:

Using ICU MessageFormat with LanguageService
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;

$languageService = GeneralUtility::makeInstance(LanguageServiceFactory::class)
    ->createFromUserPreferences($backendUser);

// ICU plural forms - use named arguments
$label = $languageService->translate(
    'file_count',
    'my_extension.messages',
    ['count' => 5]
);
// Result: "5 files"

// Combined placeholder and plural
$label = $languageService->translate(
    'greeting',
    'my_extension.messages',
    ['name' => 'John', 'count' => 3]
);
// Result: "Hello John, you have 3 messages."

// sprintf-style still works with positional arguments
$label = $languageService->translate(
    'downloaded_times',  // Label: "Downloaded %d times"
    'my_extension.messages',
    [42]  // Positional arguments use sprintf
);
// Result: "Downloaded 42 times"
Copied!
Using ICU MessageFormat with LocalizationUtility
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;

// With named arguments for ICU format
$label = LocalizationUtility::translate(
    'file_count',
    'MyExtension',
    ['count' => 1]
);
// Result: "1 file"
Copied!

Fluid usage 

In Fluid templates, use named arguments in the arguments attribute:

EXT:my_extension/Resources/Private/Templates/Example.html
<!-- ICU plural forms with named arguments -->
<f:translate key="file_count" arguments="{count: numberOfFiles}" />

<!-- Combined placeholder and plural -->
<f:translate key="greeting" arguments="{name: userName, count: messageCount}" />

<!-- Gender selection -->
<f:translate key="profile_update" arguments="{gender: userGender}" />

<!-- sprintf-style with positional arguments still works -->
<f:translate key="downloaded_times" arguments="{0: downloadCount}" />
Copied!

ICU MessageFormat syntax reference 

Plural forms:

{variable, plural,
    =0 {zero case}
    one {singular case}
    other {plural case}
}
Copied!

Select (gender/choice):

{variable, select,
    male {He}
    female {She}
    other {They}
}
Copied!

Number formatting:

{count, number}           - Basic number
{price, number, currency} - Currency format
Copied!

The # symbol in plural patterns is replaced with the actual number.

Impact 

This feature provides a standards-based approach to pluralization that:

  • Uses the well-tested ICU library (via PHP's intl extension)
  • Automatically handles locale-specific plural rules
  • Supports complex pluralization for languages like Russian or Arabic
  • Is backward compatible - existing sprintf-style translations continue to work

The system automatically detects which format to use based on the arguments:

  • Named arguments (associative array): Uses ICU MessageFormat
  • Positional arguments (indexed array): Uses sprintf

Feature: #105084 - Add setting to configure indexed_search pagination 

See forge#105084

Description 

A new TypoScript setting plugin.tx_indexedsearch.settings.pagination_type has been introduced to select the pagination implementation used by EXT:indexed_search.

Available values:

  • simple: uses \TYPO3\CMS\Core\Pagination\SimplePagination and renders all result pages.
  • slidingWindow: uses \TYPO3\CMS\Core\Pagination\SlidingWindowPagination and limits shown page links according to plugin.tx_indexedsearch.settings.page_links.

The default remains simple to preserve existing behavior. Integrators can switch to slidingWindow to make page_links effective for indexed_search result browsing.

Impact 

Integrators can now switch between core pagination implementations via TypoScript without custom PHP code.

Advanced, fully custom pagination logic can still be implemented via \TYPO3\CMS\IndexedSearch\Event\ModifySearchResultSetsEvent.

Feature: #105708 - Multiple file upload for EXT:form elements 

See forge#105708

Description 

The TYPO3 Form Framework now supports multiple file uploads for the FileUpload and ImageUpload form elements. This allows users to select and upload multiple files at once using a single form field.

The implementation follows the same security patterns as Extbase's file upload handling, using HMAC-signed deletion requests to ensure secure file removal.

Configuration 

To enable multiple file uploads for a form element, set the multiple property to true in your form definition:

fileadmin/form_definitions/someForm.yaml
type: Form
identifier: contact-form
label: 'Contact Form'
prototypeName: standard
renderables:
  - type: Page
    identifier: page-1
    label: 'Page 1'
    renderables:
      - type: FileUpload
        identifier: attachments
        label: 'Attachments'
        properties:
          multiple: true
          allowRemoval: true
          saveToFileMount: '1:/user_upload/'
          allowedMimeTypes:
            - application/pdf
            - image/jpeg

      - type: ImageUpload
        identifier: images
        label: 'Images'
        properties:
          multiple: true
          allowRemoval: true
          saveToFileMount: '1:/user_upload/'
          allowedMimeTypes:
            - image/jpeg
            - image/png
Copied!

The multiple option is also available in the Form Editor backend module as a checkbox in the element's inspector panel.

The allowRemoval property enables users to remove previously uploaded files before submitting the form. When enabled, a "Remove" checkbox is displayed next to each uploaded file.

File count validation 

The existing Count validator can now be used with FileUpload and ImageUpload elements to limit the number of uploaded files:

fileadmin/form_definitions/someForm.yaml
- type: FileUpload
  identifier: attachments
  label: 'Attachments'
  properties:
    multiple: true
  validators:
    - identifier: Count
      options:
        minimum: 1
        maximum: 5
Copied!

Frontend rendering 

When multiple is enabled:

  • The file input field renders with the HTML5 multiple attribute
  • Previously uploaded files are displayed in a list with individual remove checkboxes
  • Users can select multiple files in the browser's file picker dialog
  • On the summary page, multiple files are displayed as a list

File deletion 

The implementation uses HMAC-signed deletion requests similar to Extbase's file handling. Each uploaded file displays a checkbox that, when checked, marks the file for removal on form submission. The deletion data is signed with an HMAC to prevent manipulation.

A new ViewHelper <formvh:form.uploadDeleteCheckbox> is available for custom templates:

<formvh:form.uploadDeleteCheckbox
    property="{element.identifier}"
    fileReference="{file}"
    fileIndex="{iterator.index}"
/>
Copied!

Adapting custom finishers for multiple file uploads 

When multiple is enabled on a FileUpload element, the value returned by $formRuntime[$element->getIdentifier()] is an ObjectStorage<FileReference> instead of a single FileReference. Custom finishers that process file uploads need to be adapted to handle both cases (single and multiple uploads).

The following example shows the pattern used in the core EmailFinisher and DeleteUploadsFinisher:

EXT:my_extension/Classes/Domain/Finishers/MyFinisher.php
use TYPO3\CMS\Core\Resource\FileInterface;
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
use TYPO3\CMS\Form\Domain\Finishers\AbstractFinisher;
use TYPO3\CMS\Form\Domain\Model\FormElements\FileUpload;

class MyFinisher extends AbstractFinisher
{
    protected function executeInternal(): void
    {
        $formRuntime = $this->finisherContext->getFormRuntime();

        foreach ($formRuntime->getFormDefinition()->getRenderablesRecursively() as $element) {
            if (!$element instanceof FileUpload) {
                continue;
            }

            $file = $formRuntime[$element->getIdentifier()];

            // Single file upload: value is a FileReference
            if ($file instanceof FileReference) {
                $this->processFile($file->getOriginalResource());
            }

            // Multiple file upload: value is an ObjectStorage of FileReferences
            if ($file instanceof ObjectStorage) {
                foreach ($file as $singleFile) {
                    if ($singleFile instanceof FileReference) {
                        $this->processFile($singleFile->getOriginalResource());
                    }
                }
            }
        }
    }

    private function processFile(FileInterface $file): void
    {
        // Your custom logic, e.g. move, copy, attach, etc.
    }
}
Copied!

Per-element validation with ObjectStorageElementValidatorInterface 

When a form field's value is an ObjectStorage (e.g. a multi-file upload), the ProcessingRule must decide how to call each registered validator:

  • Collection-level validators (default) receive the entire ObjectStorage. Use this for validators that check the collection as a whole, such as CountValidator (minimum/maximum number of items).
  • Element-level validators receive each item individually. Use this for validators that inspect a single item, such as MimeTypeValidator or FileSizeValidator.

To mark a validator as element-level, implement the marker interface ObjectStorageElementValidatorInterface:

EXT:my_extension/Classes/Validation/MyPerFileValidator.php
use TYPO3\CMS\Extbase\Validation\Validator\AbstractValidator;
use TYPO3\CMS\Form\Mvc\Validation\ObjectStorageElementValidatorInterface;

final class MyFileValidator extends AbstractValidator implements ObjectStorageElementValidatorInterface
{
    public function isValid(mixed $value): void
    {
        // $value is a single element from the ObjectStorage,
        // e.g. a FileReference — NOT the whole collection.
    }
}
Copied!

For single-value fields (non-ObjectStorage), the interface has no effect — validators are always called with the field value directly.

Impact 

  • Form integrators can now create forms that accept multiple file uploads without custom extensions
  • The FileUpload and ImageUpload elements support the new multiple property
  • All existing finishers (EmailFinisher, SaveToDatabaseFinisher, DeleteUploadsFinisher) automatically support multiple file uploads
  • Email templates display multiple files as a list of filenames
  • The summary page displays multiple images as a gallery and multiple files as a filename list

Feature: #105742 - Synchronized manipulation of all crop variants 

See forge#105742

Description 

The image manipulation wizard allows images to be cropped using multiple crop variants. When there were many variants, each one had to be edited individually, and all changes had to be made manually multiple times. This became particularly tedious when the editor wanted to apply precisely the same crop values across all variants.

This feature introduces a new checkbox that allows the editor to crop all variants simultaneously. The prerequisite for enabling this checkbox is that all crop variants must be defined with identical aspect ratios and other identical configuration (apart from its title).

A new sub-option of the cropVariants TCA/TCEFORM option array called excludeFromSync allows developers to exclude specific crop variants from being affected by synchronized cropping.

This is useful, for example, when a special crop variant for a list view is added to the standard set of crop variants, and its differing configuration should still allow the other crop variants to be synchronizable.

Example 

The following code is the definition of the standard crop variants for a bootstrap-based template.

All crop variants are defined identically (except the title) to enable the synchronized cropping feature.

But for tx_news, a new crop variant for the listview is added, and excludeFromSync = 1 is used to specifically allow one exemption of the synchronize-feature.

TCEFORM.sys_file_reference.crop.config.cropVariants {
    xxl {
        title = Very Large Desktop
        selectedRatio = NaN
        allowedAspectRatios {
            # [...] array of defined aspect ratios (identical!)
        }
    }

    xl {
        title = Large Desktop
        selectedRatio = NaN
        allowedAspectRatios {
            # [...] array of defined aspect ratios (identical!)
        }
    }

    # [...]
}

# Override for news extension
TCEFORM.tx_news_domain_model_news.fal_media.config.overrideChildTca.columns.crop.config.cropVariants {
    listview {
        title = List view
        selectedRatio = default
        excludeFromSync = 1
        allowedAspectRatios {
            # [...] array of a custom aspect ratio definition
            # (or identical aspect ratios, but not taken into account
            # for synchronized cropping)
        }
    }
}
Copied!

Impact 

The editor is now able to apply changes to the image aspect ratio and the image cutting to several matching crop variants at once inside the image manipulation cropping GUI. Exemptions for specific cropVariants can be set.

Feature: #105827 - Search in backend page tree and live search can find pages by their frontend URI 

See forge#105827 and forge#105833

Description 

The backend page tree search functionality has been enhanced to allow entering a full URI like https://mysite.example.com/de/any/subtree/page/, which will show the matching page in the result tree.

Multiple URIs can be separated by the comma separator (,), just like multiple page IDs can be entered.

Combining a search input like this is possible:

Combining multiple search parts
4,8,https://example.com/first,http://sub.example.com/en/second,anyPageTitle
Copied!

Matches in frontend URIs of translated pages will be marked as such.

This functionality uses the PSR-14 event BeforePageTreeIsFilteredEvent (see Feature: #105833 - Extended page tree filter functionality) for this specific enhancement and can be used as inspiration for custom search variations.

Additionally, the Live Search is also enhanced to perform the same search by a single URI to lookup its page. This is achieved with the new PSR-14 event ModifyConstraintsForLiveSearchEvent (see Feature: #105827 - New PSR-14 ModifyConstraintsForLiveSearchEvent).

The live search will present both the default language page derived from the URI, as well as the actual translated page as a result.

Configuration 

Search by frontend URI is enabled by default and can be controlled in two ways, similar to search by translation:

User TSconfig 

Administrators can control the availability of translation search via User TSconfig:

# Disable searching by frontend URI for specific users/groups
options.pageTree.searchByFrontendUri = 0
Copied!

User Preference 

Individual backend users can toggle this setting using the page tree toolbar menu. The preference is stored in the backend user's configuration, allowing each user to customize their search behavior.

Impact 

Editors can now easily locate a backend page when only having the frontend URI available. Permissions to edit/see the page are evaluated. Invalid or non-matching URIs are ignored.

Feature: #105827 - New PSR-14 ModifyConstraintsForLiveSearchEvent 

See forge#105827, forge#105833, forge#93494

Description 

A new PSR-14 event \TYPO3\CMS\Backend\Search\Event\ModifyConstraintsForLiveSearchEvent has been added to TYPO3 Core. This event is fired in the \TYPO3\CMS\Backend\Search\LiveSearch\LiveSearch class and allows extensions to modify the CompositeExpression constraints gathered in an array, before execution. This allows to add or remove additional constraints to the main query constraints that are combined in an logical OR conjunction, and could not be accessed with the existing event \TYPO3\CMS\Backend\Search\Event\ModifyQueryForLiveSearchEvent .

The event features the following methods:

  • getConstraints(): Returns the current array of query constraints (composite expression).
  • addConstraint(): Adds a single constraint.
  • addConstraints(): Adds multiple new constraints in one go.
  • getTableName(): Returns the table, for which the query will be executed (e.g. "pages" or "tt_content").
  • getSearchDemand(): Returns the search demand that is getting searched for

Example 

The corresponding event listener class:

<?php

namespace Vendor\MyPackage\Backend\EventListener;

use TYPO3\CMS\Backend\Search\Event\ModifyConstraintsForLiveSearchEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Database\ConnectionPool;

final readonly class PageRecordProviderEnhancedSearch
{
    public function __construct(private ConnectionPool $connectionPool) {}

    #[AsEventListener('my-package/livesearch-enhanced')]
    public function __invoke(ModifyConstraintsForLiveSearchEvent $event): void
    {
        if ($event->getTableName() !== 'pages') {
            return;
        }

        $queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
        // Add a constraint so that pages marked with "show_in_all_results=1"
        // will always be shown.
        $constraints[] = $queryBuilder->expr()->eq(
            'show_in_all_results',
            1,
        );

        $event->addConstraints(...$constraints);
    }
}
Copied!

The Core itself makes uses of this event to allow searching for frontend URIs inside the backend tree.

Impact 

It is now possible to use a new PSR-14 event for adding constraints to the Live Search query, which are OR-combined.

Feature: #106153 - Improve DebugExceptionHandler with copy functionality 

See forge#106153

Description 

The debugging exception error handler (which can be set for backend and frontend error reporting) provides a large stack trace with details of an error.

This is often vital when reporting bugs in TYPO3 or debugging custom code.

The output has now been improved:

  • Each stack trace segment's filename and line where the error occurred now has a "copy path" button. Clicking on it will copy the whole path, filename and line number to the browser's clipboard.
  • The bottom of the page shows two buttons: One to toggle the output above to hide or reveals the file's contents. And another to copy the whole stack trace in plain text format, so that it can be forwarded in error reports.
  • A brief section explains what a 'stack trace' is, and a jump functionality to get from the top to the export section is available.

Thanks to Olivier Dobberkau, whose extension https://github.com/dkd-dobberkau/enhanced-error-handler inspired rework of this feature.

Impact 

Errors and their stack traces can now be much more easily copied and forwarded for support questions, without the need to dump a HTML file or even to make screenshots.

File names and the lines of an occurred error can easily be copied to be inserted into the IDE to jump directly to the related code.

Feature: #106261 - Align command line arguments of Message consumer with Symfony original 

See forge#106261

Description 

This change aligns the command line arguments of the TYPO3 Console messenger:consume command with the original Symfony Messenger implementation.

The following new options have been added:

  • --limit / -l: Limit the number of received messages
  • --failure-limit / -f: The number of failed messages the worker can consume
  • --memory-limit / -m: The memory limit the worker can consume
  • --time-limit / -t: The time limit in seconds the worker can handle new messages
  • --bus / -b: Name of the bus to which received messages should be dispatched
  • --all: Consume messages from all receivers
  • --keepalive: Whether to use the transport's keepalive mechanism if implemented

Scheduler Integration 

The command can be configured as a scheduler task in TYPO3, enabling automated consumption of messages from the configured transports. This is particularly useful for processing asynchronous messages in the background.

This integration helps projects adopt asynchronous message handling by providing a reliable way to process messages without manual intervention. Messages can be dispatched asynchronously during regular request handling and consumed in the background by the scheduler task, improving application performance and user experience.

Usage 

Consume messages from a specific receiver:

php vendor/bin/typo3 messenger:consume my_receiver
Copied!

Consume messages with a message limit:

php vendor/bin/typo3 messenger:consume my_receiver --limit=10
Copied!

Stop the worker after 2 failed messages:

php vendor/bin/typo3 messenger:consume my_receiver --failure-limit=2
Copied!

Stop the worker when memory limit is exceeded:

php vendor/bin/typo3 messenger:consume my_receiver --memory-limit=128M
Copied!

Stop the worker after a time limit:

php vendor/bin/typo3 messenger:consume my_receiver --time-limit=3600
Copied!

Consume from specific queues only:

php vendor/bin/typo3 messenger:consume my_receiver --queues=fasttrack
Copied!

Consume from all configured receivers:

php vendor/bin/typo3 messenger:consume --all
Copied!

Feature: #106640 - Localize enum labels in site settings definitions 

See forge#106640

Description 

Enum option labels in site settings definitions can now be localized consistently.

This applies to all common enum declaration styles:

  • List-style enum declarations derive localization keys using settings.<settingKey>.enum.<enumValue> in the set labels file.
  • Map-style enum declarations are independent of that key schema: only the configured label value is evaluated.
  • Map-style enum declarations with localization references (LLL:...) resolve these references.
  • Map-style enum declarations with literal labels keep these labels as-is.
  • Map-style key-only enum entries fall back to the enum value.
  • Map-style empty string labels remain empty strings.

Example 

List-style enum declaration in settings.definitions.yaml
settings:
  my.enumSetting:
    type: string
    default: optionA
    enum:
      - optionA
      - optionB
Copied!
Matching labels in labels.xlf
<trans-unit id="settings.my.enumSetting.enum.optionA">
  <source>Option A (localized)</source>
</trans-unit>
<trans-unit id="settings.my.enumSetting.enum.optionB">
  <source>Option B (localized)</source>
</trans-unit>
Copied!
Map-style enum declaration in settings.definitions.yaml
settings:
  my.enumSetting:
    type: string
    default: optionA
    enum:
      optionA: 'LLL:EXT:my_extension/Configuration/Sets/MySet/labels.xlf:settings.custom.optionA' # Explicit LLL reference
      optionB: 'Literal Option B' # Literal label
      optionC: # Key-only map-style entry, falls back to enum value "optionC"
      optionD: '' # Empty label stays empty
Copied!
Referenced label in labels.xlf
<trans-unit id="settings.custom.optionA">
  <source>Option A (localized)</source>
</trans-unit>
Copied!

If you want to work with automatically derived keys in the set labels.xlf (for example settings.<settingKey>.enum.<enumValue>), omit enum labels in YAML and use list-style enum declarations.

Impact 

Integrators can localize enum options consistently using the same resolution behavior as other setting labels.

Feature: #106681 - Support relative date formats in DateRange validator 

See forge#106681

Description 

The DateRange validator of the form extension now supports relative date expressions in addition to absolute dates in Y-m-d format.

This allows form integrators to define dynamic date constraints that are evaluated at runtime, such as ensuring a date of birth is at least 18 years in the past or that a selected date cannot be in the future.

The following relative expressions are supported (matching PHP's strtotime() syntax):

  • Named dates: today, now, yesterday, tomorrow
  • Relative offsets: -18 years, +1 month, -2 weeks, +30 days

These expressions can be used in the options.minimum and options.maximum properties of the DateRange validator.

Example 

Ensure a date of birth is at least 18 years in the past:

type: Date
identifier: date-of-birth
label: 'Date of birth'
validators:
  -
    identifier: DateRange
    options:
      maximum: '-18 years'
Copied!

Ensure a date is in the future:

type: Date
identifier: event-date
label: 'Event date'
validators:
  -
    identifier: DateRange
    options:
      minimum: '+1 day'
Copied!

Mixed absolute and relative dates are also supported:

validators:
  -
    identifier: DateRange
    options:
      minimum: '2020-01-01'
      maximum: 'today'
Copied!

The form editor in the TYPO3 backend has been updated to accept these relative expressions in the date range fields. The HTML min and max attributes on the rendered <input type="date"> element are automatically resolved to absolute Y-m-d dates at rendering time.

Impact 

Form integrators can now use relative date expressions in the DateRange validator configuration. Existing form definitions using absolute dates continue to work without changes.

Feature: #106828 - Add User TSconfig to define default Live Search action 

See forge#106828

Description 

A new User TSconfig options.liveSearch.actions has been introduced to allow an integrator to define default behaviors of a search result.

Available actions:

Action Description
edit This opens the editing form for the record (Default for all tables except pages)
layout This opens the page in the page module (Default for table pages)
list This opens the storage page of the record in the "Records" module
preview This opens the record in the frontend

Examples 

Set default for all tables

options.liveSearch.actions.default = edit

Set default for table tt_content

options.liveSearch.actions.tt_content.default = layout

Set default for custom table

options.liveSearch.actions.my_table.default = preview

Impact 

Live Search default actions can now be configured via User TSconfig. Integrators can define global or per-table behavior for search results, improving backend workflows.

The default behavior for pages has been changed to layout to improve the user workflow.

Feature: #107003 - Add event to change record data in list view 

See forge#107003

Description 

A new PSR-14 event \TYPO3\CMS\Backend\RecordList\Event\AfterRecordListRowPreparedEvent has been added. This event is fired in the \TYPO3\CMS\Backend\RecordList\DatabaseRecordList and allows extensions to change the data for rendering a single record in the list view.

The event allows to adjust the following properties:

  • data: The fields of the row as array. These are the available fields:

    • _SELECTOR_: The checkbox element
    • icon: The icon
    • __label: Special field that contains the header
    • _CONTROL_: The action buttons of the row
    • _LOCALIZATION_: The current language
    • _LOCALIZATION_b: The translated language
    • rowDescription: The row description
    • header: The header (This field is only used if __label is not set. Use __label instead)
    • uid: The record uid (readonly)
  • tagAttributes: The html tag attributes of the row. These attributes are available:

    • class
    • data-table
    • title

The corresponding event listener class:

use TYPO3\CMS\Backend\RecordList\Event\AfterRecordListRowPreparedEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener('my-package/backend/my-listener-name')]
class MyEventListener
{
    public function __invoke(AfterRecordListRowPreparedEvent $event): void
    {
        $data = $event->getData();
        $tagAttributes = $event->getTagAttributes();
        // do magic here
        $event->setData($data);
        $event->setTagAttributes($tagAttributes);
    }
}
Copied!

Impact 

The new PSR-14 event can be used to for example modify the title link in the record list.

Feature: #107058 - Simplify registration of a Custom Form Element 

See forge#107058

Description 

The registration of custom form elements in the TYPO3 Form Framework has been significantly simplified. Previously, registering a custom form element required subscribing to the JavaScript event view/stage/abstract/render/template/perform to render the element in the Form Editor's stage area.

With this improvement, custom form elements can now be registered without any custom JavaScript code. The Form Editor automatically uses a generic Web Component to render form elements in the stage area.

To use this simplified registration method, simply omit the formEditorPartials configuration in your form element's YAML definition. The Form Editor will then automatically render the element using the built-in <typo3-form-form-element-stage-item> Web Component, which provides:

  • Element type and identifier display
  • Element label with required indicator
  • Validators visualization
  • Support for select options (SingleSelect, MultiSelect, RadioButton, Checkbox)
  • Support for allowed MIME types (FileUpload, ImageUpload)
  • Element toolbar
  • Hidden state visualization

The generic rendering automatically extracts and displays relevant information from your form element's configuration without requiring any custom template or JavaScript code.

Impact 

Extension developers can now register custom form elements with minimal configuration. By omitting the formEditorPartials configuration, the Form Editor will automatically render the element using a generic Web Component, eliminating the need for:

  • Custom Fluid templates in Resources/Private/Backend/Partials/FormEditor/Stage/
  • Custom JavaScript code subscribing to view/stage/abstract/render/template/perform
  • Manual element rendering logic

This significantly reduces the complexity and maintenance burden when creating custom form elements that don't require special visualization in the Form Editor.

For custom form elements that require specialized rendering or custom interactions in the stage area, the formEditorPartials configuration can still be used to provide custom Fluid templates, which will work as before.

For a complete step-by-step tutorial on creating custom form elements, see :ref:`Creating a Custom Form Element typo3/cms-form:howtos-custom-form-element>.

Feature: #107802 - Support username and password in Redis session backend 

See forge#107802

Description 

Since Redis 6.0, it is possible to authenticate against Redis using both a username and a password. Prior to this version, authentication was only possible with a password. With this patch, you can now configure the TYPO3 Redis session backend as follows:

config/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['SYS']['session']['BE']
    = [
        'backend' => \TYPO3\CMS\Core\Session\Backend\RedisSessionBackend::class,
        'options' => [
            'database' => 0,
            'hostname' => 'redis',
            'port' => 6379,
            'username' => 'redis',
            'password' => 'redis',
        ]
    ];
Copied!

Impact 

The "password" configuration option of the Redis session backend is now typed as a array|string. Setting this configuration option with an array is deprecated and will be removed in 15.0.

Feature: #107826 - Introduce Extbase action authorization attribute 

See forge#107826

Description 

A new authorization mechanism has been introduced for Extbase controller actions using PHP attributes. Extension authors can now implement declarative access control logic directly on action methods using the #[Authorize] attribute.

The #[Authorize] attribute supports multiple authorization strategies:

Built-in checks - Require frontend user login via requireLogin - Require specific frontend user groups via requireGroups

Custom authorization logic - Dedicated authorization class (recommended for complex logic) - Public controller method (for simple checks)

Multiple #[Authorize] attributes can be stacked on a single action. All authorization checks must pass for access to be granted. If any check fails, a PropagateResponseException is thrown with an HTTP 403 response, which immediately stops the Extbase dispatching process.

Examples 

Require frontend user login 

EXT:my_extension/Classes/Controller/MyController.php
namespace MyVendor\MyExtension\Controller;

use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Attribute\Authorize;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

class MyController extends ActionController
{
    #[Authorize(requireLogin: true)]
    public function listAction(): ResponseInterface
    {
        return $this->htmlResponse();
    }
}
Copied!

Require specific user groups 

The requireGroups parameter accepts an array of frontend user group identifiers. Groups can be specified either by their UID (recommended) or by their title. If multiple groups are specified, the user must be a member of at least one of the groups (OR logic).

EXT:my_extension/Classes/Controller/MyController.php
namespace MyVendor\MyExtension\Controller;

use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Attribute\Authorize;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

class MyController extends ActionController
{
    // Recommended: Use group UIDs
    #[Authorize(requireGroups: [1, 2])]
    public function adminListAction(): ResponseInterface
    {
        // Only accessible to users in groups 1 or 2
        return $this->htmlResponse();
    }

    // Alternative: Use group titles (not recommended)
    #[Authorize(requireGroups: ['administrators', 'editors'])]
    public function editorListAction(): ResponseInterface
    {
        return $this->htmlResponse();
    }

    // Mixed: UIDs and titles can be combined (not recommended)
    #[Authorize(requireGroups: [1, 'editors'])]
    public function mixedListAction(): ResponseInterface
    {
        return $this->htmlResponse();
    }
}
Copied!

Custom authorization class 

For complex authorization logic, create a dedicated authorization class. This class supports dependency injection and can be reused across controllers.

EXT:my_extension/Classes/Authorization/MyObjectAuthorization.php
namespace MyVendor\MyExtension\Authorization;

use MyVendor\MyExtension\Domain\Model\MyObject;
use TYPO3\CMS\Core\Context\Context;

class MyObjectAuthorization
{
    public function __construct(
        private readonly Context $context
    ) {}

    public function checkOwnership(MyObject $myObject): bool
    {
        $userAspect = $this->context->getAspect('frontend.user');
        return $myObject->getOwner()->getUid() === $userAspect->get('id');
    }
}
Copied!
EXT:my_extension/Classes/Controller/MyController.php
namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Authorization\MyObjectAuthorization;
use MyVendor\MyExtension\Domain\Model\MyObject;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Attribute\Authorize;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

class MyController extends ActionController
{
    #[Authorize(callback: [MyObjectAuthorization::class, 'checkOwnership'])]
    public function editAction(MyObject $myObject): ResponseInterface
    {
        $this->view->assign('myObject', $myObject);
        return $this->htmlResponse();
    }

    #[Authorize(callback: [MyObjectAuthorization::class, 'checkOwnership'])]
    public function deleteAction(MyObject $myObject): ResponseInterface
    {
        // Delete the object
        return $this->htmlResponse();
    }
}
Copied!

Public controller method 

For simple checks, a public controller method can be used as callback.

EXT:my_extension/Classes/Controller/MyController.php
namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Domain\Model\MyObject;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Extbase\Attribute\Authorize;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

class MyController extends ActionController
{
    public function __construct(
        private readonly Context $context
    ) {}

    #[Authorize(callback: 'checkOwnership')]
    public function editAction(MyObject $myObject): ResponseInterface
    {
        $this->view->assign('myObject', $myObject);
        return $this->htmlResponse();
    }

    public function checkOwnership(MyObject $myObject): bool
    {
        $userAspect = $this->context->getAspect('frontend.user');
        return $myObject->getOwner()->getUid() === $userAspect->get('id');
    }
}
Copied!

Combining multiple authorization checks 

Multiple #[Authorize] attributes can be stacked. All checks must pass.

EXT:my_extension/Classes/Controller/MyController.php
namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Authorization\MyObjectAuthorization;
use MyVendor\MyExtension\Domain\Model\MyObject;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Attribute\Authorize;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

class MyController extends ActionController
{
    #[Authorize(requireLogin: true)]
    #[Authorize(requireGroups: [1, 2])]
    #[Authorize(callback: [MyObjectAuthorization::class, 'checkOwnership'])]
    public function editAction(MyObject $myObject): ResponseInterface
    {
        // Only accessible to logged-in users in groups 1 or 2 who own the object
        return $this->htmlResponse();
    }
}
Copied!

Authorization checks can also be combined within a single attribute:

#[Authorize(requireLogin: true, requireGroups: [1, 2])]
public function adminAction(): ResponseInterface
{
    return $this->htmlResponse();
}
Copied!

Customizing the authorization denied response 

By default, the authorization check will throw a \TYPO3\CMS\Core\Http\PropagateResponseException with a HTTP 403 response. This response can be handled by the TYPO3 page error handler configured in site settings.

The PSR-14 event BeforeActionAuthorizationDeniedEvent can be used to provide a custom PSR-7 response, which will be returned by Extbase.

EXT:my_extension/Classes/EventListener/CustomAuthorizationResponseListener.php
namespace MyVendor\MyExtension\EventListener;

use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use TYPO3\CMS\Extbase\Authorization\AuthorizationFailureReason;
use TYPO3\CMS\Extbase\Event\Mvc\BeforeActionAuthorizationDeniedEvent;

final class CustomAuthorizationResponseListener
{
    public function __construct(
        private readonly ResponseFactoryInterface $responseFactory,
        private readonly StreamFactoryInterface $streamFactory
    ) {}

    public function __invoke(BeforeActionAuthorizationDeniedEvent $event): void
    {
        // Customize response based on failure reason
        $message = match ($event->getFailureReason()) {
            AuthorizationFailureReason::NOT_LOGGED_IN => 'Please log in to access this page',
            AuthorizationFailureReason::MISSING_GROUP => 'You do not have permission to access this page',
            AuthorizationFailureReason::CALLBACK_DENIED => 'Access to this resource is denied',
        };

        $response = $this->responseFactory->createResponse()
            ->withHeader('Content-Type', 'text/html; charset=utf-8')
            ->withStatus(403)
            ->withBody($this->streamFactory->createStream($message));

        $event->setResponse($response);
    }
}
Copied!

Security Considerations 

Impact 

Extension authors can now implement secure, declarative authorization checks for Extbase controller actions using the #[Authorize] attribute.

Feature: #107887 - New "Latest backend logins" widget 

See forge#107887

Description 

A new dashboard widget Latest backend logins has been introduced to display the most recent backend user logins directly in the TYPO3 Dashboard. This allows administrators to quickly monitor user activity and track recent backend access patterns without navigating through the system log.

The widget provides a configurable interface where administrators can set the number of logins to display, offering flexibility in monitoring scope.

Each login entry shows:

  • The backend user avatar and name
  • The corresponding login time

Key benefits:

  • Displays recent backend user logins with user details and timestamps
  • Provides direct access to login monitoring without navigating system logs
  • Offers configurable display limits for different monitoring needs
  • Enhances security monitoring with quick access to login patterns

Impact 

This feature improves administrative oversight by providing immediate visibility into recent backend user activity directly within the dashboard interface.

Feature: #107906 - Recently Opened Documents Widget 

See forge#107906

Description 

A new Recently Opened Documents Dashboard Widget has been introduced to display documents that are currently open or were recently accessed in the TYPO3 backend. This allows editors and administrators to quickly return to their work and access frequently edited content without navigating through the page tree or search.

The widget provides a configurable interface where users can set the number of documents to display, offering flexibility based on their workflow needs. Each document entry shows the record icon and title for easy identification and quick access.

Key benefits: Displays recently opened documents with icons and titles Provides quick access to ongoing work without searching Offers configurable display limits for different workflow needs Shows document type icons for visual identification Improves editing efficiency by reducing navigation time Displays documents in reverse chronological order (most recent first)

The widget retrieves documents from the FormEngine session data, ensuring that only currently open or recently accessed documents are displayed. Deleted records are automatically filtered out to maintain data accuracy.

Impact 

This feature improves editorial efficiency by providing immediate access to recently opened documents directly within the dashboard interface, reducing the time spent navigating through the backend to resume work.

Feature: #107940 - Introduce report about content type usage 

See forge#107940

Description 

A new Content statistics module has been introduced in the TYPO3 backend under guilabel:Reports`. This module provides data about the usage of content elements in the TYPO3 site.

Overview 

The overview displays all available content element types along with the number of times each type is used. For each element, all associated fields are listed, including key details such as:

  • The field type
  • Whether it is marked as required
  • Whether it is configurable as excludable via user group permissions

Detail view 

The detail view for each content element type lists all corresponding records that are not marked as deleted.

Impact 

The new report offers a convenient way to analyze and optimize content structures within a TYPO3 installation. It helps administrators and developers to:

  • Identify unused content element types
  • Understand which fields are utilized by specific content element types
  • Gain insights into the overall configuration and diversity of content elements

Feature: #108557 - TCA option allowedRecordTypes for Page Types 

See forge#108557

Description 

A new TCA option allowedRecordTypes is introduced for Page Types to configure allowed database tables for specific types ("doktype").

EXT:my_extension/Configuration/TCA/Overrides/pages.php
// Allow any record on that Page Type.
$GLOBALS['TCA']['pages']['types']['116']['allowedRecordTypes'] = ['*'];

// Only allow specific tables on that Page Type.
$GLOBALS['TCA']['pages']['types']['116']['allowedRecordTypes'] = ['tt_content', 'my_custom_record'];
Copied!

The array can contain a list of table names or a single entry with an asterisk * to allow all types.

Per default only the tables pages, sys_category, sys_file_reference and sys_file_collection are allowed, if not overridden with this option.

The defaults are extended, if TCA tables enable the option ctrl.security.ignorePageTypeRestriction. Again, this won't be considered if allowedRecordTypes is set. They need to be configured there as well.

Impact 

The allowed record types for pages can now be configured in TCA. This centralizes the configuration for Page Types and further slims down the need for ext_tables.php, which was utilized before.

Feature: #108580 - Improved page module content preview 

See forge#108580

Description 

The page module's content element preview functionality has been enhanced to provide editors with better visual representations of content elements directly in the backend.

Sanitized HTML rendering for content 

Content elements with HTML in the bodytext field (such as text, text & images) now display sanitized HTML in the page module preview instead of plain text.

A new PreviewSanitizerBuilder has been introduced that creates a sanitizer specifically designed for backend previews. This sanitizer:

  • Removes clickable links (unwraps <a> tags while preserving their content)
  • Removes heading tags (:html<h1> through <h6>) while preserving their content
  • Allows safe HTML formatting (bold, italic, lists, etc.)

Enhanced bullet list preview 

Content elements of type "bullet list" now render as actual HTML lists in the preview.

Harmonized menu element rendering 

The preview rendering for menu content elements and "insert records" elements has been harmonized to match the presentation used in the record selector wizard. This provides a consistent experience across different parts of the backend.

Impact 

The mentioned enhancement improve the editorial experience in the TYPO3 backend by providing clearer, more informative content previews. Editors can now:

  • See formatted HTML content as it will appear to users
  • Quickly identify bullet list structure and content

Feature: #108581 - Record type specific label configuration 

See forge#108581

Description 

Previously, the TCA label configuration ( ctrl['label'], ctrl['label_alt'], and ctrl['label_alt_force']) applied globally to all record types within a table. This meant that all content elements in tt_content, regardless of their CType, displayed the same field(s) as their label in the backend.

It is now possible to define a type-specific label configuration directly in the TCA types section. These settings override the global ctrl label configuration for the respective record type:

  • label - Primary field used for the record title
  • label_alt - Alternative field(s) used when label is empty (or as additional fields)
  • label_alt_force - Force display of alternative fields alongside the primary label

This is especially useful for tables like tt_content where different content element types may benefit from showing different fields. For example, an "Image" content element could display the image caption, while a "Text" element shows the header field.

Examples 

EXT:my_extension/Configuration/TCA/tx_my_table.php
return [
    'ctrl' => [
        'label' => 'header',
        'type' => 'record_type',
        // ... other ctrl configuration
    ],
    'types' => [
        'article' => [
            'label_alt' => 'teaser',
        ],
        'event' => [
            'label_alt' => 'event_date,location',
            'label_alt_force' => true,
        ],
    ],
    // ... columns configuration
];
Copied!

In this example:

  • All types use header as the primary label field (from ctrl['label']).
  • The article type additionally displays the teaser field if header is empty.
  • The event type displays header together with event_date and location (since label_alt_force is enabled).

When adding a new record type to an existing table, the label configuration can be provided as the 3rd argument $additionalTypeInformation of \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addRecordType.

EXT:my_extension/Configuration/TCA/Overrides/tx_my_table.php
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addRecordType(
    [
        'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType.shortcut',
        'value' => 'my-type',
        'icon' => 'my-icon',
        'group' => 'special',
    ],
    'my-header',
    [
        'label' => 'header',
        'label_alt' => 'records',
    ]
);
Copied!

Impact 

Tables with multiple record types can now define more specific and descriptive labels for each type in the backend user interface. This improves usability and clarity for editors by making it immediately obvious which type of record is being displayed.

This feature is especially useful for:

  • Content element tables such as tt_content with different CType values
  • Tables with distinct record types serving different purposes
  • Plugin records with varying functionality per type
  • Any table where the record type changes the record's purpose or meaning

All places in TYPO3 that use \TYPO3\CMS\Backend\Utility\BackendUtility::getRecordTitle() automatically benefit from this feature without any code changes. This includes record lists, page trees, history views, workspaces, and similar backend modules.

Functionality like FormEngine records cannot profit yet from this option, as FormEngine does not support Schema API yet.

Feature: #108648 - Option to modify src attribute for Vimeo/YouTube 

See forge#108648

Description 

The new configuration option srcAttribute for the YouTubeRenderer and VimeoRenderer can be used to adjust the previously hard-coded src attribute in the generated iframe HTML code. This can be helpful if iframes shouldn't be loaded immediately because of privacy concerns, in which case an alternative such as data-src can be used in the initial HTML markup.

Example:

<f:media
    file="{youtubeVideo}"
    additionalConfig="{srcAttribute: 'data-src'}"
/>
Copied!

Impact 

The src attribute for YouTube and Vimeo embeds can now be renamed.

Feature: #108720 - QR code button for frontend preview 

See forge#108720

Description 

A new QR code button has been added next to the "View" button in various backend modules. Clicking the button opens a modal displaying a scannable QR code for the frontend preview URI.

The button is available in the following locations:

  • Page module (Layout view and Language Comparison view)
  • List module
  • Preview module
  • Workspaces module

When working in a workspace, the QR code contains a special preview URI that works without backend authentication. This makes it easy to share workspace previews with colleagues or clients, or to quickly check a draft version on a mobile device by simply scanning the code.

The QR code can be downloaded as PNG or SVG directly from the modal.

Impact 

Editors benefit from a streamlined workflow when sharing page previews or testing pages on mobile devices. The workspace-aware preview URIs eliminate the need to be logged in when scanning the QR code, making it particularly useful for review processes involving external stakeholders.

Feature: #108726 - Add PSR-14 Events ModifyRenderedContentAreaEvent and ModifyRenderedRecordEvent 

See forge#108726

Description 

With the \TYPO3\CMS\Fluid\Event\ModifyRenderedContentAreaEvent , developers can intercept the rendering of content areas in Fluid templates to modify the output. This depends on content areas being rendered with the new <f:render.contentArea> ViewHelper in Fluid templates, see Introduce Fluid f:render.contentArea ViewHelper.

With the \TYPO3\CMS\Fluid\Event\ModifyRenderedRecordEvent , developers can intercept the rendering of individual records in Fluid templates to modify the output. This depends on records being rendered with the new <f:render.contentArea> or <f:render.record> ViewHelpers in Fluid templates, see Introduce Fluid f:render.record ViewHelper.

Note that any alterations will be output as-is and will not be escaped. If you process insecure content within an event listener, be sure to escape it properly, e.g. by applying htmlspecialchars() to it.

Example 

An example event listener could look like this:

EXT:my_extension/Classes/EventListener/ModifyRenderedContentEventListener.php
namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Fluid\Event\ModifyRenderedContentAreaEvent;
use TYPO3\CMS\Fluid\Event\ModifyRenderedRecordEvent;

final class ModifyRenderedContentEventListener
{
    #[AsEventListener]
    public function modifyContentArea(ModifyRenderedContentAreaEvent $event): void
    {
        $content = 'before area<hr />'. $event->getRenderedContentArea() . '<hr />after area';
        $event->setRenderedContentArea($content);
    }

    #[AsEventListener]
    public function modifyRecord(ModifyRenderedRecordEvent $event): void
    {
        $content = 'before record<hr />'. $event->getRenderedRecord() . '<hr />after record';
        $event->setRenderedRecord($content);
    }
}
Copied!

Impact 

The new events can be used by extension authors to enhance the output of content areas and records rendered in themes.

Feature: #108726 - Introduce Fluid f:render.contentArea ViewHelper 

See forge#108726

Description 

Instead of using the <f:cObject> and <f:for> ViewHelpers to render content areas, the new <f:render.contentArea> ViewHelper can be used.

It allows rendering content areas while enabling other extensions to modify the output via PSR-14 EventListeners.

This is especially useful for adding debugging wrappers or additional HTML structure around content areas.

By default, the ViewHelper renders the content area as-is, but EventListeners can listen to the \TYPO3\CMS\Fluid\Event\ModifyRenderedContentAreaEvent and modify the output.

You need to use the PAGEVIEW config like this: .. code-block:: typoscript

page = PAGE page.10 = PAGEVIEW page.10.paths.10 = EXT:my_site_package/Resources/Private/Templates/

MyPage.fluid.html
<f:render.contentArea contentArea="{content.left}"/>
or
{content.left -> f:render.contentArea()}
Copied!

Impact 

Theme creators are encouraged to use the <f:render.contentArea> ViewHelper to allow other extensions to modify the output via EventListeners.

Feature: #108726 - Introduce Fluid f:render.record ViewHelper 

See forge#108726

Description 

Instead of using the <f:cObject> ViewHelper to render database records, the new <f:render.record> ViewHelper can be used.

It allows rendering records while enabling other extensions to modify the output via PSR-14 EventListeners.

This is especially useful for adding debugging wrappers or additional HTML structure around content elements.

By default, the ViewHelper renders the record as-is, but EventListeners can listen to the \TYPO3\CMS\Fluid\Event\ModifyRenderedRecordEvent and modify the output.

Usage with the record-transformation data processor:

dataProcessing {
    10 = record-transformation
}
Copied!
MyContentElement.fluid.html
<f:render.record record="{record}"/>
or
{record -> f:render.record()}
Copied!

You can not only render tt_content records but any database record by defining the rendering in Typoscript.

# Example Typoscript configuration for rendering custom records
sys_category = FLUIDTEMPLATE
sys_category {
  file = EXT:my_extension/Resources/Private/Templates/Category.html
  layoutRootPaths.10 = EXT:my_extension/Resources/Private/Layouts/
  partialRootPaths.10 = EXT:my_extension/Resources/Private/Partials/
  dataProcessing.1421884800 = record-transformation
}

# Example Typoscript configuration for special record types
tx_myextension_domain_model_product = COA
tx_myextension_domain_model_product.default = FLUIDTEMPLATE
tx_myextension_domain_model_product.default {
  templateName >
  templateName.ifEmpty.cObject = TEXT
  templateName.ifEmpty.cObject {
    field = record_type
    required = 1
    case = uppercamelcase
  }
  # for record_type = 'mainProduct' the template file my_extension/Resources/Private/Templates/Product/MainProduct.html will be used
  layoutRootPaths.10 = EXT:my_extension/Resources/Private/Layouts/
  partialRootPaths.10 = EXT:my_extension/Resources/Private/Partials/
  templateRootPaths.10 = EXT:my_extension/Resources/Private/Templates/Product/
  dataProcessing.1421884800 = record-transformation
}
Copied!

Impact 

Theme creators are encouraged to use the <f:render.record> ViewHelper to allow other extensions to modify the output via EventListeners.

Feature: #108763 - Console command to analyse Fluid templates 

See forge#108763

Description 

The fluid:analyse console command is introduced, which analyses Fluid templates in the current project for correct Fluid syntax and alerts about deprecations that are emitted during template parsing.

Usage:

vendor/bin/typo3 fluid:analyse
Copied!

Example output:

[DEPRECATION] packages/myext/Resources/Private/Templates/Test.fluid.html: <my:obsolete> has been deprecated in X and will be removed in Y.
[ERROR] packages/myext/Resources/Private/Templates/Test2.fluid.html: Variable identifiers cannot start with a "_": _temp
Copied!

In its initial implementation, the command automatically finds all Fluid templates within the current project based on the *.fluid.* file extension (See Feature: #108166 - Fluid file extension and template resolving) and analyses them. By default, TYPO3's system extensions are skipped, this can be adjusted by specifying the —-include-system-extensions CLI option.

The following errors and deprecations are currently supported:

If exceptions are caught during the parsing process of at least one template, the console command has a return status of 1 (error), otherwise it returns 0 (success). This means that deprecations are not interpreted as errors.

This should make it possible to use the command in CI workflows of most projects, since deprecated functionality used by third-party templates won't make the pipeline fail.

Verbose output allows to get feedback of analyzed templates and the number of errors/deprecations (or success).

Deprecating ViewHelpers 

The fluid:analyse console command can catch deprecations of whole ViewHelpers if the deprecation is emitted during parse time of a template. This is possible by implementing the \ViewHelperNodeInitializedEventInterface :

ObsoleteViewHelper.php
use TYPO3Fluid\Fluid\Core\Parser\ParsingState;
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ViewHelperNode;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperNodeInitializedEventInterface;

/**
 * @deprecated since X, will be removed in Y.
 */
final class ObsoleteViewHelper extends AbstractViewHelper implements ViewHelperNodeInitializedEventInterface
{
    // ...

    public static function nodeInitializedEvent(ViewHelperNode $node, array $arguments, ParsingState $parsingState): void
    {
        trigger_error(
            '<my:obsolete> has been deprecated in X and will be removed in Y.',
            E_USER_DEPRECATED,
        );
    }
}
Copied!

Deprecating ViewHelper arguments 

The \ViewHelperNodeInitializedEventInterface can also be used to deprecate a ViewHelper's argument. The deprecation will only be triggered if the argument is actually used in a template.

SomeViewHelper.php
use TYPO3Fluid\Fluid\Core\Parser\ParsingState;
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ViewHelperNode;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperNodeInitializedEventInterface;

final class SomeViewHelper extends AbstractViewHelper implements ViewHelperNodeInitializedEventInterface
{
    public function initializeArguments(): void
    {
        // @deprecated since X, will be removed in Y.
        $this->registerArgument('obsoleteArgument', 'string', 'Original description. Deprecated since X, will be removed in Y');
    }

    public static function nodeInitializedEvent(ViewHelperNode $node, array $arguments, ParsingState $parsingState): void
    {
        if (array_key_exists('obsoleteArgument', $arguments)) {
            trigger_error(
                'ViewHelper argument "obsoleteArgument" in <my:some> is deprecated since X and will be removed in Y.',
                E_USER_DEPRECATED,
            );
        }
    }
}
Copied!

Impact 

The new fluid:analyse console command can be used to check basic validity of Fluid templates in a project and can discover deprecated functionality used in template files.

Feature: #108776 - Allow to set user interface language when using CLI to create user 

See forge#108776

Description 

The CLI command backend:user:create now supports the option --language (or -l) to set the desired language for the user interface.

./bin/typo3 backend:user:create --language=de
Copied!

User creation using environment variables:

TYPO3_BE_USER_NAME=username \
TYPO3_BE_USER_EMAIL=admin@example.com \
TYPO3_BE_USER_GROUPS=<comma-separated-list-of-group-ids> \
TYPO3_BE_USER_LANGUAGE=de \
TYPO3_BE_USER_ADMIN=0 \
TYPO3_BE_USER_MAINTAINER=0 \
./bin/typo3 backend:user:create --no-interaction
Copied!

Feature: #108796 - Centralize Bookmark Management 

See forge#108796

Description 

The TYPO3 backend bookmark system has been comprehensively overhauled, introducing a centralized architecture that replaces the legacy "shortcut" implementation.

Bookmark Groups 

Bookmarks can be organized into three types of groups.

System groups are defined via UserTSconfig using options.bookmarkGroups and are available to all users. Global groups contain bookmarks visible to all backend users, though only administrators can add bookmarks to these groups. User groups are custom groups created by individual users for personal organization, stored in a new database table sys_be_shortcuts_group.

Five default bookmark groups are provided out of the box: Pages, Records, Files, Tools, and Miscellaneous. Previously these groups were hardcoded in PHP, but they are now defined via UserTSconfig in EXT:backend, making them fully customizable. The functionality remains the same, but administrators now have complete control over which groups are available.

The options.bookmarkGroups setting should only be modified on a global scope and not on a per-user basis, as inconsistent group configurations between users can lead to unexpected behavior:

EXT:my_ext/Configuration/user.tsconfig
# Remove a specific default group (e.g., Files)
options.bookmarkGroups.3 >

# Remove all default groups
options.bookmarkGroups >

# Add a custom group with a static label
options.bookmarkGroups.10 = My Custom Group

# Add a custom group with a translatable label using domain syntax
options.bookmarkGroups.11 = my_extension.messages:bookmark_group.custom

# Disable bookmarks entirely
options.enableBookmarks = 0
Copied!

Group labels support the TYPO3 translation domain syntax, allowing extensions to provide translated group names. The format is extension_key.messages:translation_key, which resolves to the default language file at EXT:extension_key/Resources/Private/Language/locallang.xlf.

As before, group ID -100 has special behavior as a superglobal group. Bookmarks assigned to this group are visible to all backend users, but only administrators can add or modify bookmarks in this group. This allows administrators to provide a shared set of bookmarks across the entire TYPO3 installation.

Bookmark Manager 

A new modal-based Bookmark Manager provides a centralized interface for managing all bookmarks. The manager supports drag and drop reordering to reorganize bookmarks within and across groups. Bulk operations allow selecting multiple bookmarks to move or delete at once. Users can create, edit, and delete custom bookmark groups through the group management interface, and rename bookmarks directly through inline editing.

The Bookmark Manager can be accessed via the bookmark icon in the toolbar dropdown menu.

Impact 

The bookmark toolbar item now opens a dropdown with quick access to recent bookmarks and a link to the full Bookmark Manager. Users can create custom bookmark groups for better organization of their saved pages, records, and modules. Administrators can configure global bookmarks visible to all users. The Bookmarks dashboard widget has been updated to support the new bookmark system with group filtering and limit options. The legacy "shortcut" terminology has been replaced with "bookmark" throughout the backend interface and codebase.

Feature: #108799 - LocalizationRepository methods for fetching record translations 

See forge#108799

Description 

TYPO3 historically has helper methods for localizations in various places. This patch centralizes localization-related functionality by marking \TYPO3\CMS\Backend\Domain\Repository\Localization\LocalizationRepository as public (non-internal) and adding new methods as modern, DI-friendly alternatives to the static BackendUtility methods.

getRecordTranslation() 

Fetches a single translated version of a record for a specific language.

public function getRecordTranslation(
    string|TcaSchema $tableOrSchema,
    int|array|RecordInterface $recordOrUid,
    int|LanguageAspect $language,
    int $workspaceId = 0,
    bool $includeDeletedRecords = false,
): ?RawRecord
Copied!

getRecordTranslations() 

Fetches all translations of a record. This method can also be used to count translations by using count() on the result, replacing the need for BackendUtility::translationCount().

public function getRecordTranslations(
    string|TcaSchema $tableOrSchema,
    int|array|RecordInterface $recordOrUid,
    array $limitToLanguageIds = [],
    int $workspaceId = 0,
    bool $includeDeletedRecords = false,
): array
Copied!

Returns an array of translated RawRecord objects indexed by language ID.

getPageTranslations() 

Fetches all page translations for a given page.

public function getPageTranslations(
    int $pageUid,
    array $limitToLanguageIds = [],
    int $workspaceId = 0,
    bool $includeDeletedRecords = false,
): array
Copied!

Returns an array of page translation records as RawRecord objects indexed by language ID.

Impact 

Extension developers working with record translations in the TYPO3 Backend now have access to modern, injectable repository methods that follow current TYPO3 coding practices.

The legacy static methods BackendUtility::getRecordLocalization(), BackendUtility::getExistingPageTranslations(), and BackendUtility::translationCount() remain available for backward compatibility until migrated completely.

Feature: #108817 - Introduce web component-based form editor tree 

See forge#108817

Description 

The Form Editor's tree component has been completely modernized, migrating from the legacy jQuery-based implementation to a modern Web Components architecture using Lit and the TYPO3 backend tree infrastructure.

Enhanced User Experience 

The new tree component provides a significantly improved user experience with modern interaction patterns and visual feedback:

Intuitive Drag & Drop
Form elements can now be reorganized using a smooth drag and drop interface with intelligent validation rules. The tree automatically prevents invalid operations, such as dragging the root form element, moving pages outside their designated level, or dropping elements into non-composite types.
Smart Element Organization
Only composite elements like Grid Containers and Fieldsets can receive child elements, while simple form fields remain non-droppable. Pages must always stay at the top level, ensuring proper form structure. The tree automatically distinguishes between reordering siblings and changing parent elements, providing precise control over form organization.
Visual Feedback
Clear visual indicators show valid drop zones during drag operations. Selected elements are highlighted with proper styling. The tree provides immediate feedback for all interactions, making form building more intuitive.
Persistent Navigation
The tree automatically remembers expanded and collapsed states. After drag and drop operations, the tree maintains your current view and selection, preventing disorienting resets. Navigation feels natural and responsive.
Integrated Search
A built-in search toolbar allows quick filtering of form elements by name. The search works client-side for instant results, making it easy to locate specific elements in complex forms.
Collapse All Functionality
The toolbar includes a convenient button to collapse all expanded nodes at once, helping to get a quick overview of your form structure or reset the view to a clean state.

Technical Implementation 

The new implementation leverages the proven TYPO3 backend tree infrastructure.

Impact 

Form editors will immediately notice the improved responsiveness and modern feel of the tree component. Drag and drop operations are smoother and more predictable. The search functionality makes working with large forms significantly easier. The tree maintains its state during operations, reducing friction and improving workflow efficiency.

The new Web Component-based architecture ensures better maintainability and extensibility for future enhancements. The component integrates seamlessly with the existing Form Editor without requiring changes to form definitions or configurations.

Feature: #108819 - RecordFieldPreviewProcessor for custom PreviewRenderers 

See forge#108819

Description 

A new service \TYPO3\CMS\Backend\Preview\RecordFieldPreviewProcessor has been introduced to provide common field rendering helpers for custom content element preview renderers.

Previously, these helper methods were only available in StandardContentPreviewRenderer, which required custom preview renderers to extend that class to access them.

Instead, this service uses the pattern of Composition over Inheritance.

The new service provides the following methods:

prepareFieldWithLabel() 

Renders a field value with its TCA label prepended in bold.

public function prepareFieldWithLabel(RecordInterface $record, string $fieldName): ?string
Copied!

prepareField() 

Renders a processed field value without a label.

public function prepareField(RecordInterface $record, string $fieldName): ?string
Copied!

prepareText() 

Processes larger text fields (e.g., RTE content) with truncation and HTML stripping.

public function prepareText(RecordInterface $record, string $fieldName, int $maxLength = 1500): ?string
Copied!

preparePlainHtml() 

Renders plain HTML content with line limiting.

public function preparePlainHtml(RecordInterface $record, string $fieldName, int $maxLines = 100): ?string
Copied!

prepareFiles() 

Renders thumbnails for file references.

public function prepareFiles(iterable|FileReference $fileReferences): ?string
Copied!

linkToEditForm() 

Wraps content in an edit link if the user has appropriate permissions.

public function linkToEditForm(string $linkText, RecordInterface $record, ServerRequestInterface $request): string
Copied!

Impact 

Extension developers implementing custom preview renderers can now inject RecordFieldPreviewProcessor to access common field rendering helpers without extending StandardContentPreviewRenderer.

Example usage:

use TYPO3\CMS\Backend\Preview\RecordFieldPreviewProcessor;
use TYPO3\CMS\Backend\Preview\PreviewRendererInterface;

final class MyCustomPreviewRenderer implements PreviewRendererInterface
{
    public function __construct(
        private readonly RecordFieldPreviewProcessor $fieldProcessor,
    ) {}

    public function renderPageModulePreviewContent(GridColumnItem $item): string
    {
        $record = $item->getRecord();
        $content = $this->fieldProcessor->prepareFieldWithLabel($record, 'header');
        $content .= $this->fieldProcessor->prepareFiles($record->get('image'));
        return $content;
    }
}
Copied!

Feature: #108826 - Add Short URL module 

See forge#108826

Description 

A new backend module Link Management > Short URLs has been introduced. It enables editors to create and manage short URLs that redirect visitors to a configurable target. Short URLs are stored as sys_redirect records with the dedicated record type short_url, providing a streamlined editing form that hides redirect-specific fields irrelevant to short URL use cases.

Creating Short URLs 

Short URLs can be created in two ways:

  • Manual entry: Editors type a custom path (e.g. /promo) into the source path field.
  • Auto-generation: Clicking the Generate Short URL button produces a random 8-character path (e.g. /aBcDeFgH). The generated path is guaranteed to be unique through server-side collision checking.

Uniqueness enforcement 

Short URL paths must be unique per source host. Duplicate detection happens at two levels:

  • Client-side validation: While editing, the source path and source host fields are validated against existing records. If a conflict is detected, both fields are highlighted with an error state together with a notification.
  • Server-side enforcement: On save, DataHandler rejects duplicate short URLs and displays a flash message, ensuring data integrity even if client-side validation is bypassed.

Immutability 

Once a short URL record has been saved, the source path and source host fields become read-only. This ensures that published short URLs remain stable and previously shared links continue to work. The redirect target can still be changed at any time.

Clipboard support 

The full short URL (including protocol and host) can be copied to the clipboard from both the list overview and the record edit view.

Impact 

Editors benefit from a dedicated interface for managing short URLs without needing to understand redirect configuration details. The module provides a central location for creating, reviewing, and maintaining short URLs with built-in safeguards against duplicates and accidental modifications.

Feature: #108832 - Introduce UserSettings object for backend user profile settings 

See forge#108832

Description 

A new UserSettings object provides structured access to backend user profile settings defined via $GLOBALS['TYPO3_USER_SETTINGS'].

UserSettings Object 

The UserSettings object can be retrieved via the backend user:

$userSettings = $GLOBALS['BE_USER']->getUserSettings();

// Check if a setting exists
if ($userSettings->has('colorScheme')) {
    $scheme = $userSettings->get('colorScheme');
}

// Get all settings as array
$allSettings = $userSettings->toArray();

// Typed access via dedicated methods
$emailOnLogin = $userSettings->isEmailMeAtLoginEnabled();
$showUploadFields = $userSettings->isUploadFieldsInTopOfEBEnabled();
Copied!

The class implements \Psr\Container\ContainerInterface with has() and get() methods. The get() method throws UserSettingsNotFoundException if the setting does not exist.

New JSON Storage with Backward Compatibility 

Profile settings are now stored in a new be_users.user_settings JSON field, providing a structured and queryable format. For backward compatibility, the existing serialized uc blob continues to be written alongside:

// Writing still uses the uc mechanism
$GLOBALS['BE_USER']->uc['colorScheme'] = 'dark';
$GLOBALS['BE_USER']->writeUC();
// Both uc (serialized) and user_settings (JSON) are updated
Copied!

An upgrade wizard "Migrate user profile settings to JSON format" migrates existing settings from the uc blob to the new user_settings field.

Impact 

Backend user profile settings can now be accessed via the UserSettings object, providing type safety and IDE support. The new JSON storage format improves data accessibility while maintaining full backward compatibility through dual-write to both storage formats.

Feature: #108842 - Add Badge for Slide Mode in Layout Module 

See forge#108842

Description 

This feature introduces a visual badge in the Layout Module to indicate when the Slide Mode is active. The badge serves as a clear indicator for editors, enhancing the user experience by providing immediate feedback on the current mode of operation.

For each slide mode there is a corresponding badge, with corresponding description.

For slideMode None, no badge is shown. For slideMode Slide, a badge with the text "Slide" is shown, only if there are currently no content elements in the current page. For slideMode Collect, a badge with the text "Collect" is shown. For slideMode CollectReverse, a badge with the text "CollectReverse" is shown.

Feature: #108843 - User settings configuration migrated to TCA 

See forge#108843 See forge#108832

Description 

The backend user profile settings configuration, previously stored in $GLOBALS['TYPO3_USER_SETTINGS'], is now available in TCA at $GLOBALS['TCA']['be_users']['columns']['user_settings'] .

This allows the user settings to benefit from TCA-based tooling and provides a consistent API that extensions already use for other configurations.

A new method ExtensionManagementUtility::addUserSetting() has been introduced to simplify adding custom fields to the user profile settings.

Impact 

Extensions can add custom fields to the backend user profile settings using the new addUserSetting() method in Configuration/TCA/Overrides/be_users.php:

// Configuration/TCA/Overrides/be_users.php
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addUserSetting(
    'myCustomSetting',
    [
        'label' => 'LLL:EXT:my_ext/Resources/Private/Language/locallang.xlf:myCustomSetting',
        'config' => [
            'type' => 'check',
            'renderType' => 'checkboxToggle',
        ],
    ],
    'after:emailMeAtLogin'
);
Copied!

Alternatively, extensions can directly modify the TCA:

// Configuration/TCA/Overrides/be_users.php
$GLOBALS['TCA']['be_users']['columns']['user_settings']['columns']['myCustomSetting'] = [
    'label' => 'LLL:EXT:my_ext/Resources/Private/Language/locallang.xlf:myCustomSetting',
    'config' => [
        'type' => 'check',
        'renderType' => 'checkboxToggle',
    ],
];

// Add to showitem
$GLOBALS['TCA']['be_users']['columns']['user_settings']['showitem'] .= ',myCustomSetting';
Copied!

Structure 

The user_settings TCA column has the following structure:

columns

Array of field configurations, each containing:

label
The field label (LLL reference or string)
config
Standard TCA config array (type, renderType, items, etc.)
table (optional)
Set to 'be_users' if the field is stored in a be_users table column
showitem
Comma-separated list of fields to display, supports --div--; for tabs

Available field types 

  • input - Text input field
  • number - Number input field
  • email - Email input field
  • password - Password input field
  • check with renderType => 'checkboxToggle' - Checkbox/toggle
  • select with renderType => 'selectSingle' - Select dropdown
  • language - Language selector

Backward compatibility 

For backward compatibility, the legacy $GLOBALS['TYPO3_USER_SETTINGS'] array is still supported. Third-party additions are automatically migrated to TCA after all ext_tables.php files have been loaded. However, this approach is deprecated and extensions should migrate to the new TCA-based API.

Feature: #108846 - Console command to inspect global ViewHelper namespaces 

See forge#108846

Description 

The new console command fluid:namespaces has been introduced which lists all available global ViewHelper namespaces in the current project. This can be used to verify the current configuration. With the --json option, it is also possible to access that information in a machine-readable way.

Usage:

vendor/bin/typo3 fluid:namespaces
Copied!

Example output:

+--------+------------------------------+
| Alias  | Namespace(s)                 |
+--------+------------------------------+
| core   | TYPO3\CMS\Core\ViewHelpers   |
+--------+------------------------------+
| formvh | TYPO3\CMS\Form\ViewHelpers   |
+--------+------------------------------+
| f      | TYPO3Fluid\Fluid\ViewHelpers |
|        | TYPO3\CMS\Fluid\ViewHelpers  |
+--------+------------------------------+
Copied!

The same information is also available in the Configuration module in the TYPO3 backend.

Impact 

The new console command allows developers and integrators to inspect registered global ViewHelper namespaces in the current project.

Feature: #108868 - Introduce Fluid f:render.text ViewHelper 

See forge#108868

Description 

A new <f:render.text> ViewHelper has been added. It provides a consistent approach for outputting field values in templates where the field is part of a record.

The ViewHelper follows the same conventions as other rendering-related ViewHelpers and can be used wherever a text-based database field should be displayed in the frontend.

The ViewHelper is record-aware: it receives the full record and the field name, and renders the field according to the field's TCA configuration. This includes handling of both plain text and rich text fields.

The input can be a \TYPO3\CMS\Core\Domain\RecordInterface , \TYPO3\CMS\Frontend\Page\PageInformation , or \TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface .

This allows to input a Record, a ContentBlockData object, a PageInformation object, or an Extbase Model. PageInformation and Extbase Models are internally converted to a RecordInterface.

Usage 

Usage with the record-transformation data processor:

dataProcessing {
    10 = record-transformation
}
Copied!

Based on the field's TCA configuration from the provided record, the ViewHelper chooses the appropriate processing of the field (plain text, multiline text or rich text) without further configuration in the template.

MyContentElement.fluid.html
<f:render.text record="{record}" field="title" />
or
<f:render.text field="title">{record}</f:render.text>
or
{f:render.text(record: record, field: 'title')}
or
{record -> f:render.text(field: 'title')}
Copied!

Usage with an Extbase model (property name differs from database field name):

The field argument always refers to the database/TCA column name of the underlying record, even if your Extbase model maps that column to a differently named property.

Note that Extbase models need to contain all columns that should be rendered and the record type column (if configured in TCA) for this to work correctly. For example, an Extbase model that represents tt_content must map both bodytext and ctype to be able to use <f:render.text record="{contentModel}" field="bodytext" />.

Blog/Templates/Post/Show.html
<f:render.text record="{post}" field="short_description" />

<!-- Example: Post->shortDescription maps to DB field "short_description"; use field="short_description" here. -->
Copied!

Previously, you needed to choose different processing for plain text and rich text fields; you can now use the same ViewHelper for all types of fields.

For reference, similar results could previously be achieved using:

MyContentElement.fluid.html
{record.title}
Copied!

or multiline text:

MyContentElement.fluid.html
<f:format.nl2br>{record.description}</f:format.nl2br>
or
{record.description -> f:format.nl2br()}
Copied!

or, for rich text:

MyContentElement.fluid.html
<f:format.html>{record.bodytext}</f:format.html>
or
{record.bodytext -> f:format.html()}
Copied!

Migration 

Extensions that previously accessed field values directly via {record.title} can continue to do so. However, using <f:render.text> is recommended because it renders the field in the context of the record and applies processing based on the field configuration.

When migrating from formatting ViewHelpers like <f:format.nl2br> or <f:format.html> to <f:render.text>, the main difference is that the new ViewHelper is aware of the record it belongs to and renders the field based on the record's TCA schema.

Impact 

Theme creators are encouraged to use the <f:render.text> ViewHelper for rendering text-based fields (plain and rich text), as it provides a standardized, record-aware approach that can be built upon in future versions.

Since the ViewHelper takes both the record and the field name as arguments, the rendering process has access to the complete record context. This makes the ViewHelper more flexible compared to directly accessing the field value.

Feature: #108904 - Add generic error action for custom HTTP status codes in ErrorController 

See forge#108904

Description 

The \TYPO3\CMS\Frontend\Controller\ErrorController has been enhanced with a new method customErrorAction(), which allows flexible error handling for custom HTTP status codes.

The new method can be used with TYPO3 site page error handling, allowing site administrators to configure dedicated error handling (e.g., rendering a Fluid template) for a given status code.

Example for usage in an Extbase action:

$response = GeneralUtility::makeInstance(ErrorController::class)->customErrorAction(
    $this->request,
    429,
    'Rate limit exceeded.',
    'You have exceeded the rate limit.'
);
throw new PropagateResponseException($response, 1771065101);
Copied!

Impact 

It is now possible to trigger custom error pages with specific HTTP status codes and messages from within TYPO3 or extensions, while still respecting the site's configured error handling.

Feature: #108941 - Provide language labels as virtual JavaScript modules 

See forge#108941

Description 

JavaScript modules can now import language labels as code. The labels are exposed as a object that offers a get() method and allows to substitute placeholders according to the ICU message format.

// Import labels from language domain "core.bookmarks"
import { html } from 'lit';
import labels from '~labels/core.bookmarks';

// Use label
html`<p>{labels.get('groupType.global')}</p>`

// Retrieve label and use ICU Message Format placeholders
// Example label: <source>File "{filename}" deleted</source>
html`<p>{labels.get('file.deleted', { filename: 'my-file.txt' })}</p>`

// Render a label containing pseudo xml-tags
// Example label: "File <bold>{filename}</bold> deleted"
html`<p>{labels.get('file.deleted', {
    filename: 'my-file.txt'
    // Callback function that renders the contents of `<bold>`
    bold: chunks => html`<strong>${chunks}</strong>`,
})}</p>`
Copied!

This avoids the need for controllers to inject arbitrary labels into global TYPO3.lang configuration, which impeded writing generic web components. (Often hindered simple adopting by a plain import)

Virtual JavaScript modules (schema ~label/{language.domain}) are created that resolve the labels for the specified language domain, that is provided after the prefix ~label/. Technically this mapping is implemented using an importmap path prefix, which instructs to the JavaScript engine to append the specified suffix to the mapped prefix.

The labels are allowed to be cached client side with a far future cache timeout, similar to static resources. We therefore generate version and locale-specific URLs, to ensure labels can be cached by the user agent, without requiring explicit cache invalidation.

Impact 

Extension developers can now use labels in JavaScript components, without requiring to preload labels globally or per module, reducing the risk for missing labels and simplifying developer workflows.

Several hacks like pushing labels to the top frame, loading labels globally or adding labels to component attributes have been used previously and will be replaced by this infrastructure.

Feature: #108966 - Rich text editor support in TYPO3 form editor 

See forge#108966

Description 

The TYPO3 form editor now supports rich text editing for textarea fields using CKEditor 5. Form elements can be configured to use any available RTE preset, providing a consistent editing experience across the entire TYPO3 backend.

The implementation includes a new RichTextConfigurationService that resolves CKEditor configuration from global TYPO3 RTE presets and prepares it for use in the form editor context. External plugins like the TYPO3 link browser are automatically configured.

Impact 

Form integrators can now enable rich text editing for any textarea field in the form editor by configuring it in the form YAML configuration.

The following form elements and finishers now support rich text editing out of the box:

  • StaticText element - Formatted text in forms
  • Checkbox element - Labels with links for privacy policies, etc.
  • Confirmation finisher - Formatted confirmation messages

All textarea fields in custom form elements can be configured to use the RTE.

Basic Configuration 

Enable rich text editing for a form element:

EXT:my_extension/Configuration/Form/MyFormSetup.yaml
prototypes:
  standard:
    formElementsDefinition:
      StaticText:
        formEditor:
          editors:
            300:
              identifier: staticText
              templateName: Inspector-TextareaEditor
              label: formEditor.elements.StaticText.editor.staticText.label
              propertyPath: properties.text
              enableRichtext: true
              richtextConfiguration: form-label
Copied!

The richtextConfiguration option accepts any registered RTE preset name, e.g.:

  • form-label - Simple formatting for labels (bold, italic, link) - default
  • form-content - Extended formatting for content fields (includes lists)
  • default - Standard TYPO3 RTE with all features
  • minimal - Minimal feature set

New Form RTE Presets 

Two new RTE presets specifically designed for the form extension are now available:

form-label
Essential formatting options for labels and short text fields. Includes: bold, italic, link
form-content
Extended formatting options for content fields like StaticText. Includes: bold, italic, link, bulleted lists, numbered lists

Configuration Options 

The following options are available for textarea editors in the form editor:

enableRichtext
Data type
boolean
Default
false
Description
Enable rich text editing for this textarea field.
richtextConfiguration
Data type
string
Default
'form-label'
Description
Name of the RTE preset to use. The preset must be registered in $GLOBALS['TYPO3_CONF_VARS']['RTE']['Presets'] . Common presets: form-label, form-content, default, minimal, full

Custom Sanitizer Configuration 

The form extension uses a multi-layer sanitization approach for security:

  1. Backend: Content is sanitized using the htmlSanitize.build setting from the RTE preset's processing configuration
  2. Frontend: Content is sanitized again using the default sanitizer via the f:sanitize.html() ViewHelper

To use a custom sanitizer in the backend, configure it in your RTE preset:

EXT:my_extension/Configuration/RTE/MyPreset.yaml
processing:
  HTMLparser_db:
    htmlSanitize:
      build: \MyVendor\MyExtension\Html\MySanitizerBuilder
Copied!

Frontend Customization 

The frontend templates use f:sanitize.html() with the default sanitizer for defense-in-depth security. To customize the frontend sanitization, integrators have two options:

Option 1: Override Fluid templates

Override the form element templates and specify a custom sanitizer build:

EXT:my_extension/Resources/Private/Frontend/Partials/StaticText.html
{formvh:translateElementProperty(element: element, property: 'text')
    -> f:sanitize.html(build: 'myCustomBuild')
    -> f:transform.html()}
Copied!

Option 2: Register a custom default sanitizer

Register a custom sanitizer builder as the default sanitizer globally:

EXT:my_extension/ext_localconf.php
$GLOBALS['TYPO3_CONF_VARS']['SYS']['htmlSanitizer']['default']
    = \MyVendor\MyExtension\Html\MySanitizerBuilder::class;
Copied!

Feature: #108975 - Add Configuration Provider for Extbase Class Configuration 

See forge#108975

Description 

The Extbase class configuration (persistence mapping) is now exposed in the backend Configuration module (System > Configuration). The module is available when the system extension lowlevel is installed.

The displayed configuration reflects the configured mapping that Extbase uses at runtime: it is built by collecting and merging all Configuration/Extbase/Persistence/Classes.php definitions from active packages.

Impact 

This is a read-only usability improvement: developers and integrators can inspect and verify the resolved Extbase persistence class mapping (including extension overrides) directly in the backend, without dumping configuration arrays or manually checking each Configuration/Extbase/Persistence/Classes.php file.

Feature: #108982 - Introduce rate limiting for extbase actions 

See forge#108982

Description 

Extbase now supports rate limiting for controller actions using the new PHP attribute \TYPO3\CMS\Extbase\Attribute\RateLimit . This feature allows developers to restrict the number of requests a user can make to a specific action within a given time frame.

The rate limiting is based on the client's IP address and uses Symfony's RateLimiter component with a caching framework storage.

The #[RateLimit] attribute supports the following properties:

  • limit: The maximum number of requests allowed (default: 5).
  • interval: The time window for the limit (e.g., '15 minutes', '1 hour') (default: '15 minutes').
  • policy: The rate limiting policy to use (e.g., 'sliding_window', 'fixed_window') (default: 'sliding_window').
  • message: An optional translation key for the error message shown when the limit is reached.
  • message: An optional, localizable translation key (not a hard-coded string) for the error message shown when the limit is reached like messages.rate_limit_message (the translation domain like my_extension will be automatically used, and must not be part of the key here), or LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:rate_limit_message.

When a rate limit is exceeded, Extbase returns by default a response with HTTP status code 429 (Too Many Requests).

Usage 

To apply a rate limit to an Extbase action, add the #[RateLimit] attribute to the action method:

EXT:my_extension/Classes/Controller/MyController.php
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Attribute\RateLimit;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

class MyController extends ActionController
{
    #[RateLimit(limit: 3, interval: '1 minute', message: 'message.ratelimitexceeded')]
    public function createAction(): ResponseInterface
    {
        // Business logic for creating an entity
        return $this->redirect('index');
    }
}
Copied!

PSR-14 Event: BeforeActionRateLimitResponseEvent 

The new PSR-14 event \TYPO3\CMS\Extbase\Event\Mvc\BeforeActionRateLimitResponseEvent is dispatched when a rate limit is triggered, but before the response is returned. This allows extension developers to modify the response or perform additional actions, such as logging, throwing a custom exception or enqueuing a flash message.

The following example implementation shows, how to throw a custom error (which is handled by a possible site error handler) if the rate limit is reached.

EXT:my_extension/Classes/EventListener/ModifyRateLimitResponse.php
#[AsEventListener('my_extension/modify-rate-limit-response')]
public function __invoke(BeforeActionRateLimitResponseEvent $event): void
{
    $response = GeneralUtility::makeInstance(ErrorController::class)->accessDeniedAction(
        $event->getRequest(),
        $event->getRateLimit()->message
    );
    throw new PropagateResponseException($response, 1771077885);
}
Copied!

Impact 

Developers can now protect sensitive Extbase actions (e.g., form submissions, login attempts, or heavy API endpoints) from abuse, spam, or brute-force attacks with minimal effort.

Feature: #108992 - New PSR-14 event for workspace dependency resolution 

See forge#108992

Description 

A new PSR-14 event \TYPO3\CMS\Workspaces\Event\IsReferenceConsideredForDependencyEvent has been added. It is dispatched per sys_refindex row when the workspace dependency resolver evaluates which references constitute structural dependencies during publish, stage, discard, or display operations.

Listeners decide whether a given reference should be treated as a workspace dependency. References are opt-in: the default is "not a dependency", and listeners must explicitly mark relevant references.

The event provides the following methods:

  • getTableName(): The table owning the field (refindex tablename).
  • getRecordId(): The record owning the field (refindex recuid).
  • getFieldName(): The TCA field name (refindex field).
  • getReferenceTable(): The referenced table (refindex ref_table).
  • getReferenceId(): The referenced record id (refindex ref_uid).
  • getAction(): The \TYPO3\CMS\Workspaces\Dependency\DependencyCollectionAction enum value (Publish, StageChange, Discard, or Display).
  • getWorkspaceId(): The current workspace id.
  • isDependency() / setDependency(): Read or change whether this reference is a structural dependency.

TYPO3 Core registers a listener that marks type=inline, type=file (with foreign_field), and type=flex fields as dependencies.

A new enum \TYPO3\CMS\Workspaces\Dependency\DependencyCollectionAction has been added to represent the action context.

Example 

A third-party extension that stores parent-child relationships in a custom field can register a listener to include those references as workspace dependencies:

<?php

namespace Vendor\MyPackage\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Workspaces\Event\IsReferenceConsideredForDependencyEvent;

#[AsEventListener('my-package/workspace-dependency')]
final class WorkspaceDependencyListener
{
    public function __invoke(IsReferenceConsideredForDependencyEvent $event): void
    {
        if ($event->getFieldName() === 'tx_mypackage_parent') {
            $event->setDependency(true);
        }
    }
}
Copied!

Impact 

Extensions can now register custom parent-child relationships as workspace dependencies via this PSR-14 event. This ensures that structurally dependent records are published, staged, or discarded together, preventing orphaned records in workspaces.

The internal pseudo-event mechanism (EventCallback, ElementEntityProcessor) used previously has been removed. This is an internal change that does not affect public API.

Feature: #109018 - PSR-14 event to modify indexed_search result sets 

See forge#109018

Description 

A new PSR-14 event \TYPO3\CMS\IndexedSearch\Event\AfterSearchResultSetsAreGeneratedEvent has been introduced to modify complete search result sets in \TYPO3\CMS\IndexedSearch\Controller\SearchController .

The event is dispatched in searchAction() after all result sets have been built. Event listeners can manipulate complete result sets, including pagination, rows, section data, and category metadata.

The event provides the following methods:

  • getResultSets(): Returns all result sets of the current search.
  • setResultSets(array $resultSets): Replaces the result sets.
  • getSearchData(): Returns the search configuration array.
  • getSearchWords(): Returns the array of search words.
  • getView(): Returns the view instance.
  • getRequest(): Returns the current server request.

Example 

The following example replaces every result set pagination with SlidingWindowPagination:

EXT:my_extension/Classes/EventListener/ModifySearchPaginationListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Pagination\SimplePagination;
use TYPO3\CMS\Core\Pagination\SlidingWindowPagination;
use TYPO3\CMS\IndexedSearch\Event\AfterSearchResultSetsAreGeneratedEvent;

#[AsEventListener(identifier: 'my-extension/modify-search-result-sets')]
final readonly class ModifySearchPaginationListener
{
    public function __invoke(AfterSearchResultSetsAreGeneratedEvent $event): void
    {
        $resultSets = $event->getResultSets();
        foreach ($resultSets as $key => $resultSet) {
            if (($resultSet['pagination'] ?? null) instanceof SimplePagination) {
                $resultSets[$key]['pagination'] = new SlidingWindowPagination(
                    $resultSet['pagination']->getPaginator(),
                    5
                );
            }
        }

        $event->setResultSets($resultSets);
    }
}
Copied!

Impact 

This event allows modifying complete search result sets in a single listener call. It enables custom pagination strategies as well as advanced search result transformations.

Feature: #109080 - Unified RateLimiterFactory with admin overrides 

See forge#109080

Description 

TYPO3's \TYPO3\CMS\Core\RateLimiter\RateLimiterFactory has been refactored to serve as the single entry point for all rate limiting across the system. A new \TYPO3\CMS\Core\RateLimiter\RateLimiterFactoryInterface extends Symfony's RateLimiterFactoryInterface with additional convenience methods for request-based and login rate limiting.

Previously, the backend and frontend password recovery features, as well as the Extbase rate limiting, each created Symfony rate limiter factories directly, bypassing TYPO3's factory. All consumers now use the central TYPO3 factory, which enables a unified admin override mechanism.

Extension developers should type-hint against \TYPO3\CMS\Core\RateLimiter\RateLimiterFactoryInterface when injecting the factory.

Admin overrides via TYPO3_CONF_VARS 

A new configuration option $GLOBALS['TYPO3_CONF_VARS']['SYS']['rateLimiter'] allows administrators to override any rate limiter's settings by its ID. Each key is a limiter ID, and each value is an array of settings to override:

config/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['SYS']['rateLimiter']['login-be'] = [
    'limit' => 3,
    'interval' => '5 minutes',
];

$GLOBALS['TYPO3_CONF_VARS']['SYS']['rateLimiter']['backend-password-recovery'] = [
    'limit' => 1,
    'interval' => '1 hour',
];
Copied!

Known limiter IDs:

  • login-be — Backend login
  • login-fe — Frontend login
  • backend-password-recovery — Backend password reset
  • felogin-password-recovery — Frontend password recovery
  • extbase-<classSlug>-<actionMethod> — Extbase #[RateLimit] actions

Example limiter ID for Extbase action 

The limiter ID for an Extbase action which uses the #[RateLimit] attribute is constructed using the "slugified" classname and the action method name.

EXT:my_extension/Classes/Controller/MyController.php
namespace Vendor\MyExtension\Controller;

use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
use TYPO3\CMS\Extbase\Attribute\RateLimit;

class MyController extends ActionController
{
    #[RateLimit(limit: 5, interval: '10 minutes', message: 'ratelimit.dosomething')]
    public function doSomethingAction(): ResponseInterface
    {
        return $this->redirect('index');
    }
}
Copied!

The limiter ID for the action is: extbase-vendor-myextension-controller-mycontroller-dosomethingaction

General-purpose rate limiting 

Extension developers can now use the factory for custom rate limiting needs.

The createRequestBasedLimiter() method is the recommended entry point for request-scoped rate limiting. It automatically extracts the client's remote IP from the PSR-7 request and uses it as the limiter key:

EXT:my_extension/Classes/Service/MyService.php
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\RateLimiter\RateLimiterFactoryInterface;

class MyService
{
    public function __construct(
        private readonly RateLimiterFactoryInterface $rateLimiterFactory,
    ) {}

    public function doSomething(ServerRequestInterface $request): void
    {
        $limiter = $this->rateLimiterFactory->createRequestBasedLimiter(
            $request,
            [
                'id' => 'my-extension-action',
                'policy' => 'sliding_window',
                'limit' => 10,
                'interval' => '1 hour',
            ]
        );

        $limit = $limiter->consume();
        if (!$limit->isAccepted()) {
            // handle rate limit exceeded
        }
    }
}
Copied!

For cases where a custom key is needed (e.g. a user ID instead of the IP), the createLimiter() method accepts an explicit configuration array and key:

$limiter = $this->rateLimiterFactory->createLimiter(
    [
        'id' => 'my-extension-action',
        'policy' => 'sliding_window',
        'limit' => 10,
        'interval' => '1 hour',
    ],
    $userId
);
Copied!

Pre-configured named services can also be defined in Services.yaml, which are then injectable with the create() method from the RateLimiterFactoryInterface:

EXT:my_extension/Configuration/Services.yaml
myRateLimiter:
  class: TYPO3\CMS\Core\RateLimiter\RateLimiterFactory
  arguments:
    $config:
      id: 'my-custom-limiter'
      policy: 'sliding_window'
      limit: 5
      interval: '10 minutes'
Copied!

Impact 

All rate limiting in TYPO3 now flows through a single factory that supports admin-level overrides. Administrators can tune or restrict rate limits for any component — login, password recovery, Extbase actions, or custom extensions — without modifying code, using $GLOBALS['TYPO3_CONF_VARS']['SYS']['rateLimiter'] .

The login rate limiter now uses human-readable IDs ( login-be, login-fe) instead of SHA1 hashes. Existing cached rate limit state from previous versions will expire naturally.

Feature: #109087 - Introduce BeforeBackendPageRenderEvent for BackendController 

See forge#109087

Description 

A new PSR-14 event \TYPO3\CMS\Backend\Controller\Event\BeforeBackendPageRenderEvent has been introduced. It is dispatched in \TYPO3\CMS\Backend\Controller\BackendController before the main backend page is rendered and provides access to:

  • $view ( \TYPO3\CMS\Core\View\ViewInterface ) – assign additional template variables to the backend top frame view
  • $javaScriptRenderer ( \TYPO3\CMS\Core\Page\JavaScriptRenderer ) – add custom JavaScript modules to the backend top frame
  • $pageRenderer ( \TYPO3\CMS\Core\Page\PageRenderer ) – add further assets such as CSS files (marked @internal)

Example 

EXT:my_extension/Classes/EventListener/BeforeBackendPageRenderEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Backend\Controller\Event\BeforeBackendPageRenderEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;

#[AsEventListener(identifier: 'my-extension/before-backend-page-render')]
final class BeforeBackendPageRenderEventListener
{
    public function __invoke(BeforeBackendPageRenderEvent $event): void
    {
        $event->javaScriptRenderer->addJavaScriptModuleInstruction(
            JavaScriptModuleInstruction::create(
                '@my-vendor/my-extension/backend-module.js'
            )
        );
    }
}
Copied!

Impact 

It is now possible to add custom JavaScript modules and other assets to the TYPO3 backend top frame using the new PSR-14 event \TYPO3\CMS\Backend\Controller\Event\BeforeBackendPageRenderEvent .

Feature: #109126 - Introduce date editor for ext:form 

See forge#109126

Description 

A new web component <typo3-form--date-editor> has been introduced for the form editor backend. It replaces the plain text input for the DateRange validator minimum/maximum fields and the defaultValue field of the Date form element.

Previously, editors had to manually type date values or relative expressions like -18 years into a plain text field. The new structured editor provides a user-friendly UI with five modes:

  • No value – Clears the constraint (empty value)
  • Today – Sets the value to today
  • Absolute date – Native HTML5 date picker producing Y-m-d values
  • Relative date – Structured input with direction (past/future), amount and unit (days, weeks, months, years) producing expressions like -18 years or +1 month
  • Custom relative expression – Free-text input for arbitrary relative date expressions that go beyond the structured input, such as compound expressions like +1 month +3 days. The input is validated against the configured relative date pattern.

Impact 

The form editor backend now provides a structured, user-friendly editor for date constraints and default values on Date form elements. Editors no longer need to know the PHP relative date syntax — they can simply select a mode, direction, amount and unit from dropdown fields. For advanced use cases, the custom mode allows typing arbitrary relative date expressions with real-time validation. Existing form definitions are not affected and continue to work without changes.

Feature: #109130 - Context-aware editing in the page module 

See forge#109130

Description 

The page module now features a context panel for editing page properties and content elements. Clicking an edit button opens a slide-in panel next to the page layout. The editing form is displayed inside the panel while the page layout remains visible in the background.

The panel supports all FormEngine fields in an improved UI. The panel header displays the record title along with a Save and Close button. An expand button allows switching to the full record editing form in the content area at any time. After saving, the panel stays open for further edits.

User settings 

The context panel is enabled by default. It can be disabled per user in User Settings via the Use quick editing for records in the page module option. When disabled, edit buttons navigate directly to the full record editing form as before. The setting takes effect immediately.

Impact 

Editors can now edit records in the page module without leaving the page layout context. The full editing form remains accessible for more complex editing tasks.

Deprecation: #69190 - Deprecate Random Password generator for frontend and backend users 

See forge#69190

Description 

The passwordRules option of the passwordGenerator field control has been deprecated. Password generation is now configured through password policies registered in $GLOBALS['TYPO3_CONF_VARS']['SYS']['passwordPolicies'] .

Each password policy can define a generator section with a class implementing \TYPO3\CMS\Core\PasswordPolicy\Generator\PasswordGeneratorInterface . The field control references a policy by name via the new passwordPolicy option instead of defining rules inline.

Impact 

Using the passwordRules option in TCA field control configuration will trigger a PHP deprecation warning. Support for passwordRules will be removed in TYPO3 v15.

Affected installations 

Installations that use the passwordGenerator field control with the passwordRules option in custom TCA configurations, for example on password fields or secret token fields.

Migration 

Replace the passwordRules option with a passwordPolicy reference.

Before:

EXT:my_extension/Configuration/TCA/Overrides/be_users.php
'fieldControl' => [
    'passwordGenerator' => [
        'renderType' => 'passwordGenerator',
        'options' => [
            'passwordRules' => [
                'length' => 20,
                'upperCaseCharacters' => true,
                'lowerCaseCharacters' => true,
                'digitCharacters' => true,
                'specialCharacters' => false,
            ],
        ],
    ],
],
Copied!

After:

EXT:my_extension/Configuration/TCA/Overrides/be_users.php
'fieldControl' => [
    'passwordGenerator' => [
        'renderType' => 'passwordGenerator',
        'options' => [
            'passwordPolicy' => 'myCustomPolicy',
        ],
    ],
],
Copied!

The referenced password policy must be registered in $GLOBALS['TYPO3_CONF_VARS']['SYS']['passwordPolicies'] :

config/system/additional.php OR typo3conf/system/additional.php
<?php

$GLOBALS['TYPO3_CONF_VARS']['SYS']['passwordPolicies']['myCustomPolicy'] = [
    'generator' => [
        'className' => \TYPO3\CMS\Core\PasswordPolicy\Generator\PasswordGenerator::class,
        'options' => [
            'length' => 20,
            'upperCaseCharacters' => true,
            'lowerCaseCharacters' => true,
            'digitCharacters' => true,
            'specialCharacters' => false,
        ],
    ],
    'validators' => [],
];
Copied!

See Feature: #69190 - Add password generator "wizard" for details on password policies and custom password generators.

Deprecation: #107068 - Rename fieldExplanationText to description 

See forge#107068

Description 

The configuration option fieldExplanationText has been deprecated in favor of description. The new name better reflects its purpose and is easier to understand.

This affects form element type definitions in prototypes.*.formElementsDefinition.*.formEditor configurations, including editors, validators, and finishers in any extension.

Impact 

Using fieldExplanationText will trigger a PHP deprecation warning. The migration service will automatically convert fieldExplanationText to description when form configurations are loaded, ensuring backward compatibility.

Support for fieldExplanationText will be removed in TYPO3 v15.0.

Affected installations 

Any installation using extensions that provide custom form element type definitions with the configuration option fieldExplanationText in their form prototype YAML files (e.g., Configuration/Form/*.yaml or Configuration/Yaml/FormSetup.yaml).

Migration 

Rename any occurrence of fieldExplanationText to description in your form element type definition YAML files (typically located in Configuration/Yaml/FormElements/*.yaml).

Example migration:

# Before (deprecated)
formEditor:
  editors:
    200:
      identifier: placeholder
      label: Placeholder
      fieldExplanationText: Enter the placeholder text

# After
formEditor:
  editors:
    200:
      identifier: placeholder
      label: Placeholder
      description: Enter the placeholder text
Copied!

Deprecation: #107208 - <f:debug.render> ViewHelper 

See forge#107208

Description 

The <f:debug.render> ViewHelper, which has been used internally to render the Fluid debug output for the admin panel, has been deprecated.

Impact 

Calling the ViewHelper from a template will trigger a deprecation warning. The ViewHelper will be removed with TYPO3 v15.

Affected installations 

Projects or extensions that use <f:debug.render> in a template.

Migration 

A custom ViewHelper can be created that mimics the behavior of the Core ViewHelper.

Deprecation: #107802 - Deprecate usage of array in password for authentication in Redis session backend 

See forge#107802

Description 

Since Redis 6.0, it is possible to authenticate against Redis using both a username and a password. Prior to this version, authentication was only possible with a password. With this patch, you can now configure the TYPO3 Redis session backend as follows:

config/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['SYS']['session']['BE']
    = [
        'backend' => \TYPO3\CMS\Core\Session\Backend\RedisSessionBackend::class,
        'options' => [
            'database' => 0,
            'hostname' => 'redis',
            'port' => 6379,
            'username' => 'redis',
            'password' => 'redis',
        ]
    ];
Copied!

Impact 

The "password" configuration option of the Redis session backend is now typed as a array|string. Setting this configuration option with an array is deprecated and will be removed in 15.0.

Affected installations 

All installations using Redis session backend and using the password configuration option to pass an array with username and password to it.

Migration 

Use the configuration options username and password.

Before:

config/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['SYS']['session']['BE']
    = [
        'backend' => \TYPO3\CMS\Core\Session\Backend\RedisSessionBackend::class,
        'options' => [
            'database' => 0,
            'hostname' => 'redis',
            'port' => 6379,
            'username' => 'redis',
            'password' =>[
                 'user' => 'redis',
                 'pass' => 'redis'
             ]
        ]
    ];
Copied!

After:

config/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['SYS']['session']['BE']
    = [
        'backend' => \TYPO3\CMS\Core\Session\Backend\RedisSessionBackend::class,
        'options' => [
            'database' => 0,
            'hostname' => 'redis',
            'port' => 6379,
            'username' => 'redis',
            'password' => 'redis',
        ]
    ];
Copied!

Deprecation: #108557 - TCA option allowedRecordTypes for Page Types 

See forge#108557

Description 

The following methods of \TYPO3\CMS\Core\DataHandling\PageDoktypeRegistry have been marked as deprecated:

  • PageDoktypeRegistry->add()
  • PageDoktypeRegistry->addAllowedRecordTypes()
  • PageDoktypeRegistry->doesDoktypeOnlyAllowSpecifiedRecordTypes()

Impact 

Calling any of the above mentioned methods will trigger a deprecation-level log entry and will result in a fatal PHP error in TYPO3 v15.0.

Affected installations 

All installations using the PageDoktypeRegistry to configure Page Types using the add() method. Or, in some rare cases, using the methods addAllowedRecordTypes() or doesDoktypeOnlyAllowSpecifiedRecordTypes.

Migration 

A new TCA option is introduced to configure allowed record types for pages:

Before:

EXT:my_extension/ext_tables.php
$dokTypeRegistry = GeneralUtility::makeInstance(PageDoktypeRegistry::class);
$dokTypeRegistry->add(
    116,
    [
        'allowedTables' => '*',
    ],
);
Copied!

After:

EXT:my_extension/Configuration/TCA/Overrides/pages.php
$GLOBALS['TCA']['pages']['types']['116']['allowedRecordTypes'] = ['*'];
Copied!

The array can contain a list of table names or a single entry with an asterisk * to allow all types. If no second argument was provided to the add method, then the specific configuration can be omitted, as it will fall back to the default allowed records.

Also note that Page Types are registered through TCA types. The former usage of PageDoktypeRegistry was only useful to define allowed record types different to the default.

The option allowedRecordType is only evaluated within the "pages" table.

Deprecation: #108568 - BackendUserAuthentication::recordEditAccessInternals() and $errorMsg 

See forge#108568

Description 

The method \TYPO3\CMS\Core\Authentication\BackendUserAuthentication::recordEditAccessInternals() and the property \TYPO3\CMS\Core\Authentication\BackendUserAuthentication::$errorMsg have been deprecated.

These methods and properties represented an anti-pattern where the method returned a boolean value but communicated error details through a class property, making the API difficult to use and test.

A new method checkRecordEditAccess() has been introduced that returns an \TYPO3\CMS\Core\Authentication\AccessCheckResult value object containing both the access decision and any error message.

Impact 

Calling the deprecated method recordEditAccessInternals() or accessing the deprecated property $errorMsg will trigger a deprecation-level log entry and will stop working in TYPO3 v15.0.

The extension scanner reports usages as a strong match.

Affected installations 

Instances or extensions that directly call recordEditAccessInternals() or access the $errorMsg property are affected.

Migration 

Replace calls to recordEditAccessInternals() with checkRecordEditAccess(). The new method returns an AccessCheckResult object with two public properties:

  • isAllowed - boolean indicating if access is granted
  • errorMessage - string containing the error message (empty if access is allowed)

Before 

use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;

$backendUser = $this->getBackendUser();
if ($backendUser->recordEditAccessInternals($table, $record)) {
    // Access granted
} else {
    // Access denied, error message is in $backendUser->errorMsg
    $errorMessage = $backendUser->errorMsg;
}
Copied!

After 

use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;

$backendUser = $this->getBackendUser();
$accessResult = $backendUser->checkRecordEditAccess($table, $record);
if ($accessResult->isAllowed) {
    // Access granted
} else {
    // Access denied
    $errorMessage = $accessResult->errorMessage;
}
Copied!

Deprecation: #108843 - ExtensionManagementUtility::addFieldsToUserSettings 

See forge#108843 See forge#108832

Description 

The method \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addFieldsToUserSettings() has been deprecated in favor of the new addUserSetting() method.

The legacy method required two separate steps to add a field to user settings: first adding the field configuration to the columns array, then calling addFieldsToUserSettings() to add it to the showitem list. The new method combines both steps into a single call and uses TCA as the storage location.

Impact 

Calling the deprecated method will trigger a deprecation-level log entry. The method will be removed in TYPO3 v15.0.

The extension scanner reports usages as a strong match.

Affected installations 

Instances or extensions that use ExtensionManagementUtility::addFieldsToUserSettings() or directly modify $GLOBALS['TYPO3_USER_SETTINGS'] to add custom fields to the backend user profile settings are affected.

Migration 

Replace the two-step approach with the new addUserSetting() method. Note that the new method uses TCA-style configuration and should be called from Configuration/TCA/Overrides/be_users.php instead of ext_tables.php.

Before 

// In ext_tables.php
$GLOBALS['TYPO3_USER_SETTINGS']['columns']['myCustomSetting'] = [
    'type' => 'check',
    'label' => 'LLL:EXT:my_ext/Resources/Private/Language/locallang.xlf:myCustomSetting',
];
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addFieldsToUserSettings(
    'myCustomSetting',
    'after:emailMeAtLogin'
);
Copied!

After 

// In Configuration/TCA/Overrides/be_users.php
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addUserSetting(
    'myCustomSetting',
    [
        'label' => 'LLL:EXT:my_ext/Resources/Private/Language/locallang.xlf:myCustomSetting',
        'config' => [
            'type' => 'check',
            'renderType' => 'checkboxToggle',
        ],
    ],
    'after:emailMeAtLogin'
);
Copied!

Field type mapping 

When migrating, use the following type mappings:

Legacy type TCA config
text ['type' => 'input']
email ['type' => 'email']
number ['type' => 'number']
password ['type' => 'password']
check ['type' => 'check', 'renderType' => 'checkboxToggle']
select ['type' => 'select', 'renderType' => 'selectSingle']
language ['type' => 'language']

Deprecation: #108963 - Deprecate PageRenderer->addInlineLanguageDomain() 

See forge#108963

Description 

PageRenderer->addInlineLanguageDomain() has been deprecated in favor of importing JavaScript modules as added in Feature: #108941 - Provide language labels as virtual JavaScript modules.

Impact 

Extension developers can now use labels in JavaScript components, without requiring to preload labels globally or per module, reducing the risk for missing labels and simplifying developer workflows.

Affected installations 

The deprecated method has been added in 14.1, that means only installations that used addInlineLanguageDomain() in 14.1 are affected.

Migration 

The method call to PageRenderer::addInlineLanguageDomain() can be removed and the JavaScript part adds an module import that imports from the '~label/' prefix.

Before:

$pageRenderer->addInlineLanguageDomain('core.bookmarks');
Copied!
import { html } from 'lit';
import { lll } from '@typo3/core/lit-helper.js'

html`<p>{lll('core.bookmarks:groupType.global')}</p>`
Copied!

After:

import { html } from 'lit';
// Import labels from language domain "core.bookmarks"
import labels from '~labels/core.bookmarks';

// Use label
html`<p>{labels.get('groupType.global')}</p>`
Copied!

Deprecation: #109027 - Move language:update command and events to EXT:core 

See forge#109027

Description 

The language:update CLI command and the related \TYPO3\CMS\Install\Service\LanguagePackService have been moved from EXT:install to EXT:core, allowing installations to update language packs without requiring EXT:install to be installed.

Since TYPO3 v13, it is possible to run TYPO3 without EXT:install in composer based installations. However, the language:update command still required EXT:install, which made this advantage impractical for deployments that need to update language packs.

The following classes have been moved and their old class names deprecated:

  • \TYPO3\CMS\Install\Command\LanguagePackCommand is now \TYPO3\CMS\Core\Command\UpdateLanguagePackCommand
  • \TYPO3\CMS\Install\Service\Event\ModifyLanguagePackRemoteBaseUrlEvent is now \TYPO3\CMS\Core\Localization\Event\ModifyLanguagePackRemoteBaseUrlEvent
  • \TYPO3\CMS\Install\Service\Event\ModifyLanguagePacksEvent is now \TYPO3\CMS\Core\Localization\Event\ModifyLanguagePacksEvent

The old class names are registered as aliases via ClassAliasMap and continue to work in TYPO3 v14. Event listeners registered for the deprecated event class names are still called when the new event is dispatched, with a deprecation notice triggered at runtime.

Impact 

Using the old class names will trigger a deprecation notice. The extension scanner will report usages of the deprecated class names.

The old class names will be removed in TYPO3 v15.

Affected installations 

Extensions that use one or more of the deprecated class names listed above.

Migration 

Replace the old class names with the new ones in use statements:

EXT:my_extension/Classes/EventListener/MyEventListener.php
 <?php

 declare(strict_types=1);

 namespace MyVendor\MyExtension\EventListener;

 use TYPO3\CMS\Core\Attribute\AsEventListener;
-use TYPO3\CMS\Install\Service\Event\ModifyLanguagePacksEvent;
+use TYPO3\CMS\Core\Localization\Event\ModifyLanguagePacksEvent;

 final class MyEventListener
 {
     #[AsEventListener(
         identifier: 'my-extension/modify-language-packs',
     )]
     public function __invoke(
         ModifyLanguagePacksEvent $event,
     ): void {
         // ...
     }
 }
Copied!
EXT:my_extension/Classes/EventListener/MyOtherEventListener.php
 <?php

 declare(strict_types=1);

 namespace MyVendor\MyExtension\EventListener;

 use TYPO3\CMS\Core\Attribute\AsEventListener;
-use TYPO3\CMS\Install\Service\Event\ModifyLanguagePackRemoteBaseUrlEvent;
+use TYPO3\CMS\Core\Localization\Event\ModifyLanguagePackRemoteBaseUrlEvent;

 final class MyOtherEventListener
 {
     #[AsEventListener(
         identifier: 'my-extension/modify-language-pack-remote-base-url',
     )]
     public function __invoke(
         ModifyLanguagePackRemoteBaseUrlEvent $event,
     ): void {
         // ...
     }
 }
Copied!

Deprecation: #109029 - FormEngine doSave hidden field 

See forge#109029

Description 

The <input type="hidden" name="doSave"> field in FormEngine was a legacy mechanism where JavaScript set the field value to 1 in order to signal PHP that the submitted form data should be processed as a save operation.

This indirection is no longer needed. TYPO3 now uses native submit button values such as _savedok directly, which are sufficient to determine whether a save should be performed. The field is no longer evaluated internally.

For backwards compatibility the doSave field is still appended to the form on programmatic saves in TYPO3 v14, but this behaviour is deprecated and will be removed in TYPO3 v15.

Impact 

Third-party code reading $request->getParsedBody()['doSave'] to detect whether a save operation was triggered will stop working in TYPO3 v15.

Affected Installations 

Installations with custom backend modules or extensions that inspect the doSave POST field to determine whether incoming form data should be persisted.

Migration 

Replace any check against the doSave POST field with a check against the native submit action fields that FormEngine sends as part of its regular form submission.

Before:

$parsedBody = $request->getParsedBody();
$doSave = (bool)($parsedBody['doSave'] ?? false);
if ($doSave) {
    // process data
}
Copied!

After:

$parsedBody = $request->getParsedBody();
$isSaveAction = !empty($parsedBody['_savedok'])
    || !empty($parsedBody['_saveandclosedok'])
    || !empty($parsedBody['_savedokview'])
    || !empty($parsedBody['_savedoknew']);
if ($isSaveAction) {
    // process data
}
Copied!

Deprecation: #109102 - FormEngine "additionalHiddenFields" key 

See forge#109102

Description 

The additionalHiddenFields result array key in FormEngine was a legacy mechanism that stored hidden <input> HTML strings separately from the main html key. This indirection is no longer needed — elements can simply add their hidden fields to the html key directly.

The following have been deprecated:

  • The additionalHiddenFields key in FormEngine result arrays
  • FormResult::$hiddenFieldsHtml
  • FormResultCollection::getHiddenFieldsHtml()

Impact 

Third-party FormEngine elements that add entries to php:$resultArray['additionalHiddenFields'] will trigger a PHP E_USER_DEPRECATED level error when their result is merged via AbstractNode::mergeChildReturnIntoExistingResult().

Affected installations 

Installations with custom FormEngine elements or containers that populate the additionalHiddenFields result array key.

Migration 

Move hidden field HTML from additionalHiddenFields into the html key directly.

Before:

$resultArray = $this->initializeResultArray();
$resultArray['additionalHiddenFields'][] = '<input type="hidden" name="myField" value="myValue" />';
Copied!

After:

$resultArray = $this->initializeResultArray();
$resultArray['html'] .= '<input type="hidden" name="myField" value="myValue" />';
Copied!

Important: #70867 - XLIFF whitespace handling now respects xml:space 

See forge#70867

Description 

TYPO3's XLIFF parser now properly respects the xml:space attribute according to the XML specification (https://www.w3.org/TR/xml/#sec-white-space).

This affects how whitespace (spaces, tabs, newlines) in translation strings are handled.

Without xml:space="preserve" (default behavior):

Multiple consecutive whitespace characters (spaces, tabs, newlines) are collapsed into a single space, and leading/trailing whitespace is trimmed.

Example XLIFF source:

<trans-unit id="my.label">
  <source>This is a
    multi-line
    string.</source>
</trans-unit>
Copied!

Before: The string contained literal newlines and indentation. After: The string becomes "This is a multi-line string."

With xml:space="preserve":

Whitespace is kept exactly as written in the XLIFF file.

<trans-unit id="my.label" xml:space="preserve">
  <source>This is a
    multi-line
    string.</source>
</trans-unit>
Copied!

The string remains "This is a\n multi-line\n string."

Impact 

Translation strings that previously contained unintended whitespace (from formatting in the XLIFF file) will now display correctly without extra spaces or line breaks.

If you intentionally need preserved whitespace in a translation string, add the xml:space="preserve" attribute to the <trans-unit> element (XLIFF 1.2) or <segment> element (XLIFF 2.0).

This change affects both XLIFF 1.2 and XLIFF 2.0/2.1 formats.

Important: #93765 - Extbase identity map now language-aware 

See forge#93765

Description 

The Extbase persistence session's identity map now includes language context when caching domain objects. Previously, the identity map used identifiers based only on the record's UID and localized UID, which could cause incorrect translations to be returned when the same object was accessed with different LanguageAspect configurations within the same request.

The identity map identifier now includes the language context, specifically the contentId, overlayType, and fallbackChain properties of the LanguageAspect.

Impact 

This change ensures that objects loaded with different language configurations are cached separately in the identity map. For example, if an object is first loaded with OVERLAYS_ON and then queried again with OVERLAYS_MIXED, the system will correctly return different cached objects for each context.

The change is transparent for most use cases. However, objects retrieved with different language settings are now distinct instances. Code relying on object identity (e.g., using === comparison) between objects loaded with different language settings will need adjustment.

Important: #102906 - Prevent Extbase errorAction from writing session data 

See forge#102906

Description 

Previously, any validation error handled implicitly by the Extbase ActionController::errorAction() would persist the resulting FlashMessage items to the user session. In cases where no session existed, a new session would be generated and a session cookie sent to the client. This behavior could lead to automated crawlers generating a large number of unnecessary sessions.

When the errorAction() is invoked (for example, due to validation errors), flash messages are no longer persisted to the session but are instead transferred with the corresponding ForwardResponse.

The implementation introduces two new public methods in ForwardResponse:

  • withFlashMessages(FlashMessage ...$flashMessages) - Adds flash messages to the forward response
  • getFlashMessages() - Retrieves flash messages from the forward response

Flash messages are transferred through ExtbaseRequestParameters when forwarding requests and are restored from ExtbaseRequestParameters in ActionController::initializeStateFromExtbaseRequestParameters().

Important: #105441 - TCA select fields with null item values create nullable columns 

See forge#105441

Description 

When configuring a TCA select field with renderType => 'selectSingle' and adding an item with 'value' => null, the generated database column is now nullable regardless of whether the other item values are integers or strings.

Previously, the following configuration with integer item values would incorrectly generate a VARCHAR(255) column:

'config' => [
    'type' => 'select',
    'renderType' => 'selectSingle',
    'foreign_table' => 'some_table',
    'default' => null,
    'items' => [
        ['label' => 'Please choose', 'value' => null],
    ],
],
Copied!

This now correctly generates INT UNSIGNED DEFAULT NULL.

Similarly, a configuration with string item values now also generates a nullable column:

'config' => [
    'type' => 'select',
    'renderType' => 'selectSingle',
    'default' => null,
    'items' => [
        ['label' => 'Default', 'value' => null],
        ['label' => 'Option', 'value' => 'some_value'],
    ],
],
Copied!

This now correctly generates VARCHAR(255) DEFAULT NULL instead of VARCHAR(255) DEFAULT '' NOT NULL.

Important: #108433 - Workspace selector moved to sidebar with color and description 

See forge#108433

Description 

The workspace selector has been moved from the top toolbar to the backend sidebar. The selector now displays the full name of the currently active workspace and provides a dropdown to switch between available workspaces.

A colored top bar indicator is shown whenever a workspace is active, giving editors a clear visual cue about which workspace they are working in.

Color per workspace 

Administrators can now assign a color to each workspace via the Color field in the workspace record. The selected color is used for the top bar indicator and the workspace selector in the sidebar, making it easier to visually distinguish workspaces at a glance.

The following colors are available: red, orange, yellow, lime, green, teal, blue, indigo, purple, and magenta. The default color for new workspaces is orange.

Description as tooltip 

The Description field of a workspace record is now displayed as a tooltip on both the workspace selector dropdown items and the top bar indicator. This allows administrators to provide additional context about the purpose of a workspace, which editors can see by hovering over the workspace name.

Workspace Live indicator option 

A user setting Enable workspace Live indicator is available in User Settings > Personalization. When enabled (the default), the top bar indicator is shown while working in the Live workspace. Users who find the indicator distracting can disable it. The setting takes effect immediately without requiring a page reload. The indicator remains visible when any non-Live workspace is active, regardless of this setting.

Impact 

Editors will see the workspace selector in the sidebar instead of the top toolbar. The workspace indicator bar at the top of the backend now reflects the configured color of the active workspace.

Administrators are encouraged to assign meaningful colors and descriptions to their workspaces to improve the editing experience for their teams.

Important: #108557 - Drop PageDoktypeRegistry onlyAllowedTables option 

See forge#108557

Description 

It is possible to limit allowed tables for Page Types ("doktype"). However, up until now the default behavior when switching types was to ignore possible violations to these rules. The behavior could be changed on a per doktype level like this:

EXT:my_extension/ext_tables.php
$dokTypeRegistry = GeneralUtility::makeInstance(PageDoktypeRegistry::class);
$dokTypeRegistry->add(
    116,
    [
        'onlyAllowedTables' => true,
    ],
);
Copied!

This would make the Page Type 116 strict, when switching the Page Type (aka doktype).

This option is now obsolete, as this functionality is always enabled. Switching Page Types is no longer possible if it violates the configured allowed tables, making the system more consistent.

Some remarks: This option was rarely used and often misunderstood. The option to configure allowed tables was called allowedTables. It was not clear what onlyAllowedTables should even mean without looking into the documentation.

On a rational point of view it does not make sense to configure restrictions for Page Types, when they are ignored per default for the action of switching types. Allowing to violate the rules makes them useless in the first place. So either remove those restrictions altogether or make them always work like it is done now.

Important: #108783 - Backend user language default changed to "en" 

See forge#108783

Description 

The backend user language field (be_users.lang) historically used default as the value for English. This has been changed to use the standard ISO 639-1 language code en instead.

The language key default is still accepted for backwards compatibility with custom code, but is no longer selectable in the backend user interface.

An upgrade wizard "Migrate backend user language from 'default' to 'en'" is available to migrate existing backend user records.

Impact 

  • New backend users will have en as their default language instead of default.
  • Existing backend users with lang=default should run the upgrade wizard to migrate to lang=en.
  • In general: Code that uses default as a language key (e.g. custom instances of LanguageService) will continue to work as default is still mapped to en internally.

Important: #108796 - Internal shortcut classes renamed to bookmark 

See forge#108796

Description 

As part of the centralized bookmark management feature, several internal classes related to backend shortcuts have been renamed to use "bookmark" terminology. These classes were marked as @internal and are not part of the public TYPO3 API. However, some extensions might have used them despite being internal.

The following classes have been renamed or replaced:

\TYPO3\CMS\Backend\Backend\Shortcut\ShortcutRepository
Removed and replaced by \TYPO3\CMS\Backend\Backend\Bookmark\BookmarkService and \TYPO3\CMS\Backend\Backend\Bookmark\BookmarkRepository .
\TYPO3\CMS\Backend\Controller\ShortcutController
Renamed to \TYPO3\CMS\Backend\Controller\BookmarkController .
\TYPO3\CMS\Backend\Backend\ToolbarItems\ShortcutToolbarItem
Renamed to \TYPO3\CMS\Backend\Backend\ToolbarItems\BookmarkToolbarItem .

The JavaScript module @typo3/backend/toolbar/shortcut-menu has been removed and replaced by the new bookmark management modules in @typo3/backend/bookmark/.

14.1 Changes 

Table of contents

Breaking Changes 

None since TYPO3 v14.0 release.

Features 

Deprecation 

Important 

Feature: #107088 - Allow to utilize password validators in Install Tool 

See forge#107088

Description 

The TYPO3 Install Tool password can now utilize validators as defined via the $GLOBALS['TYPO3_CONF_VARS']['SYS']['passwordPolicies']['installTool']['validators'] array. By default, this re-uses the default validator \TYPO3\CMS\Core\PasswordPolicy\Validator\CorePasswordValidator with the configuration:

'SYS' => [
    'passwordPolicies' => [
        'installTool' => [
            'validators' => [
                \TYPO3\CMS\Core\PasswordPolicy\Validator\CorePasswordValidator::class => [
                    'options' => [
                        'minimumLength' => 8,
                        'upperCaseCharacterRequired' => true,
                        'lowerCaseCharacterRequired' => true,
                        'digitCharacterRequired' => true,
                        'specialCharacterRequired' => true,
                    ],
                    'excludeActions' => [],
                ],
            ],
        ],
    ],
],
Copied!

This will require 8 characters minimum (as it was before) and now also require at least one upper-case, one lower-case, one digit and one special character.

The validator is utilized in both scenarios when setting the Install Tool password via CLI (bin/typo3 install:password:set) as well as the Install Tool GUI via Admin Tools > Settings > Change Install Tool Password in the TYPO3 backend.

If a password is auto-generated via the mentioned CLI command, by default it uses 8 characters. The password-length for adapted validator configurations can then be specified with the new --password-length=XXX argument.

Impact 

Maintainers now need to set secure Install Tool passwords and can configure validation rules.

Existing Install Tool passwords are not affected, making this a non-breaking feature. However, these should be revisited by maintainers and maybe set to a more secure password.

To disable password policies (not recommended!), the configuration option $GLOBALS['TYPO3_CONF_VARS']['SYS']['passwordPolicies']['installTool']['validators'] can be set to an empty array (or null).

Feature: #107756 - Add QR Code module 

See forge#107756

Description 

A new Link Management > QR Codes backend module has been introduced, grouped alongside the existing Link Management > Redirects module.

The QR Codes module provides editors with an efficient way to generate reusable QR codes for various purposes, such as printing them on promotional materials, booth displays, or marketing collateral.

Each generated QR code contains a permanent, unique URL that never changes, ensuring printed materials remain valid indefinitely. While the QR code URL itself stays constant, the destination it redirects to can be updated at any time, providing flexibility to adapt campaigns or redirect visitors to current content without requiring reprints.

The module includes a convenient button to generate QR codes on demand, offering multiple download options including different formats (PNG, SVG) and customizable sizes to suit various use cases and printing requirements.

Impact 

The new QR Code module enables users to create scannable QR codes that redirect to any specified URL. This feature is particularly valuable for marketing campaigns, events, and printed materials where maintaining flexibility in the destination URL is essential while preserving the QR code itself.

Feature: #107837 - Route enhancers in site sets 

See forge#107837

Description 

Site sets can now define route enhancers in a dedicated route-enhancers.yaml file. This allows extensions to provide route enhancers as part of their site set configuration, which are automatically merged into the site configuration when the set is used as a dependency.

The route enhancers from site sets are applied as presets. This means that site-level route enhancer configuration takes precedence and can override set-defined enhancers.

Usage 

Create a route-enhancers.yaml file in your site set directory alongside the config.yaml:

EXT:my_extension/Configuration/Sets/MySet/
├── config.yaml
└── route-enhancers.yaml
Copied!

The file must contain a routeEnhancers key with the route enhancer definitions:

EXT:my_extension/Configuration/Sets/MySet/route-enhancers.yaml
routeEnhancers:
  MyEnhancer:
    type: Simple
    routePath: '/my-path/{param}'
    aspects:
      param:
        type: StaticValueMapper
        map:
          value1: '1'
          value2: '2'
Copied!

The route enhancers file supports YAML imports, allowing you to split configuration across multiple files:

EXT:my_extension/Configuration/Sets/MySet/route-enhancers.yaml
imports:
  - { resource: 'route-enhancers/*.yaml' }

routeEnhancers:
  # Additional enhancers can be defined here
Copied!

Merging behavior 

Route enhancers from site sets are merged in dependency order. When a site uses multiple sets, enhancers from earlier dependencies are loaded first, and later sets can override them.

Site-level route enhancer configuration always takes precedence over set-defined enhancers. This allows sites to customize or override preset configurations from sets.

Example 

Given a site set with:

EXT:my_extension/Configuration/Sets/MySet/route-enhancers.yaml
routeEnhancers:
  PageType:
    type: PageType
    default: '0'
    map:
      feed.xml: '100'
Copied!

And a site configuration with:

config/sites/my-site/config.yaml
dependencies:
  - my_extension/my-set

routeEnhancers:
  PageType:
    type: PageType
    default: '100'
    map:
      rss.xml: '200'
Copied!

The resulting configuration will be:

routeEnhancers:
  PageType:
    type: PageType
    default: '100'
    map:
      feed.xml: '100'
      rss.xml: '200'
Copied!

Scalar values from the site configuration override set-defined values, while new keys are appended.

Impact 

Extensions can now ship route enhancers as part of their site sets, providing a streamlined way to configure routing for extension functionality. This is particularly useful for extensions that require specific URL patterns, such as sitemap extensions or API endpoints.

Invalid route enhancer configurations are handled gracefully: sets with invalid route-enhancers.yaml files are skipped and logged, similar to other set validation errors.

Feature: #107837 - Sitemap route enhancers provided via site set 

See forge#107837

Description 

The SEO extension now ships its sitemap route enhancers as part of the typo3/seo-sitemap site set. When this set is used as a dependency, the route enhancers for XML sitemaps are automatically configured.

This enables clean URLs for sitemaps out of the box:

  • /sitemap.xml - Main sitemap index
  • /sitemap-type/pages - Pages sitemap

Previously, these route enhancers had to be manually configured in each site's config.yaml.

Impact 

Sites using the typo3/seo-sitemap set no longer need to manually configure sitemap route enhancers. The clean URLs are available automatically.

Sites can still override or extend the route enhancers in their site configuration if needed.

Feature: #107961 - Search translated pages in page tree 

See forge#107961

Description 

The page tree filter has been extended with the ability to search for pages through their translated content. This enhancement makes it significantly easier to find pages in multilingual TYPO3 installations, particularly when editors work primarily with translated content.

The page tree filter now supports two translation search methods:

  1. Search by translated page title: When a search phrase matches a translated page title or nav_title, the corresponding default language page will be found and displayed in the page tree.
  2. Search by translation UID: When searching for a numeric page UID that belongs to a translated page, the parent default language page will be found and displayed.

Both search methods work seamlessly alongside the existing search capabilities (searching by page title, nav_title, or default language UID).

Configuration 

Translation search is enabled by default and can be controlled in two ways:

User TSconfig 

Administrators can control the availability of translation search via User TSconfig:

# Disable searching in translated pages for specific users/groups
options.pageTree.searchInTranslatedPages = 0
Copied!

User Preference 

Individual backend users can toggle this setting using the page tree toolbar menu. The preference is stored in the backend user's configuration, allowing each user to customize their search behavior.

Visual Feedback 

When a page is found through a translation match, a colored label is automatically added to provide clear visual feedback:

Single translation match

Displays "Found in translation: [Language Name]"

Example: When searching for "Produkte", a page found via its German translation shows "Found in translation: German"

Multiple translation matches

Displays "Found in multiple translations"

Example: When searching for "Home", a page with matching French and German translations shows "Found in multiple translations"

Direct matches

Pages matching the search phrase directly (L=0) show "Search result"

Example: When searching for "Products", the English page titled "Products" shows "Search result"

Combined matches

When a page matches both directly and through a translation, both labels are displayed.

Example: Searching for "Home" finds a page titled "Home" with a German translation "Startseite Home" - the page shows both labels.

Flexibility for developers 

Developers can still use the PSR-14 event \TYPO3\CMS\Backend\Controller\Event\AfterPageTreeItemsPreparedEvent to add custom labels or modify the prepared tree items before they are rendered.

Impact 

Editors working in multilingual TYPO3 installations can now efficiently search for pages using translated titles or translation UIDs. The visual labels provide immediate feedback about how search results were matched, improving the user experience when navigating complex page trees.

The feature respects user permissions (language restrictions from user groups) and workspace context, ensuring that only accessible translations are searched.

Feature: #108344 - Allow number of decimals in stdWrap.bytes function 

See forge#108344

Description 

The TypoScript function stdWrap.bytes now accepts an additional configuration parameter decimals. It allows to explicitly define the number of decimals in the resulting number representation. By default, the number of decimals is derived from the formatted size.

In addition, the consumed PHP function TYPO3\CMS\Core\Utility\GeneralUtility::formatSize is extended as well. The additional parameter $decimals is added and defaults to null, which results in the same behavior as for the TypoScript function stdWrap.bytes.

Example 

lib.fileSize = TEXT
lib.fileSize {
    value = 123456
    bytes = 1
    bytes.decimals = 1
}
Copied!

Impact 

By allowing to configure the number of decimals in stdWrap.bytes, integrators can now better adapt the output of the formatted size returned by the TypoScript function. This was previously not possible by default and needed some workarounds in TypoScript.

Feature: #108431 - Add TCA datetime format=datetimesec option 

See forge#108431

Description 

The TCA configuration config option type=datetime can now specify the format=datetimesec format to offer a date/time picker for entering a date (day, month, year) with a specific time (hour, minute, second).

Previously, only a datepicker for hour and minute was available, even though the utilized component (Flatpickr) supports entering seconds.

This format can either be specified for dbType=datetime (native SQL datetime columns based on a timestamp that always includes seconds) or for the integer-based storage without a dbType option (UNIX timestamp).

Example TCA configuration:

EXT:my_extension/Configuration/TCA/tx_domain_model_myelement.php
<?php

return [
    // ...
    'columns' => [
        'meteor_impact' => [
            'label' => 'Time of estimated impact',
            'config' => [
                'type' => 'datetime',
                'format' => 'datetimesec',
                'dbType' => 'datetime', // can also be omitted for integer-based storage
                'nullable' => true, // can also be false
            ],
        ],
    ],
    // ...
];
Copied!

Impact 

Editors and integrators can now specify dates including seconds in database record fields, if the fields are configured with the new TCA config format datetimesec.

This can be set for any TCA field that already internally receives a UNIX timestamp value.

-- For the editor who has everything, but seconds.

Feature: #108462 - Add PSR-14 Event AfterPageContentPreviewRenderedEvent 

See forge#108462

Description 

The class \TYPO3\CMS\Backend\View\BackendLayout\Grid\GridColumnItem is the central entity to generate various previews of content elements.

Developers can either use the event \TYPO3\CMS\Backend\View\Event\PageContentPreviewRenderingEvent to generate a preview or implementing \TYPO3\CMS\Backend\Preview\PreviewRendererInterface .

The new PSR-14 event \TYPO3\CMS\Backend\View\Event\AfterPageContentPreviewRenderedEvent can now be used to enrich the output generated by one of those.

Example 

An example event listener could look like this:

Example event listener class
namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Backend\View\Event\AfterPageContentPreviewRenderedEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener('my-extension')]
final class AfterPageContentPreviewRenderedEventListener
{
    public function __invoke(AfterPageContentPreviewRenderedEvent $event): void
    {
        $content = 'before<hr />'. $event->getPreviewContent() . '<hr />after';
        $event->setPreviewContent($content);
    }
}
Copied!

Impact 

As integrator it is sometimes handy to enrich the previews of other content elements and plugins to display additional fields.

Feature: #108463 - User-specific configuration of date-timepicker's first day of a week 

See forge#108463

Description 

Previously, the date-time picker used the selected locale of a backend user.

However, certain locale configurations might rather be a user preference. For example, users may prefer an english backend but want to use a weekday start on "Monday" instead of "Sunday" due to their cultural habits.

Thus, the "first day of a week" has been decoupled from the locale selection and can be configured on a per-user setting. By default, it still inherits the locale's default setting, if not changed (for example, english=sunday and german=monday).

Inside the user settings and tab panel Backend appearance, a new dropdown First day of week in calendar popups appears.

The setting is stored in both the persisted be_users.uc preference blob, as well as on the JavaScript-persistence side.

Impact 

Editors can now choose the first day of a week as a user preference, independent from locale selection.

Feature: #108508 - Fluid components integration 

See forge#108508

Description 

Fluid 4.3 introduced the concept of components to Fluid (see Components). Since then, it was already possible to use components in TYPO3 projects by creating a custom ComponentCollection class that essentially connects a folder of template files to a Fluid ViewHelper namespace. Using that class it was also possible to use an alternative folder structure for a component collection and to allow passing arbitrary arguments to components within that collection.

Now it is possible to define component collections purely with configuration. For the most common use cases, it is no longer necessary to create a custom PHP class, which makes it much easier for integrators to setup components in TYPO3 projects.

Registering component collections 

The new extension-level configuration file Configuration/Fluid/ComponentCollections.php is introduced, which allows extensions to register one or multiple new component collections. It is also possible to extend existing collections registered by other extensions (such as adding template paths to override components defined by another extension).

Basic example:

EXT:my_extension/Configuration/Fluid/ComponentCollections.php
<?php

return [
    'MyVendor\\MyExtension\\Components' => [
        'templatePaths' => [
            10 => 'EXT:my_extension/Resources/Private/Components',
        ],
    ],
];
Copied!

Components in that collection can then be used in any Fluid template:

<html
    xmlns:my="http://typo3.org/ns/MyVendor/MyExtension/Components"
    data-namespace-typo3-fluid="true"
>

<my:organism.header.navigation />
Copied!

By default, component collections use a folder structure that requires a separate folder per component. This is handy if you want to put other files right next to your component template, such as the matching CSS or JS file, or even a custom language file. Using the example above, <my:organism.header.navigation /> would point to EXT:my_extension/Resources/Private/Components/Organism/Header/Navigation/Navigation.fluid.html.

If not otherwise specified, components use a strict API, meaning that all arguments that are passed to a component need to be defined with <f:argument> in the component template.

Both defaults can be adjusted per collection by providing configuration options:

  • templateNamePattern allows you to use a different folder structure, available variables are {path} and {name}. For <my:organism.header.navigation>, {path} would be Organism/Header and {name} would be Navigation.
  • setting additionalArgumentsAllowed to true allows passing undefined arguments to components.

Advanced example:

EXT:my_extension/Configuration/Fluid/ComponentCollections.php
<?php

return [
    'MyVendor\\MyExtension\\Components' => [
        'templatePaths' => [
            10 => 'EXT:my_extension/Resources/Private/Components',
        ],
        'templateNamePattern' => '{path}/{name}',
        'additionalArgumentsAllowed' => true,
    ],
];
Copied!

Using this example <my:organism.header.navigation /> would point to EXT:my_extension/Resources/Private/Components/Organism/Header/Navigation.fluid.html (note the missing Navigation folder).

It is possible to influence certain aspects of Fluid components using PSR-14 events, see PSR-14 events for Fluid components

Creating components 

A typical component looks just like a normal Fluid template, except that it defines all of its arguments with the Argument ViewHelper <f:argument>. Also, the Slot ViewHelper <f:slot> can be used to receive HTML content.

Example:

EXT:my_extension/Resources/Private/Components/Molecule/TeaserCard/TeaserCard.fluid.html
<html
    xmlns:my="http://typo3.org/ns/MyVendor/MyExtension/Components"
    data-namespace-typo3-fluid="true"
>

<f:argument name="title" type="string" />
<f:argument name="link" type="string" />
<f:argument name="icon" type="string" optional="{true}" />

<a href="{link}" class="teaserCard">
    <f:if condition="{icon}">
        <my:atom.icon identifier="{icon}">
    </f:if>
    <div class="teaserCard__title">{title}</div>
    <div class="teaserCard__content"><f:slot /></div>
</a>
Copied!

The example also demonstrates that components can (and should) use other components, in this case <my:atom.icon>. Depending on the use case, it might also make sense to pass the output of one component to another component via a slot:

<html
    xmlns:my="http://typo3.org/ns/MyVendor/MyExtension/Components"
    data-namespace-typo3-fluid="true"
>

<my:molecule.teaserCard
    title="TYPO3"
    link="https://typo3.org/"
    icon="typo3"
>
    <my:atom.text>{content}</my:atom.text>
</my:molecule.teaserCard>
Copied!

You can learn more about components in Defining Components. Note that this is part of the documentation of Fluid Standalone, which means that it doesn't mention TYPO3 specifics.

Migration and co-existence with class-based collections 

Configuration-based and class-based component collections can be used side by side. For more advanced use cases, it might still be best to ship a custom class to define a component collection. However, most use cases can easily be migrated to the configuration-based approach, since they usually just consist of boilerplate code around the configuration options.

Since the new approach is not available in TYPO3 13, it is possible to ship both variants to provide backwards-compatibility: If a specific component collection is defined both via class and via configuration, in TYPO3 13 the class will be used, while in TYPO3 14 the configuration will be used and the class will be ignored completely.

Extending component collections from other extensions 

It is possible to extend the configuration of other extensions using the introduced configuration file. This allows integrators to merge their own set of components into an existing component collection:

EXT:vendor_extension/Configuration/Fluid/ComponentCollections.php
<?php

return [
    'SomeVendor\\VendorExtension\\Components' => [
        'templatePaths' => [
            10 => 'EXT:vendor_extension/Resources/Private/Components',
        ],
    ],
];
Copied!
EXT:my_extension/Configuration/Fluid/ComponentCollections.php
<?php

return [
    'SomeVendor\\VendorExtension\\Components' => [
        'templatePaths' => [
            1765990741 => 'EXT:my_extension/Resources/Private/Extensions/VendorExtension/Components',
        ],
    ],
];
Copied!

For template paths, the familiar rule applies: They will be sorted by their keys and will be processed in reverse order. In this example, if my_extension defines a component that already exists in vendor_extension, it will override the original component in vendor_extension.

Impact 

Fluid component collections no longer need to be defined by creating a custom class, but can now be registered purely by configuration. Existing class-based collections will continue to work. If a collection namespace is registered both by a class and by configuration, the configuration overrules the class and any custom code in the class is ignored.

Feature: #108508 - PSR-14 events for Fluid components 

See forge#108508

Description 

Three PSR-14 events have been added to influence the processing and rendering of Fluid components that are registered using the new configuration file (see Fluid components integration).

ModifyComponentDefinitionEvent 

The ModifyComponentDefinitionEvent can be used to modify the definition of a component before it's written to the cache. Component definitions must not have any dependencies on runtime information, as they might be used for static analysis or IDE auto-completion. Due to the component definitions cache, this is already enforced, as the registered events are only executed once and not on every request.

Example:

EXT:my_extension/Classes/EventListener/ModifyComponentDefinitionListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Fluid\Event\ModifyComponentDefinitionEvent;
use TYPO3Fluid\Fluid\Core\Component\ComponentDefinition;
use TYPO3Fluid\Fluid\Core\ViewHelper\ArgumentDefinition;

#[AsEventListener]
final readonly class ModifyComponentDefinitionListener
{
    public function __invoke(ModifyComponentDefinitionEvent $event): void
    {
        // Add required argument to one specific component
        if (
            $event->getNamespace() === 'MyVendor\\MyExtension\\Components' &&
            $event->getComponentDefinition()->getName() === 'myComponent'
        ) {
            $originalDefinition = $event->getComponentDefinition();
            $event->setComponentDefinition(new ComponentDefinition(
                $originalDefinition->getName(),
                [
                    ...$originalDefinition->getArgumentDefinitions(),
                    'myArgument' => new ArgumentDefinition('myArgument', 'string', '', true),
                ],
                $originalDefinition->additionalArgumentsAllowed(),
                $originalDefinition->getAvailableSlots(),
            ));
        }
    }
}
Copied!

ProvideStaticVariablesToComponentEvent 

The ProvideStaticVariablesToComponentEvent can be used to inject additional static variables into component templates. As with the ModifyComponentDefinitionEvent , these variables must not have any dependencies on runtime information, as they might be used for static analysis or IDE auto-completion. The RenderComponentEvent can be used to add variables with runtime dependencies.

Valid use cases for this event might be:

  • providing static (!) design tokens (colors, icons, ...) to all components in a collection
  • generating prefix strings based on the component's name

Example:

EXT:my_extension/Classes/EventListener/ProvideStaticVariablesToComponentListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Fluid\Event\ProvideStaticVariablesToComponentEvent;

#[AsEventListener]
final readonly class ProvideStaticVariablesToComponentListener
{
    public function __invoke(ProvideStaticVariablesToComponentEvent $event): void
    {
        // Provide design tokens to all components in a collection
        if ($event->getComponentCollection()->getNamespace() === 'MyVendor\\MyExtension\\Components') {
            $event->setStaticVariables([
                ...$event->getStaticVariables(),
                'designTokens' => [
                    'color1' => '#abcdef',
                    'color2' => '#123456',
                ],
            ]);
        }
    }
}
Copied!

RenderComponentEvent 

The RenderComponentEvent can be used to alter or replace the rendering of Fluid components. There are three possible use cases:

  1. fully take over the rendering of components by filling the $renderedContent with $event->setRenderedContent(). The first event that does this skips all following event listeners.
  2. provide additional arguments (= variables in the component template) or slots to the component with $event->setArguments()/ $event->setSlots().
  3. execute additional code that doesn't influence the component rendering directly, e. g. adding certain frontend assets to the page automatically.

Example:

EXT:my_extension/Classes/EventListener/RenderComponentListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Page\AssetCollector;
use TYPO3\CMS\Fluid\Event\RenderComponentEvent;

#[AsEventListener]
final readonly class RenderComponentListener
{
    public function __construct(private AssetCollector $assetCollector) {}

    public function __invoke(RenderComponentEvent $event): void
    {
        // Add bundled components CSS if a component is used on the page
        if ($event->getComponentCollection()->getNamespace() === 'MyVendor\\MyExtension\\Components') {
            $this->assetCollector->addStyleSheet(
                'componentsBundle',
                'EXT:my_extension/Resources/Public/ComponentsBundle.css'
            );
        }
    }
}
Copied!

Impact 

Three new PSR-14 events can be used to influence the processing and rendering of Fluid components.

Feature: #108524 - Configuration file to register global Fluid namespaces 

See forge#108524

Description 

The extension-level configuration file Configuration/Fluid/Namespaces.php is introduced, which enables a structured way to register and extend global Fluid namespaces. This replaces the old configuration in $GLOBALS['TYPO3_CONF_VARS']['SYS']['fluid']['namespaces'] , see deprecation.

Example:

EXT:my_extension/Configuration/Fluid/Namespaces.php
<?php

return [
    'myext' => ['MyVendor\\MyExtension\\ViewHelpers'],
    'mycmp' => ['MyVendor\\MyExtension\\Components'],
];
Copied!

Overriding existing ViewHelpers 

TYPO3 reads and merges Configuration/Fluid/Namespaces.php files from all loaded extensions in the usual loading order, which can be manipulated by declaring dependencies in composer.json and possibly ext_emconf.php. If an extension registers a namespace that has already been registered by another extension, these namespaces will be merged by Fluid. This allows extensions to override ViewHelpers of another extension selectively.

Example (extension2 depends on extension1):

EXT:my_extension1/Configuration/Fluid/Namespaces.php
<?php

return [
    'myext' => ['MyVendor\\MyExtension1\\ViewHelpers'],
];
Copied!
EXT:my_extension2/Configuration/Fluid/Namespaces.php
<?php

return [
    'myext' => ['MyVendor\\MyExtension2\\ViewHelpers'],
];
Copied!

Resulting namespace definition:

[
    'myext' => [
        'MyVendor\\MyExtension1\\ViewHelpers',
        'MyVendor\\MyExtension2\\ViewHelpers',
    ],
];
Copied!

Namespaces are processed in reverse order, which means that <myext:demo /> would first check for EXT:my_extension2/Classes/ViewHelpers/DemoViewHelper.php, and would fall back to EXT:my_extension1/Classes/ViewHelpers/DemoViewHelper.php.

PSR-14 event to modify namespaces 

The new ModifyNamespacesEvent is introduced, which allows modification of the whole namespaces array before it is being passed to Fluid. This allows for example to:

  • completely redefine an existing namespace (instead of extending it)
  • add namespaces conditionally
  • modify order of merged namespaces

Example:

EXT:my_extension/Classes/EventListener/ModifyNamespacesListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Fluid\Event\ModifyNamespacesEvent;

#[AsEventListener]
final readonly class ModifyNamespacesListener
{
    public function __invoke(ModifyNamespacesEvent $event): void
    {
        $namespaces = $event->getNamespaces();
        // Replace existing "theme" namespace completely
        $namespaces['theme'] = ['MyVendor\\MyExtension\\ViewHelpers'];
        $event->setNamespaces($namespaces);
    }
}
Copied!

Note that namespaces might still be imported locally from within a template file, which is unaffected by this event.

Backwards-compatible namespaces in extensions 

There are several ways to provide backwards-compatible global namespaces in extensions, depending on the concrete use case:

  • preferred: Define namespace in TYPO3_CONF_VARS (with version check) and in new Namespaces.php. This means that in TYPO3 v14 installations the new Namespaces.php can already be used to extend the namespace.
  • alternative: Define namespace both in TYPO3_CONF_VARS (without version check) and in new Namespaces.php. This means however that the namespace can only be extended with TYPO3_CONF_VARS, not with the new Namespaces.php.
  • keep TYPO3_CONF_VARS until support for < v14 is dropped by the extension. This can only be extended with TYPO3_CONF_VARS as well.
  • implement own merging logic in ModifyNamespacesEvent if necessary.

Example for preferred option:

EXT:my_extension/Configuration/Fluid/Namespaces.php
<?php

return [
    'myext' => ['MyVendor\\MyExtension\\ViewHelpers'],
];
Copied!
EXT:my_extension/ext_localconf.php
<?php

if ((new \TYPO3\CMS\Core\Information\Typo3Version())->getMajorVersion() < 14) {
    $GLOBALS['TYPO3_CONF_VARS']['SYS']['fluid']['namespaces']['myext'][] = 'MyVendor\\MyExtension\\ViewHelpers';
}
Copied!

To sum up:

  • Namespaces.php can only extend namespaces defined in Namespaces.php.
  • TYPO3_CONF_VARS can extend both TYPO3_CONF_VARS and Namespaces.php.
  • ModifyNamespacesEvent can modify everything.

Impact 

Extensions can now register global Fluid namespaces in a dedicated configuration file Configuration/Fluid/Namespaces.php. The old TYPO3_CONF_VARS registration can be used for backwards compatibility.

Feature: #108539 - Introduce default theme "Camino" 

See forge#108539

Description 

A new default theme has been added. Its main purpose is to build new sites more rapidly in TYPO3 v14.

The name Camino (spanish "the way") was chosen as v14 development series, marking the first steps on the way to a glorious future for TYPO3.

It serves to show that a new site in TYPO3 can be set up within minutes, being customizable (at least in a limited way), without having to rely on any external library, and to avoid ANY error message for newcomers to TYPO3.

Camino will be packaged for new installations by default and can be activated for new sites alongside existing sites.

The theme shows off basic page structures as well as some default content elements - completely without any third-party dependencies nor requiring the "Fluid styled content" extension.

The rough structure of the theme:

  • Four different color schemes can be selected in the site settings
  • The main menu structure and footer structure can be configured on the root page inside the backend layout / colPos positions
  • Common content elements for a Hero area and regular content is available
  • Minimal configuration is handled in TypoScript

The theme is not meant to evolve within TYPO3, as it will be moved to TER/Packagist/GitHub in a separate repository in v15.0. In v15.x a new theme will be added with more modern features, and it will utilize features that will be added during v15.x development.

The theme is 100% optional and encapsulated - existing setups will have no interference.

The theme will be fine-tuned before the TYPO3 v14 LTS release, specific documentation for its features will be provided in the theme's documentation.

Installation 

For now, the theme is the same as a regular TYPO3 extension.

On fresh classic-mode installations, the theme will be enabled by default. A new site and a first page will be created, which can be used to insert content.

On Composer-mode installations, the package typo3/theme-camino needs to be required, and a fresh installation will also create the site and a first page.

For existing installations, the theme must be enabled first (depending on the TYPO3 setup) either via extension manager, or by requiring the Composer package.

Once the "extension" is activated, the steps to enable the Camino frontend are:

  • Create a new root page in the Content > Layout page tree. Be sure to edit the created page properties and enable Behavior > Use as Root Page .
  • This will automatically create a new Site. Check Sites > Setup to see the created Site. Edit that Site's properties. In General > Sets for this Site ensure that the Theme: Camino Site set is added as a dependency.
  • (Depending on the TYPO3 setup, TYPO3 caches might need to be cleared)
  • Then edit the created root page properties via Content > Layout again, and pick Camino: Start page from the tab Appearance > Backend Layout (this page only).
  • Now the Camino theme will be applied to the site. Content can be added in the specific columns, and sub-pages can be created (ensure to set the backend layout of subpages to the appropriate Camino backend layout, either Camino: Content page (full-width) or Camino: Content page (with sidebar).
  • A custom logo can be set in root page properties Appearance, just above the backend layout picker.
  • In Sites > Setup, the Site set configuration can be accessed to adjust the color scheme and further options.

Impact 

A default frontend theme is now available. It can be easily activated in the TYPO3 installation process, or also be enabled afterwards.

It is dependency-free and provides and utilizes site sets.

Feature: #108558 - Add original file name to SanitizeFileNameEvent 

See forge#108558

Description 

The PSR-14 event \TYPO3\CMS\Core\Resource\Event\SanitizeFileNameEvent does now also provide the original file name. Event listeners can use the original file name to perform custom string replacements (e.g. space to hyphen instead of underscore) for the sanitized file name.

Impact 

It is now possible to retrieve the original file name in the PSR-14 event \TYPO3\CMS\Core\Resource\Event\SanitizeFileNameEvent .

Feature: #108623 - Allow content element restrictions per colPos 

See forge#108623

Description 

Backend layouts have been extended with options to allow only configured types of content elements (referencing tt_content.CType with names like "text", "textmedia", "felogin_pi1" and so on) in backend layout columns ( colPos): The two keys allowedContentTypes and disallowedContentTypes add allow and deny lists on column level. These settings can be set with Page TSConfig based backend layouts, Database based backend layouts do not allow configuring this value at the moment, but this will be added soon.

Example for a backend layout with two rows and two columns configured using Page TSConfig:

mod.web_layout.BackendLayouts {
  exampleKey {
    title = Example
    config {
      backend_layout {
        colCount = 1
        rowCount = 2
        rows {
          1 {
            columns {
              1 {
                identifier = main
                name = Main content
                colPos = 0
                allowedContentTypes = header, textmedia
              }
              2 {
                identifier = right
                name = Panel right
                colPos = 1
                allowedContentTypes = my_custom_cta
              }
            }
          }
          2 {
            columns {
              1 {
                identifier footer
                name = Footer
                colpos = 2
                colspan = 2
                disallowedContentTypes = header
            }
        }
      }
    }
  }
}
Copied!

The implementation adapts the "New content element wizard" to show only allowed (or not disallowed) content elements types when adding a content element to a column. When editing records, the select boxes "Type" and Column position" are reduced to not allow invalid values based on the configuration. Similar logic is applied when moving and copying content elements.

The feature has been created with extension content_defender in mind. This extension by Nicole Hummel has been around for many years and found huge adoption rates within the community. In comparison to content_defender, the core configuration is slightly simplified and the core implementation does not provide the additional content_defender feature to restrict the number of elements per column ( maxitems).

The core implementation supports the content_defender syntax using the arrays allowed.CType and disallowed.CType. With the example below, allowed.CType is internally mapped to allowedContentTypes. When both allowed.CType and allowedContentTypes are given, allowed.CType is ignored.

mod.web_layout.BackendLayouts {
  exampleKey {
    config {
      backend_layout {
        rows {
          1 {
            columns {
              1 {
                allowed {
                  CType = header, textmedia
[...]
Copied!

Codewise, the PSR-14 event ManipulateBackendLayoutColPosConfigurationForPageEvent has been added. It allows manipulation of the calculated column configuration. It is marked @internal and thus needs to be used with care since it may change in the future without further note: The event is not dispatched as systematically as it should be, but refactoring the surrounding code can probably not be provided with TYPO3 v14 anymore. Extensions like ext:container however must be able to adapt column configuration with TYPO3 v14 already. The decisions was to provide an event, but to mark it internal for the time being, declaring it as "use at your own risk" if you know what you are doing and within extensions that set up proper automatic testing to find issues if the core changes internals.

Impact 

Backend layout columns can now restrict, which content element types are allowed or disallowed inside of it.

Feature: #108627 - Allow adding inline language domains to JavaScript 

See forge#108627

Description 

The new method PageRenderer->addInlineLanguageDomain() allows loading all labels from a language domain and making them available in JavaScript via the TYPO3.lang object.

The domain name follows the format extension.domain (e.g. core.common, core.modules.media). The language file is resolved automatically by the LanguageService, resolving to files like EXT:core/Resources/Private/Language/locallang_common.xlf and EXT:core/Resources/Private/Language/Modules/media.xlf.

See translation domain syntax for more details.

Labels are automatically prefixed with the domain name and accessible as TYPO3.lang['domain:key'], e.g. TYPO3.lang['core.common:notAvailableAbbreviation'].

Example 

EXT:my_extension/Classes/Controller/MyController.php
use TYPO3\CMS\Core\Page\PageRenderer;

final class MyController
{
    public function __construct(
        private readonly PageRenderer $pageRenderer,
    ) {}

    public function myAction(): void
    {
        // Load all labels from the 'myextension.frontend' domain
        $this->pageRenderer->addInlineLanguageDomain('myextension.frontend');
    }
}
Copied!

The labels are then available in JavaScript:

EXT:my_extension/Resources/Public/JavaScript/my-script.js
// Access a label from the domain
const label = TYPO3.lang['myextension.frontend:button.submit'];
Copied!

Impact 

Previously, it was possible to load entire language files using addInlineLanguageLabelFile() (which is still available), but labels were added without any prefix. This could lead to naming conflicts when multiple extensions used the same label keys, potentially overriding each other's translations.

With the new domain-based approach, all labels are automatically prefixed with the domain name (e.g. myextension.frontend:label.key). This provides a unified, namespaced access pattern that eliminates the risk of collisions between labels from different extensions or language files.

Feature: #108663 - Adjust visibility of form elements in Form Editor 

See forge#108663

Description 

The Form Editor now provides the ability to configure the visibility of form elements. This allows form administrators to control which form elements are displayed or hidden in the form, providing better control over the form structure and user experience.

Usage 

When editing a form element in the Form Editor, a new "Visibility" option is available in the element's configuration panel. This option allows you to:

  • Show the element (default behavior)
  • Hide the element

The visibility setting is stored in the form definition and is evaluated when the form is rendered on the frontend.

Example for Integrators 

If you want to extend your own form elements with the visibility feature, you need to add the following configuration to the element definition:

prototypes:
  standard:
    formElementsDefinition:
      CustomElement:
        formEditor:
          editors:
            # Choose a key / position according to your own needs
            240:
              identifier: enabled
              templateName: Inspector-CheckboxEditor
              label: formEditor.elements.FormElement.editor.enabled.label
              propertyPath: renderingOptions.enabled
Copied!

Impact 

This feature improves the usability of the Form Editor and makes it more accessible to non-technical users who need to manage form visibility.

Feature: #108694 - Context menu items to edit site configuration for pages 

See forge#108694

Description 

Two new context menu items have been added for pages that are site roots:

  • Edit Site: Opens the site configuration editor for the site associated with the page.
  • Edit Site Settings: Opens the site settings editor for the site.

These items appear directly after the "Edit" item in the context menu and are only visible to admin users on pages that have a site configuration.

Impact 

Admin users can now quickly access the site configuration and site settings directly from the context menu when right-clicking on a site root page. This improves the workflow for managing sites without needing to navigate to the Site Management module first.

Deprecation: #108086 - Raise deprecation error on using deprecated labels 

See forge#108086

Description 

Until now, localization labels marked as deprecated, for example by using the x-unused-since attribute in XLIFF 1.2 or the subState="deprecated" attribute in XLIFF 2.0, did not trigger a runtime warning. As a result, integrators and developers had no automatic way to detect deprecated labels that were still in use.

With this change, TYPO3 now triggers an E_USER_DEPRECATED error when a deprecated label is first written to the localization cache.

Impacted formats 

XLIFF 1.2 

<trans-unit id="deprecated_label" x-unused-since="4.5">
    <source>This label is deprecated</source>
</trans-unit>
Copied!

XLIFF 2.0 

<unit id="label5">
    <segment subState="deprecated">
        <source>This is label #5 (deprecated in English)</source>
    </segment>
</unit>
Copied!

Custom loaders 

When a label identifier ends with .x-unused, TYPO3 raises a deprecation warning if the label is referenced, regardless of whether the reference includes the .x-unused suffix.

Custom loaders can use this behaviour to also provide mechanisms to deprecate labels.

Fallback behaviour 

  • If a label is deprecated in the fallback language but is overridden in the current locale without a deprecation marker, no deprecation warning is raised.
  • If a label is deprecated only in the current locale, TYPO3 falls back to the default language and does not raise a deprecation warning.

A deprecation warning is triggered the first time a deprecated label is written to cache. Subsequent resolutions of the same label use the cached entry and do not trigger additional warnings until the cache is cleared.

The following usages emit a deprecation warning when a deprecated label is resolved.

LanguageService 

$this->languageService->sL('EXT:core/Resources/Private/Language/locallang.xlf:someDeprecation');
$this->languageService->sL('core.messages:someDeprecation');
Copied!

Fluid ViewHelper 

Usage of the f:translate ViewHelper in both Extbase and non-Extbase contexts:

<f:translate key="some_deprecation" domain="core.messages" />

<f:translate key="core.messages:some_deprecation" />

<f:translate key="EXT:core/Resources/Private/Language/locallang.xlf:some_deprecation" />
Copied!

The Extension Scanner does not detect the usage of deprecated localization labels. Developers must rely on runtime deprecation logs to identify these occurrences.

Impact 

Integrators and developers may encounter new deprecation warnings during runtime or in the deprecation log when deprecated localization labels are used. The warnings help identify and replace outdated labels before they are removed in a future TYPO3 version.

Affected installations 

All TYPO3 installations that use localization labels marked as deprecated are affected. This includes custom extensions, site packages, or integrations that still reference deprecated labels from system or extension language files.

When a custom extension or project defines a label whose identifier ends with .x-unused, that label is considered deprecated regardless of the loader used. Such usage is technically possible but generally unlikely.

Migration 

  1. Review the deprecation log for warnings related to localization labels. Note that deprecations are only written the first time a label is used after deleting the cache.
  2. Replace usages of deprecated labels with non-deprecated ones where possible.
  3. If required, override deprecated labels in a custom locale without a deprecation marker.
  4. Remove or update labels marked with x-unused-since in XLIFF 1.2 or with subState="deprecated" in XLIFF 2.0 when they are no longer needed.
  5. Avoid defining labels with identifiers ending in .x-unused.

Deprecation: #108524 - Fluid namespaces in TYPO3_CONF_VARS 

See forge#108524

Description 

Registering global namespaces for Fluid templates in TYPO3_CONF_VARS has been deprecated.

Impact 

Defined namespaces in $GLOBALS['TYPO3_CONF_VARS']['SYS']['fluid']['namespaces'] will no longer be registered in TYPO3 v15.

Affected installations 

Installations and extensions that use $GLOBALS['TYPO3_CONF_VARS']['SYS']['fluid']['namespaces'] to define additional global namespaces or extend existing global namespaces.

Migration 

Standard use cases, such as registering a new global namespace or extending an existing one, can be migrated to the dedicated Configuration/Fluid/Namespaces.php configuration file.

Before:

EXT:my_extension/ext_localconf.php
$GLOBALS['TYPO3_CONF_VARS']['SYS']['fluid']['namespaces']['myext'][] = 'MyVendor\\MyExtension\\ViewHelpers';
Copied!

After:

EXT:my_extension/Configuration/Fluid/Namespaces.php
<?php

return [
    'myext' => ['MyVendor\\MyExtension\\ViewHelpers'],
];
Copied!

See Feature: #108524 - Configuration file to register global Fluid namespaces for more details and examples.

Deprecation: #108667 - Deprecate CommandNameAlreadyInUseException 

See forge#108667

Description 

The exception \TYPO3\CMS\Core\Console\CommandNameAlreadyInUseException is unused within TYPO3 Core and has been deprecated.

Impact 

Creating a new instance of \TYPO3\CMS\Core\Console\CommandNameAlreadyInUseException will trigger a PHP deprecation message.

Affected installations 

TYPO3 installations with custom extensions using this exception.

Migration 

As the exception is unused in TYPO3 Core, there is no direct replacement. Extensions relying on this exception should implement their own exception if needed.

Important: #107971 - XLF files now use 2-space indentation 

See forge#107971

Description 

TYPO3 Core XLF (XLIFF) translation files now consistently use 2-space indentation instead of tabs. This aligns with the formatting used by Crowdin and by PHP's \DOMDocument. The .editorconfig file has been updated accordingly.

In addition to indentation, all XLF files have been normalized using a unified XML formatter. This ensures consistent XML declaration, attribute ordering, and whitespace structure across all XLF files in the Core.

Checking and normalizing XLF formatting 

A new script has been introduced to check and normalize XLF formatting.

Checking formatting via runTests.sh 

To check whether XLF files have correct formatting (dry-run, no modifications):

./Build/Scripts/runTests.sh -s normalizeXliff -n
Copied!

This command scans all XLF files in typo3/sysext/ and reports files that would be changed.

Normalizing XLF files via runTests.sh 

To normalize all XLF files in-place:

./Build/Scripts/runTests.sh -s normalizeXliff
Copied!

This command applies consistent indentation and XML normalization to all XLF files in typo3/sysext/.

Using the standalone script 

The script can also be run directly. Requires PHP 8.2+ with DOM and intl extensions enabled.

# Show help
./Build/Scripts/xliffNormalizer.php --help

# Check files only (dry-run)
./Build/Scripts/xliffNormalizer.php --root typo3/sysext --dry-run

# Normalize files in place
./Build/Scripts/xliffNormalizer.php --root typo3/sysext

# Check a custom directory
./Build/Scripts/xliffNormalizer.php --root path/to/xlf/files --dry-run
Copied!

Impact 

Extension developers should update their XLF files to use 2-space indentation and expect normalized XML formatting. The provided script can also be used within extensions to keep XLF files consistent with TYPO3 Core standards.

14.0 Changes 

Table of contents

Breaking Changes 

Features 

Deprecation 

Important 

Breaking: #17406 - Field "url" in table "pages" has been removed 

See forge#17406

Description 

The former page type "External link" has been renamed to "Link" and now fully supports all typolink capabilities. It now uses the field link.

Since the field url was only used by this former page type, it has been removed.

Impact 

Custom code that expects the field url to exist in the table pages will fail as the field is not found anymore.

Affected installations 

  • TYPO3 projects that used the former page type External URL to link to resources other than true external URLs, for example sections such as #abc.
  • TYPO3 projects with custom code that expects the field url to exist in the table pages.

Migration 

The upgrade wizard "Migrate links of pages of type link" automatically migrates pages of the former type External URL to the new page type Link.

It migrates all links that resolve to external URLs. If your project used the External URL field for other purposes — for example, to link sections such as #abc — you must migrate those links manually.

If your project contains custom code that expects the field url to exist in the table pages, you can reintroduce this field via a TCA override, for example:

EXT:my_extension/Configuration/TCA/Overrides/pages.php
<?php

defined('TYPO3') || die('Access restricted.');

$GLOBALS['TCA']['pages']['columns']['url'] = [
    'label' => 'External URL',
    'config' => [
        'type' => 'input',
        'size' => 50,
        'max' => 255,
        'required' => true,
        'eval' => 'trim',
        'softref' => 'url',
        'behaviour' => [
            'allowLanguageSynchronization' => true,
        ],
    ],
];

// Adding column to a existing or new palette and configure showitem,
// for a specific doktype the field is still required, for example:
$GLOBALS['TCA']['pages']['palettes]['custom_url'] = [
    'showitem' => 'url',
];
$GLOBALS['TCA']['pages']['types'][$customDokTypeValue] = [
    'showitem' => [
        --div--;core.form.tabs:general,
            doktype,
            --palette--;;title,
            --palette--;;custom_url,
    ],
];
Copied!

Adding it back at least ensures that the database field is not renamed and dropped by the Database Analyzer. In case TCA is not required while keeping the database field it could be added to a extension ext_tables.sql file.

Breaking: #33747 - Remove non-implemented sortable Collection logic 

See forge#33747

Description 

The \TYPO3\CMS\Core\Collection\SortableCollectionInterface has been removed from the TYPO3 Core.

This interface was never properly implemented and served no purpose in the codebase. It defined methods for sorting collections via callback functions and moving items within collections, but no concrete implementations existed.

The interface defined the following methods:

  • usort($callbackFunction) – for sorting a collection via a given callback function
  • moveItemAt($currentPosition, $newPosition = 0) – for moving items within the collection

Impact 

Any code that implements or references \SortableCollectionInterface will trigger a PHP fatal error.

Since this interface was never implemented in the TYPO3 Core and had no real-world usage, the impact should be minimal for most installations.

Affected installations 

Installations with custom extensions that implement or reference the \SortableCollectionInterface are affected.

Migration 

Remove any references to \SortableCollectionInterface from your code.

If you need sortable collection functionality, implement your own sorting logic directly in your collection classes, or use PHP's built-in array sorting functions such as usort(), uasort(), or uksort().

Breaking: #68303 - Make tt_content imagewidth/imageheight nullable 

See forge#68303

Description 

The default values of the fields imagewidth and imageheight in the tt_content table are now set to null.

This change removes the awkward UI behavior where the fields were previously set to 0 when no value was entered.

Impact 

Custom queries might fail if they expect the fields to be 0 instead of null.

Affected installations 

TYPO3 installations that rely on the imagewidth and imageheight fields of the tt_content table always being integers are affected.

Migration 

Use the "Media fields zero to null" upgrade wizard to update existing field values.

Also, modify your queries to handle null values instead of 0.

Breaking: #92434 - Use Record API in Page Module Preview Rendering 

See forge#92434

Description 

The Page Module preview rendering has been refactored to use the Record API internally instead of accessing raw database arrays. This affects both custom preview renderers that extend StandardContentPreviewRenderer and Fluid-based preview templates.

The method signature has changed for \TYPO3\CMS\Backend\Preview\StandardContentPreviewRenderer :

  • StandardContentPreviewRenderer->linkEditContent() now expects a RecordInterface object as the second $record parameter instead of an array

The \TYPO3\CMS\Backend\View\Event\PageContentPreviewRenderingEvent has also been updated:

  • PageContentPreviewRenderingEvent->getRecord() now returns a RecordInterface object instead of an array
  • PageContentPreviewRenderingEvent->setRecord() now expects a RecordInterface object instead of an array

Additionally, the @internal class \TYPO3\CMS\Backend\View\BackendLayout\Grid\GridColumnItem has been updated to work with Record objects:

  • The constructor of GridColumnItem now requires a RecordInterface object as the third $record parameter instead of an array
  • GridColumnItem->getRecord() now returns a RecordInterface object instead of an array
  • GridColumnItem->setRecord() now expects a RecordInterface object instead of an array
  • A new method GridColumnItem->getRow() has been added to access the raw database array if needed

For Fluid-based content element previews, the template variables have changed. Previously, all record fields were passed as individual variables to the Fluid template. Now, only a single {record} variable is passed, which is a RecordInterface object providing access to all record data through the Record API.

Using the {pi_flexform_transformed} variable in Fluid-based content element previews no longer works. The resolved flex form can be directly accessed on the RecordInterface object, for example via {record.pi_flexform}. The value is a FlexFormFieldValues object, which properly groups the fields by their sheets.

Impact 

Extensions that extend StandardContentPreviewRenderer and override the linkEditContent() method will need to update their method signature.

Extensions that access GridColumnItem->getRecord() expecting an array will need to update their code to work with RecordInterface objects.

Extensions using event listeners for PageContentPreviewRenderingEvent that access the record via getRecord() expecting an array will need to update their code to work with RecordInterface objects.

Custom Fluid templates for content element preview rendering must be updated to use the {record} variable instead of accessing individual field variables.

Affected installations 

All installations with extensions that:

  • Extend StandardContentPreviewRenderer and call or override the linkEditContent() method
  • Instantiate GridColumnItem or call GridColumnItem->getRecord() / GridColumnItem->setRecord()
  • Register event listeners for PageContentPreviewRenderingEvent
  • Use custom Fluid templates for content element preview rendering via PageTSconfig mod.web_layout.tt_content.preview.[recordType]

Migration 

For custom preview renderers extending StandardContentPreviewRenderer :

Update the method signature of linkEditContent() to accept a RecordInterface object:

Before (TYPO3 v13 and lower)
protected function linkEditContent(string $linkText, array $row, string $table = 'tt_content'): string
{
    $uid = (int)$row['uid'];
    $pid = (int)$row['pid'];
    // ...
}
Copied!
After (TYPO3 v14+)
use TYPO3\CMS\Core\Domain\RecordInterface;

protected function linkEditContent(string $linkText, RecordInterface $record): string
{
    $uid = $record->getUid();
    $pid = $record->getPid();
    $table = $record->getMainType();
    // ...
}
Copied!

For code working with GridColumnItem :

Before (TYPO3 v13 and lower)
$row = $columnItem->getRecord();
$uid = (int)$row['uid'];
$title = $row['header'];
Copied!
After (TYPO3 v14+)
use TYPO3\CMS\Backend\View\BackendLayout\Grid\GridColumnItem;
use TYPO3\CMS\Core\Domain\RecordInterface;

$record = $columnItem->getRecord();
$uid = $record->getUid();
$title = $record->has('header') ? $record->get('header') : '';

// Or if raw array access is needed:
$row = $columnItem->getRow();
$uid = (int)$row['uid'];
Copied!

For custom Fluid templates used for content element preview rendering:

Before (TYPO3 v13 and lower)
<h2>{header}</h2>
<p>{bodytext}</p>
<f:if condition="{image}">
    <p>Image UID: {image}</p>
</f:if>
Copied!
After (TYPO3 v14+)
<h2>{record.header}</h2>
<p>{record.bodytext}</p>
<f:if condition="{record.image}">
    <p>Image UID: {record.image.uid}</p>
</f:if>
Copied!

For flex form value rendering, there are two options:

Before (TYPO3 v13 and lower)
<h2>{header}</h2>
<p>{bodytext}</p>
<small>{pi_flexform_transformed.settings.welcome_header}</small>
<f:if condition="{image}">
    <p>Image UID: {image}</p>
</f:if>
Copied!
After (TYPO3 v14+)
<f:variable name="path" value="s_messages/settings" />
<small>{record.pi_flexform.{path}.welcome_header}</small>

<!-- or -->

<small>{record.pi_flexform.sheets.s_messages.settings.welcome_header}</small>
Copied!

Breaking: #97151 - Remove "Database Relations" backend module 

See forge#97151

Description 

The backend submodule Database Relations within DB Check provided information about potentially broken database relations. However, the information it displayed was very limited and barely helpful. In addition, the entire module and its code have not received any meaningful updates in recent years.

Due to this, the module has been removed.

Impact 

The module has been removed. Existing links and stored bookmarks will no longer work.

Affected installations 

All TYPO3 installations are affected.

Migration 

There is no migration available.

Breaking: #98070 - Remove eval method year 

See forge#98070

Description 

The eval method year was used to validate the value of a TCA field. Its implementation was never completed and simply cast the value to an integer.

As there is no clear definition of what a valid year value should be, the method has been removed without substitution.

Impact 

The value year has been removed from the list of supported eval options.

The TCA migration will trigger a deprecation log entry when building the final TCA.

Affected installations 

TYPO3 installations using old extensions that define custom TCA configurations with this option set are affected.

Migration 

Remove the year eval setting from your TCA configuration and use a TCA field type that better suits your needs.

// Use type "number" with an optional range restriction
'variant_a' => [
    'label' => 'My year',
    'config' => [
        'type' => 'number',
        'range' => [
            'lower' => 1990,
            'upper' => 2038,
        ],
        'default' => 0,
    ],
],

// Use a date field with an optional range restriction
'variant_b' => [
    'label' => 'My year',
    'config' => [
        'type' => 'datetime',
        'range' => [
            'lower' => gmmktime(0, 0, 0, 1, 1, 1990),
            'upper' => gmmktime(23, 59, 59, 12, 31, 2038),
        ],
        'nullable' => true,
    ],
],
Copied!

Breaking: #98239 - Removed "afterBuildingFinished" hook 

See forge#98239

Description 

The hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterBuildingFinished'] has been removed in favor of the more powerful PSR-14 events \TYPO3\CMS\Form\Event\BeforeRenderableIsAddedToFormEvent and \TYPO3\CMS\Form\Event\AfterFormIsBuiltEvent .

Impact 

Any hook implementation registered under this identifier will no longer be executed in TYPO3 v14.0 and later.

Affected installations 

TYPO3 installations with custom extensions that implement this hook are affected. The extension scanner reports such usages as a weak match.

Migration 

The hook has been removed without a deprecation phase to allow extensions to remain compatible with both TYPO3 v13 (using the hook) and v14+ (using the new events). Implementing the PSR-14 events provides the same or greater control over form rendering.

Use the BeforeRenderableIsAddedToFormEvent or AfterFormIsBuiltEvent to achieve the same functionality with the new event-based system.

Breaking: #101292 - Strong-typed PropertyMappingConfigurationInterface 

See forge#101292

Description 

Extbase's PropertyMappingConfigurationInterface is now fully typed with native PHP types.

Impact 

Existing implementations will no longer work without adjustment. According to the Liskov Substitution Principle, all implementations must follow the updated method signatures and type restrictions defined in the interface.

Affected installations 

TYPO3 installations with custom PHP code implementing a custom PropertyMappingConfiguration are affected. Such cases are rare.

Migration 

Add the required native PHP types to all custom implementations of the PropertyMappingConfigurationInterface to fulfill the updated interface definition.

Breaking: #101392 - getIdentifier() and setIdentifier() from AbstractFile removed 

See forge#101392

Description 

When using the PHP API of the File Abstraction Layer (FAL), several classes are involved in representing file objects.

In addition to the FileInterface , there is also the AbstractFile class, from which most file- related classes inherit.

To ensure stricter type consistency, the abstract class no longer implements the methods getIdentifier() and setIdentifier(). Implementing these methods is now the responsibility of each subclass.

The methods are now implemented in the respective concrete classes inheriting from AbstractFile .

Impact 

In the unlikely case that the TYPO3 File Abstraction Layer has been extended with custom PHP classes derived from AbstractFile , this change will cause a fatal PHP error, as the new abstract methods getIdentifier() and setIdentifier() must be implemented by the subclass.

Affected installations 

TYPO3 installations that include custom code extending the File Abstraction Layer are affected. Such cases are considered highly uncommon.

Migration 

Implement the two methods getIdentifier() and setIdentifier() in any custom file class extending AbstractFile .

This can also be implemented in older TYPO3 versions to ensure forward compatibility with TYPO3 v14 and later.

Breaking: #103141 - Use doctrine GUID type for TCA type=uuid 

See forge#103141

Description 

The TYPO3 Doctrine implementation can now handle the proper matching database type for UUID's.

Postgresql natively supports the UUID data type and is way faster than the prior VARCHAR(36) generated from the string type.

The Doctrine DBAL GUID type uses CHAR(26) as the fixed field column size for non-postgres databases, which is compatible as long as valid UUID values were persisted in the configured database table.

Impact 

TYPO3 database tables can now natively properly apply the suitable GUID column type when configured as TCA type=uuid.

A prerequisite for this is that you only have valid UUID's stored in the database table, otherwise the database update will report an error when applying migrations.

Affected installations 

Projects with database table columns set as TCA type=uuid.

Error are likely to occur, if invalid UUID data is stored in field columns configured with this type.

Migration 

Use the database analyzer to migrate the database fields. Invalid values are not migrated and need to be manually cleaned up in affected instances.

Breaking: #103910 - Change logout handling in EXT:felogin 

See forge#103910

Description 

The logout handling has been adjusted to correctly dispatch the PSR-14 event \TYPO3\CMS\FrontendLogin\Event\LogoutConfirmedEvent when a logout redirect is configured. The actionUri variable has been removed, and the logout template has been updated to reflect this change, including correct use of the noredirect functionality.

Impact 

The PSR-14 event LogoutConfirmedEvent is now correctly dispatched when a logout redirect is configured. Additionally, the noredirect parameter is now evaluated during logout.

Affected installations 

TYPO3 installations using EXT:felogin with a custom Fluid template for the logout form.

Migration 

The {actionUri} variable is no longer available and must be removed from custom templates.

Before:

Fluid template adjustment (before)
<!-- Before -->
<f:form action="login" actionUri="{actionUri}" target="_top" fieldNamePrefix="">
Copied!

After:

Fluid template adjustment (after)
<f:form action="login" target="_top" fieldNamePrefix="">
Copied!

The evaluation of the {noRedirect} variable must be added to the template:

Before:

Fluid template adjustment for noRedirect (before)
<div class="felogin-hidden">
    <f:form.hidden name="logintype" value="logout" />
</div>
Copied!

After:

Fluid template adjustment for noRedirect (after)
<div class="felogin-hidden">
    <f:form.hidden name="logintype" value="logout" />
    <f:if condition="{noRedirect} != ''">
        <f:form.hidden name="noredirect" value="1" />
    </f:if>
</div>
Copied!

Breaking: #103913 - Do not perform redirect in EXT:felogin logoutAction 

See forge#103913

Description 

Redirect handling for the logoutAction has been removed.

Impact 

The logoutAction no longer performs any configured redirect via plugin settings or GET parameters.

Affected installations 

TYPO3 installations relying on redirect handling within logoutAction are affected.

Migration 

No migration is required. The previous redirect logic in logoutAction() has been removed because it was incorrect: it ignored the showLogoutFormAfterLogin setting and could trigger an unintended redirect even when this option was enabled.

Valid redirects are already processed correctly by loginAction() and overviewAction(), so the faulty branch was removed without replacement.

Breaking: #104422 - Move GET parameters in sitemap into namespace 

See forge#104422

Description 

The names of the GET parameters used in the sitemap generated by EXT:seo have been changed from page and sitemap to tx_seo[page] and tx_seo[sitemap], respectively.

Impact 

Applications, routing configurations, and third-party tools that rely on the parameters being named page and sitemap will break.

Affected installations 

This affects installations that use the arguments page and sitemap or override the sitemap templates of EXT:seo.

Migration 

If the arguments are mapped in the routing configuration, the code needs to be slightly adapted. A working example:

routeEnhancers:
  Sitemap:
    type: Simple
    routePath: 'sitemap-type/{sitemap}'
    aspects:
      sitemap:
        type: StaticValueMapper
        map:
          pages: pages
          tx_news: tx_news
          my_other_sitemap: my_other_sitemap
    _arguments:
      sitemap: 'tx_seo/sitemap'
Copied!

If the templates in EXT:seo/Resources/Private/Templates/XmlSitemap/Index.xml have been modified, adjust the generated links to match the original ones.

If the URL to a single sitemap has been provided to a third-party tool such as a crawler or search engine, it must be re-added using the new URL.

Breaking: #105377 - Deprecated functionality removed 

See forge#105377

Description 

The following PHP classes that have previously been marked as deprecated with v13 have been removed:

The following PHP classes have been declared final:

The following PHP interfaces that have previously been marked as deprecated with v13 have been removed:

The following PHP interfaces changed:

  • \TYPO3\CMS\Backend\LoginProvider\LoginProviderInterface->modifyView() added (Deprecation entry)
  • \TYPO3\CMS\Backend\LoginProvider\LoginProviderInterface->render() removed (Deprecation entry)
  • \TYPO3\CMS\Core\PageTitle\PageTitleProviderInterface->setRequest() added forge#102817

The following PHP class aliases that have previously been marked as deprecated with v13 have been removed:

The following PHP class methods that have previously been marked as deprecated with v13 have been removed:

The following PHP static class methods that have previously been marked as deprecated for v13 have been removed:

The following methods changed signature according to previous deprecations in v13 at the end of the argument list:

  • \TYPO3\CMS\Backend\View\BackendLayout\DataProviderContext->__construct() - All arguments are now mandatory (Deprecation entry)
  • \TYPO3\CMS\Core\Imaging\IconFactory->getIcon() (argument 4 is now of type \TYPO3\CMS\Core\Imaging\IconState|null) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\AbstractFile->copyTo() (argument 3 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\AbstractFile->moveTo() (argument 3 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\AbstractFile->rename() (argument 2 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\FileInterface->rename() (argument 2 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\FileReference->rename() (argument 2 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\Folder->addFile() (argument 3 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\Folder->addUploadedFile() (argument 2 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\Folder->copyTo() (argument 3 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\Folder->moveTo() (argument 3 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\InaccessibleFolder->addFile() (argument 3 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\InaccessibleFolder->addUploadedFile() (argument 2 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\InaccessibleFolder->copyTo() (argument 3 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\InaccessibleFolder->moveTo() (argument 3 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\ResourceStorage->addFile() (argument 4 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\ResourceStorage->addUploadedFile() (argument 4 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\ResourceStorage->copyFile() (argument 4 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\ResourceStorage->copyFolder() (argument 4 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\ResourceStorage->moveFile() (argument 4 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\ResourceStorage->moveFolder() (argument 4 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Resource\ResourceStorage->renameFile() (argument 3 is now of type \TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior ) (Deprecation entry)
  • \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPlugin() (argument 2 $type and 3 $extensionKey have been dropped) (Deprecation entry)

The following public class properties have been dropped:

The following class property has changed/enforced type:

  • \TYPO3\CMS\Extbase\Mvc\Controller\ActionController->view (is now \TYPO3\CMS\Core\View\ViewInterface ) (Deprecation entry)

The following TypoScript options have been dropped or adapted:

The following user TSconfig options have been removed:

The following class constants have been dropped:

The following global option handling have been dropped and are ignored:

The following global variables have been changed:

  • $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['SecondDatabase']['driverMiddlewares']['driver-middleware-identifier'] must be an array, not a class string (Deprecation entry)

The following hooks have been removed:

  • $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList']['customizeCsvHeader'] (Deprecation entry)
  • $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList']['customizeCsvRow'] (Deprecation entry)

The following TCA options are not evaluated anymore:

  • $GLOBALS['TCA'][$table]['types']['subtype_value_field']
  • $GLOBALS['TCA'][$table]['types']['subtypes_addlist']
  • $GLOBALS['TCA'][$table]['types']['subtypes_excludelist']

The following extbase validator options have been removed:

The following fallbacks have been removed:

  • Accepting arrays returned by readFileContent() in Indexed Search external parsers (Deprecation entry)
  • Allowing instantiation of \TYPO3\CMS\Core\Imaging\IconRegistry in ext_localconf.php (Deprecation entry)
  • Accepting a comma-separated list of fields as value for the columnsOnly parameter (Deprecation entry)
  • Support for extbase repository magic findByX(), findOneByX() and countByX() methods (Deprecation entry)
  • Fluid view helpers that extend \TYPO3\CMS\Fluid\ViewHelpers\Form\AbstractFormFieldViewHelper should no longer register class attribute and should rely on attribute auto registration for the error class to be added correctly. (Deprecation entry)
  • The legacy backend entry point typo3/index.php has been removed along with handling of composer.json setting extra.typo3/cms.install-deprecated-typo3-index-php (Deprecation entry)

The following upgrade wizards have been removed:

  • Install extension "fe_login_mode" from TER
  • Migrate base and path to the new identifier property of the "sys_filemounts" table
  • Migrate site settings to separate file
  • Set workspace records in table "sys_template" to deleted
  • Migrate backend user and groups to new module names
  • Migrate backend groups "explicit_allowdeny" field to simplified format
  • Migrate sys_log entries to a JSON formatted value
  • Migrate storage and folder to the new folder_identifier property of the "sys_file_collection" table

The following row updater has been removed:

  • \TYPO3\CMS\Install\Updates\RowUpdater\SysRedirectRootPageMoveMigration

The following database table fields have been removed:

The following JavaScript modules have been removed:

The following JavaScript method behaviours have changed:

  • FormEngineValidation.markFieldAsChanged() always requires HTMLInputElement|HTMLTextAreaElement|HTMLSelectElement to be passed as first argument (Deprecation entry)
  • FormEngineValidation.validateField() always requires HTMLInputElement|HTMLTextAreaElement|HTMLSelectElement to be passed as first argument (Deprecation entry)

The following JavaScript method has been removed:

The following smooth migration for JavaScript modules have been removed:

  • @typo3/backend/page-tree/page-tree-element to @typo3/backend/tree/page-tree-element (Deprecation entry)

The following localization XLIFF files have been removed:

The following template files have been removed:

The following content element definitions have been removed:

Impact 

Using above removed functionality will most likely raise PHP fatal level errors, may change website output or crashes browser JavaScript.

Breaking: #105549 - Improved ISO8601 Date Handling in TYPO3 DataHandler 

See forge#105549

Description 

The DataHandler PHP API has been extended to support both qualified and unqualified ISO8601 date formats, in order to correctly process supplied timezone offsets when provided.

  • Qualified ISO8601: Includes an explicit timezone offset (for example, 1999-12-11T10:09:00+01:00 or 1999-12-11T10:09:00Z)
  • Unqualified ISO8601: Omits timezone offsets, representing LOCALTIME (for example, 1999-12-11T10:09:00)

Previously, TYPO3 incorrectly used qualified ISO8601 with Z (UTC+00:00) to denote LOCALTIME and applied the server's timezone offset, which led to misinterpretations when another timezone offset was provided, or when real UTC-0 was intended instead of LOCALTIME. Now, timezone offsets are accurately applied if supplied, and are based on server localtime if omitted.

TYPO3 will use unqualified ISO8601 dates internally for communication between FormEngine and the DataHandler API, ensuring timezone offsets are correctly processed – instead of being shifted – when supplied to the DataHandler API.

In essence, this means that existing workarounds for previously applied timezone offsets should be reviewed and removed.

Impact 

TYPO3 now provides accurate and consistent handling of ISO8601 dates, eliminating previous issues related to timezone interpretation and LOCALTIME representation.

Affected installations 

Installations with custom TYPO3 extensions that invoke the DataHandler API with data for type="datetime" fields are affected.

Migration 

Qualified ISO8601 dates with intended timezone offsets and \DateTimeInterface objects can now be passed directly to the DataHandler without requiring manual timezone adjustments.

An example of a previous workaround that added timezone offsets for the DataHandler:

Passing datetime data via DataHandler PHP API (before)
$myDate = new \DateTime('yesterday');
$this->dataHandler->start([
    'tx_myextension_mytable' => [
        'NEW-1' => [
            'pid' => 2,
            // A previous workaround added the localtime offset to supplied
            // dates, as it was subtracted by the DataHandler persistence
            // layer
            'mydatefield_1' => gmdate('c', $myDate->getTimestamp() + (int)date('Z')),
        ],
    ],
]);
Copied!

Previous timezone-shifting workarounds can be removed and replaced with more intuitive formats.

Passing datetime data via DataHandler PHP API (after)
$myDate = new \DateTime('yesterday');
$this->dataHandler->start([
    'tx_myextension_mytable' => [
        'NEW-1' => [
            'pid' => 2,
            // Pass \DateTimeInterface object directly
            'mydatefield_1' => $myDate,
            // Format as LOCALTIME
            'mydatefield_2' => $myDate->format('Y-m-d\TH:i:s'),
            // Format with timezone information
            // (offsets will be normalized to the persistence timezone
            // format: UTC for integer fields, LOCALTIME for native
            // DATETIME fields)
            'mydatefield_3' => $myDate->format('c'),
        ],
    ],
]);
Copied!

Breaking: #105686 - Avoid obsolete $charset in sanitizeFileName() 

See forge#105686

Description 

The interface \TYPO3\CMS\Core\Resource\Driver\DriverInterface has been updated.

The method signature

public function sanitizeFileName(string $fileName, string $charset = ''): string
Copied!

has been simplified to:

public function sanitizeFileName(string $fileName): string
Copied!

Implementing classes no longer need to handle a second argument.

Impact 

This change has little to no impact, since the main API caller - the Core class ResourceStorage - never passed a second argument. The default implementation, LocalDriver , has therefore always behaved as if handling UTF-8 strings.

Affected installations 

TYPO3 installations with custom File Abstraction Layer (FAL) drivers implementing DriverInterface may be affected.

Migration 

Implementing classes should drop support for the second argument. Retaining it does not cause a conflict with the interface, but the TYPO3 Core will never call sanitizeFileName() with a second parameter.

Breaking: #105695 - Simplified CharsetConverter 

See forge#105695

Description 

The following methods have been removed from \TYPO3\CMS\Core\Charset\CharsetConverter :

  • CharsetConverter->conv()
  • CharsetConverter->utf8_encode()
  • CharsetConverter->utf8_decode()
  • CharsetConverter->specCharsToASCII(), use CharsetConverter->utf8_char_mapping() instead
  • CharsetConverter->sb_char_mapping()
  • CharsetConverter->euc_char_mapping()

This removes most helper methods that implemented conversions between different character sets from the TYPO3 Core. The vast majority of websites now use UTF-8 and no longer require the expensive charset conversions previously provided by the Core framework.

Impact 

Calling any of the removed methods will trigger a fatal PHP error.

Affected installations 

The TYPO3 Core has not exposed any of this low-level functionality in upper layers such as TypoScript for quite some time. The removal should therefore have little to no impact on most installations.

The only cases that may be affected are import or export extensions that perform conversions between legacy character sets (for example, those in the EUC family). Affected extensions can mitigate this change by copying the TYPO3 v13 version of the class CharsetConverter , including the relevant files from core/Resources/Private/Charsets/csconvtbl/, into their own codebase.

The extension scanner will detect usages and classify them as weak matches.

Migration 

Avoid calling any of the removed methods. Extensions that still require this functionality should copy the necessary logic into their own codebase or use a third-party library.

This particular case has a direct substitution:

// Before
$charsetConverter->specCharsToASCII('utf-8', $myString);

// After
$charsetConverter->utf8_char_mapping($myString);
Copied!

Breaking: #105728 - Extbase backend modules not in page context rely on global TypoScript only 

See forge#105728

Description 

Configuration of Extbase-based backend modules can be done using frontend TypoScript.

The standard prefix in TypoScript to do this is module.tx_myextension. Extbase backend module controllers can typically retrieve their configuration using a call like: $configuration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_SETTINGS, 'myextension');

TypoScript itself is always bound to a page: The frontend must have either some rootline page with a sys_template record or a page that has a site

set, otherwise frontend rendering will terminate with an error message.

Extbase-based backend modules are sometimes bound to pages as well: They can have a rendered page tree configured in their module configuration and then receive the selected page UID within the request as GET parameter id.

Other Extbase-based backend modules, however, are not inside a page scope and do not render the page tree. Examples of such modules within the TYPO3 Core are the backend modules delivered by the form and beuser extensions.

Such Extbase-based backend modules without a page tree had a hard time calculating their relevant frontend TypoScript-based configuration: Since TypoScript is bound to pages, they looked for "the first" valid page in the page tree, and the first valid sys_template record to calculate their TypoScript configuration. This dependency on guesswork made final configuration of Extbase backend module configuration not in page context brittle, opaque, and clumsy.

TYPO3 v14 puts an end to this: Extbase backend modules without page context compile their TypoScript configuration from global TypoScript only and no longer calculating TypoScript by guessing "the first valid" page.

The key call to register such "global" TypoScript is the method ExtensionManagementUtility::addTypoScriptSetup() in ext_localconf.php files.

Impact 

Configuration of Extbase-based backend modules may change if their configuration is defined by the first valid page in the page tree. Configuration of such backend modules can no longer be changed by including TypoScript on the "first valid" page.

Affected installations 

Instances with Extbase-based backend modules without a page tree may be affected.

Migration 

Configuration of Extbase-based backend modules without a page tree must be supplied programmatically and made "global" by extending $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_setup'] using ExtensionManagementUtility::addTypoScriptSetup() within extensions’ ext_localconf.php files. The backend module of the form extension is a good example. Additional locations of extensions that deliver form YAML definitions are defined like this:

EXT:my_extension/ext_localconf.php
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

ExtensionManagementUtility::addTypoScriptSetup('
    module.tx_form {
        settings {
            yamlConfigurations {
                1732884807 = EXT:my_extension/Configuration/Yaml/FormSetup.yaml
            }
        }
    }
');
Copied!

Note it is also possible to use the method ExtensionManagementUtility::addTypoScriptConstants() to declare "global" TypoScript constants and to use them in the TypoScript shown above.

Breaking: #105733 - FileNameValidator no longer accepts custom regex in __construct() 

See forge#105733

Description 

The class FileNameValidator no longer accepts a custom file deny pattern in __construct(). The service is now stateless and can be injected without side effects.

Impact 

A custom partial regex passed as the first constructor argument when instantiating the service is now ignored. The service relies on $GLOBALS['TYPO3_CONF_VARS']['BE']['fileDenyPattern'] configuration and a hard-coded constant as a fallback.

Affected installations 

Instances with custom extensions using GeneralUtility::makeInstance(FileNameValidator::class, 'some-custom-pattern'); are affected. This is expected to be a very rare case.

Migration 

Extensions that need to be tested with custom patterns that cannot be declared globally using $GLOBALS['TYPO3_CONF_VARS']['BE']['fileDenyPattern'] should implement their own service for this purpose or inline the necessary code. The core implementation performing the check is only a few lines long.

Breaking: #105809 - AfterMailerInitializationEvent removed 

See forge#105809

Description 

The event \TYPO3\CMS\Core\Mail\Event\AfterMailerInitializationEvent has been removed. This event became obsolete with the introduction of the Symfony-based mailer in TYPO3 v10. It was only able to influence the TYPO3 Core mailer by calling the @internal method injectMailSettings() after the settings had already been determined. The event has been removed since it no longer had a meaningful use case.

Impact 

Event listeners registered for this event will no longer be triggered.

Affected installations 

This event has had little purpose since the switch to the Symfony-based mailer and is probably not used in most instances. The extension scanner will find usages.

Migration 

Check if this event can be substituted by reconfiguring $GLOBALS['TYPO3_CONF_VARS']['MAIL'] , or by listening for the event BeforeMailerSentMessageEvent instead.

Breaking: #105863 - Remove exposeNonexistentUserInForgotPasswordDialog setting in EXT:felogin 

See forge#105863

Description 

The TypoScript setting exposeNonexistentUserInForgotPasswordDialog has been removed in EXT:felogin.

Impact 

Using the TypoScript setting exposeNonexistentUserInForgotPasswordDialog has no effect anymore. The password recovery process in EXT:felogin now always shows the same message when a username or email address is submitted in the password recovery form.

Affected installations 

Websites using the TypoScript setting exposeNonexistentUserInForgotPasswordDialog in EXT:felogin are affected.

Migration 

The setting has been removed without replacement. It is possible to use the PSR-14 event SendRecoveryEmailEvent to implement similar functionality if absolutely necessary. From a security perspective, however, it is strongly recommended not to expose the existence of email addresses or usernames.

Breaking: #105920 - Folder->getSubFolder() throws FolderDoesNotExistException 

See forge#105920

Description 

An exception handling detail within the File Abstraction Layer (FAL) resource handling has been changed. When calling getSubFolder('mySubFolderName') on a Folder object, and if this subfolder does not exist, the specific \TYPO3\CMS\Core\Resource\Exception\FolderDoesNotExistException is now raised instead of the global \InvalidArgumentException.

Impact 

The change may affect extensions that directly or indirectly call Folder->getSubFolder() and expect a \InvalidArgumentException to be thrown.

Affected installations 

FolderDoesNotExistException does not extend \InvalidArgumentException. Code that currently expects a \InvalidArgumentException to be thrown needs to be adapted.

Migration 

The change is breaking for code that takes an "optimistic" approach like: "get the subfolder object, and if this throws, create one". Example:

try {
    $mySubFolder = $myFolder->getSubFolder('mySubFolder');
} catch (\InvalidArgumentException) {
    $mySubFolder = $myFolder->createFolder('mySubFolder');
}
Copied!

This should be changed to catch a FolderDoesNotExistException instead:

use TYPO3\CMS\Core\Resource\Exception\FolderDoesNotExistException;

try {
    $mySubFolder = $myFolder->getSubFolder('mySubFolder');
} catch (FolderDoesNotExistException) {
    $mySubFolder = $myFolder->createFolder('mySubFolder');
}
Copied!

Extensions that need to stay compatible with both TYPO3 v13 and v14 should catch both exceptions and should later avoid catching \InvalidArgumentException when v13 compatibility is dropped:

use TYPO3\CMS\Core\Resource\Exception\FolderDoesNotExistException;

try {
    $mySubFolder = $myFolder->getSubFolder('mySubFolder');
} catch (\InvalidArgumentException|FolderDoesNotExistException) {
    // @todo: Remove \InvalidArgumentException from catch list when
    //        TYPO3 v13 compatibility is dropped.
    $mySubFolder = $myFolder->createFolder('mySubFolder');
}
Copied!

Breaking: #106041 - TypoScript Extbase toggle config.tx_extbase.persistence.updateReferenceIndex removed 

See forge#106041

Description 

Extbase previously supported the TypoScript toggle config.tx_extbase.persistence.updateReferenceIndex to control whether the reference index should be updated when records are persisted.

It has become increasingly important that the reference index is always kept up to date, since an increasing number of TYPO3 Core components rely on current reference index data. Using the reference index at key points can improve read and rendering performance significantly.

This toggle has been removed. Reference index updating is now always enabled.

Impact 

The change may slightly increase database load, which can become noticeable when Extbase updates many records at once.

Affected installations 

Instances with extensions that write many records using the Extbase persistence layer may be affected.

Migration 

The TypoScript toggle config.tx_extbase.persistence.updateReferenceIndex should be removed from any extension codebase, as it is now ignored by Extbase.

Breaking: #106056 - Add setRequest and getRequest to Extbase ValidatorInterface 

See forge#106056

Description 

Custom validators implementing \TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface must now also implement the methods setRequest() and getRequest().

Impact 

Missing implementations of the methods setRequest() and getRequest() will now result in a PHP fatal error.

Affected installations 

TYPO3 installations with custom extensions implementing ValidatorInterface .

Migration 

The methods setRequest() and getRequest() must be implemented in affected validators.

If there is no need to directly implement ValidatorInterface , it is recommended to extend AbstractValidator , where both methods are already implemented.

Breaking: #106118 - Property DataHandler->storeLogMessages removed 

See forge#106118

Description 

The public property \TYPO3\CMS\Core\DataHandling\DataHandler->storeLogMessages has been removed without substitution. It should no longer be used by extensions.

Impact 

Setting or reading the property in an extension will now raise a PHP warning- level error.

Affected installations 

Instances with extensions that access this property are affected. This should be a very rare use case. No TYPO3 Extension Repository (TER) extensions were affected during verification. The extension scanner is configured to find usages as a weak match.

Migration 

The property has been removed. Any code setting or reading it from \TYPO3\CMS\Core\DataHandling\DataHandler instances should be removed. The DataHandler->log() method now always writes the given $details to the sys_log table.

Breaking: #106307 - Use stronger cryptographic algorithm for HMAC 

See forge#106307

Description 

TYPO3 now uses SHA3-256 for HMAC operations across multiple components, replacing the previously used MD5, SHA1, and SHA256 algorithms. SHA3-256 (Keccak) produces 64-character hexadecimal hashes compared to the 32 or 40 characters of the older algorithms.

The following components have been upgraded:

Component Previous HMAC New HMAC
cHash MD5 SHA3-256
Backend password recovery SHA1 SHA3-256
Frontend password recovery SHA1 SHA3-256
File dump controller SHA1 SHA3-256
Show image controller SHA1 SHA3-256
Backend form protection SHA1 SHA3-256
Extbase form request attributes SHA1 SHA3-256
Form extension request attributes SHA1 SHA3-256
Database session backend SHA256 SHA3-256
Redis session backend SHA256 SHA3-256

Database fields have been extended to accommodate the longer hash values (and would even support SHA3-512 with 128 hexadecimal characters in the future):

  • be_users.password_reset_token: 100 → 128 characters
  • fe_users.felogin_forgotHash: 80 → 160 characters (including additional timestamp details)

Impact 

The algorithm change has the following immediate effects:

URLs with HMAC tokens become invalid:

  • cHash parameters in frontend URLs are invalidated
  • File dump URLs (file downloads) require regeneration
  • Show image URLs require regeneration

Active password reset tokens expire:

  • Backend user password reset links in progress become invalid
  • Frontend user password reset links in progress become invalid
  • Users must request new password reset emails

Session handling:

  • Existing session identifiers will be regenerated on next user login
  • No immediate session invalidation occurs

Database schema:

  • Field lengths are automatically updated during upgrade
  • No data migration is required for existing records

Affected installations 

All installations upgrading to TYPO3 v14 are affected.

The impact varies based on usage:

  • High impact: installations with active password reset processes or cached frontend URLs with cHash parameters
  • Medium impact: installations using file dump or show image controllers with externally stored URLs
  • Low impact: all other installations (automatic migration on next use)

Migration 

Database schema updates:

Execute the database analyzer in the Install Tool or run vendor/bin/typo3 upgrade:run.

URLs and caching:

  • Frontend caches should be cleared to regenerate cHash values
  • File dump and show image URLs regenerate automatically on next access
  • External references to file or image URLs must be updated

Sessions:

No manual intervention is required. Sessions are automatically rehashed on next login.

Custom extensions:

If custom code uses HashService::hmac() directly, review whether the default SHA1 algorithm is still appropriate. Consider explicitly passing HashAlgo::SHA3_256 for new HMAC operations:

use TYPO3\CMS\Core\Crypto\HashAlgo;
use TYPO3\CMS\Core\Crypto\HashService;

$hash = $hashService->hmac($data, 'my-additional-secret', HashAlgo::SHA3_256);
Copied!

Breaking: #106405 - TypolinkBuilder signature changes 

See forge#106405

Description 

To enable dependency injection for TypolinkBuilder classes, several breaking changes were introduced to the TypolinkBuilder architecture.

The following breaking changes have been made:

  • The constructor of AbstractTypolinkBuilder has been removed. Extending classes can no longer rely on receiving ContentObjectRenderer and \TypoScriptFrontendController through the constructor.
  • All concrete TypolinkBuilder implementations now implement the new TypolinkBuilderInterface and use dependency injection via their constructors instead of extending AbstractTypolinkBuilder with constructor arguments.
  • The method signature of the main link-building method has changed from build(array &$linkDetails, string $linkText, string $target, array $conf) to buildLink(array $linkDetails, array $configuration, ServerRequestInterface $request, string $linkText = '').

Impact 

Custom TypolinkBuilder implementations extending AbstractTypolinkBuilder will fail with fatal errors due to the removed constructor and changed method signatures.

Extensions that instantiate TypolinkBuilder classes directly will also fail, as the constructor signatures have fundamentally changed to use dependency injection.

Affected installations 

TYPO3 installations with extensions that:

  • Create custom TypolinkBuilder classes extending AbstractTypolinkBuilder
  • Directly instantiate TypolinkBuilder classes in PHP code
  • Override or extend the build() method of TypolinkBuilder classes

Migration 

For custom TypolinkBuilder implementations:

  1. Implement TypolinkBuilderInterface
  2. Use dependency injection in the constructor for required services
  3. Replace the build() method with buildLink()

Note: Classes implementing TypolinkBuilderInterface are automatically configured as public services in the dependency injection container - no manual configuration is required.

Example migration:

Before (TYPO3 v13 and lower)
use TYPO3\CMS\Frontend\Typolink\AbstractTypolinkBuilder;

class MyCustomLinkBuilder extends AbstractTypolinkBuilder
{
    public function build(array &$linkDetails, string $linkText, string $target, array $conf): LinkResultInterface
    {
        // Custom link building logic
        return new LinkResult('news', $linkText);
    }
}
Copied!
After (TYPO3 v14+)
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Frontend\Typolink\TypolinkBuilderInterface;

class MyCustomLinkBuilder implements TypolinkBuilderInterface
{
    public function __construct(
        // Inject required dependencies
    ) {}

    public function buildLink(
        array $linkDetails,
        array $configuration,
        ServerRequestInterface $request,
        string $linkText = '',
    ): LinkResultInterface {
        // Custom link building logic - access ContentObjectRenderer via:
        $contentObjectRenderer = $request->getAttribute('currentContentObject');
        return new LinkResult('news', $linkText);
    }
}
Copied!

For code that instantiates TypolinkBuilder classes directly:

It is strongly recommended to use the LinkFactory instead of instantiating TypolinkBuilder classes directly. The LinkFactory handles proper instantiation and dependency injection automatically.

Breaking: #106412 - TCA interface settings for list view removed 

See forge#106412

Description 

Each TCA definition previously had an optional section named ['interface'], which defined parameters for displaying TCA records.

The last remaining options within this section, $GLOBALS['TCA'][$table]['interface']['maxSingleDBListItems'] and $GLOBALS['TCA'][$table]['interface']['maxDBListItems'] , have been removed. As a result, the entire ['interface'] section is no longer supported and will be ignored.

These settings were used to define the number of table rows displayed within the Content > List backend module.

Impact 

The $GLOBALS['TCA'][$table]['interface'] section in TCA definitions is no longer evaluated.

Setting any values under this key in custom extensions has no effect and will be automatically removed during build time.

Affected installations 

TYPO3 installations with custom TCA settings defining $GLOBALS['TCA'][$table]['interface'] are affected.

Migration 

Visual display settings can still be overridden on a per-user or per-page basis via TSconfig. This approach is more flexible, as it allows rendering different numbers of items per site, page tree, or user group.

The TCA option $GLOBALS['TCA'][$table]['interface']['maxSingleDBListItems'] has been removed in favor of mod.web_list.itemsLimitSingleTable.

The TCA option $GLOBALS['TCA'][$table]['interface']['maxDBListItems'] has been removed in favor of mod.web_list.itemsLimitPerTable.

Breaking: #106503 - Removal of fields from sys_file_metadata 

See forge#106503

Description 

The following database fields and corresponding TCA definitions have been removed from the database table sys_file_metadata without substitution:

  • visible
  • fe_groups

These fields were added to the table when the system extension EXT:filemetadata was installed.

Although the field names suggested access control functionality similar to those found in other TYPO3 tables, they were never configured to restrict frontend or backend access. Their intended meaning would have depended on custom implementation and was not supported by the TYPO3 Core.

Additionally, the field status has been moved from the Access tab to the Metadata tab to avoid the impression that this field has any restrictive behavior.

Impact 

The fields visible and fe_groups in sys_file_metadata are no longer used. After performing a Database Compare in the Install Tool, these columns will be removed and their data permanently lost.

Accessing these fields in PHP or TypoScript will result in PHP warnings.

Affected installations 

Any TYPO3 installation using the fields visible or fe_groups provided by the system extension EXT:filemetadata is affected.

Migration 

No automatic migration is available.

If the fields are still required for custom logic, reintroduce their database columns and TCA configuration within a custom extension.

Breaking: #106596 - Remove legacy form templates 

See forge#106596

Description 

In earlier TYPO3 versions, the Form Framework provided two template variants for frontend rendering:

  • The initial, legacy templates in EXT:form/Resources/Private/Frontend/Templates and EXT:form/Resources/Private/Frontend/Partials, which were deprecated in forge#95456
  • The newer, Bootstrap 5 compatible and accessible templates in EXT:form/Resources/Private/FrontendVersion2/Templates and EXT:form/Resources/Private/FrontendVersion2/Partials, introduced with forge#94868

The legacy form templates have now been removed. The rendering option templateVariant, which toggled the template and form configuration variant, has been removed as well.

The newer template variants have been moved to the original file paths of the legacy templates.

Impact 

The removal of the legacy templates and the templateVariant configuration option simplifies the Form Framework rendering logic.

Developers no longer need to choose between multiple template variants, reducing complexity and improving maintainability. Projects already using the newer templates benefit from a cleaner configuration and a unified rendering approach.

Affected installations 

All TYPO3 installations using the Form Framework are affected.

Migration 

If you still rely on the legacy templates, you must migrate your templates and partials to the structure of the newer templates.

Websites that use templateVariant: version2 can simplify their form configuration. Variants with the condition 'getRootFormProperty("renderingOptions.templateVariant") == "version2"' are no longer necessary and can be removed.

Before:

prototypes:
  standard:
    formElementsDefinition:
      Text:
        variants:
          -
            identifier: template-variant
            condition: 'getRootFormProperty("renderingOptions.templateVariant") == "version2"'
            properties:
              containerClassAttribute: 'form-element form-element-text mb-3'
              elementClassAttribute: form-control
              elementErrorClassAttribute: ~
              labelClassAttribute: form-label
Copied!

After:

prototypes:
  standard:
    formElementsDefinition:
      Text:
        properties:
          containerClassAttribute: 'form-element form-element-text mb-3'
          elementClassAttribute: form-control
          elementErrorClassAttribute: ~
          labelClassAttribute: form-label
Copied!

Breaking: #106863 - TCA control option is_static removed 

See forge#106863

Description 

The TCA control option $GLOBALS['TCA'][$table]['ctrl']['is_static'] has been removed, as it is no longer evaluated by the TYPO3 Core.

Originally, this option was introduced to mark certain database tables (for example, from static_info_tables) as containing static, non-editable reference data. Over time, the TYPO3 ecosystem has evolved, and the original purpose of is_static has become obsolete.

Modern TYPO3 installations rarely rely on static data tables. Better mechanisms now exist for managing read-only or reference data, such as the TCA options readOnly and editlock, or backend access control. Removing this legacy option improves maintainability and reduces complexity for newcomers.

Impact 

The option is_static is no longer evaluated. It is automatically removed at runtime by a TCA migration, and a deprecation log entry is generated to indicate where adjustments are required.

Affected installations 

All TYPO3 installations defining $GLOBALS['TCA'][$table]['ctrl']['is_static'] in their TCA configuration are affected.

Migration 

Remove the is_static option from the ctrl section of your TCA configuration.

Breaking: #106869 - Remove static function parameter in AuthenticationService 

See forge#106869

Description 

The method \TYPO3\CMS\Core\Authentication\AuthenticationService::processLoginData() no longer accepts the parameter $passwordTransmissionStrategy. Additionally, the method now declares a strict return type.

Impact 

Authentication services extending or overriding AuthenticationService and its method processLoginData() (or a subtype such as processLoginDataBE() or processLoginDataFE()) will no longer receive the $passwordTransmissionStrategy parameter.

Affected installations 

TYPO3 installations with custom authentication services that extend AuthenticationService and implement or override processLoginData() or one of its subtypes.

Migration 

Extensions extending AuthenticationService must remove the $passwordTransmissionStrategy parameter from their method signature and add the strict return type bool|int.

Extensions implementing subtype methods such as processLoginDataBE() or processLoginDataFE() must also remove the parameter, as it is no longer passed to these methods.

Breaking: #106949 - Duplicate doktype restriction configuration removed 

See forge#106949

Description 

The TSconfig option mod.web_list.noViewWithDokTypes has been removed, as it duplicated the existing configuration TCEMAIN.preview.disableButtonForDokType.

Since forge#96861, the latter has been established as the single source of truth for disabling the View button for specific doktype values.

Impact 

The option mod.web_list.noViewWithDokTypes is no longer evaluated. Only the configuration TCEMAIN.preview.disableButtonForDokType is now respected.

Affected installations 

TYPO3 installations that still rely on mod.web_list.noViewWithDokTypes in Page TSconfig to control the visibility of the View button in backend modules are affected.

Migration 

Remove any usage of mod.web_list.noViewWithDokTypes from Page TSconfig.

Use the existing configuration TCEMAIN.preview.disableButtonForDokType instead:

EXT:site_package/Configuration/TSconfig/Page/TCEMAIN.tsconfig
TCEMAIN.preview.disableButtonForDokType = 199, 254
Copied!

This change ensures consistent behavior and avoids duplicate configuration.

Breaking: #106964 - Enable "Light/Dark Mode" context awareness for CKEditor RTE by default 

See forge#106964

Description 

With forge#105640, context awareness for the CKEditor Rich Text Editor (RTE) was introduced, allowing the editor interface to automatically adapt to the user’s system-wide light or dark mode preference.

This feature is now enabled by default. The TYPO3 Core stylesheet EXT:rte_ckeditor/Resources/Public/Css/contents.css has been updated to support light and dark mode variants automatically. Previously fixed white backgrounds now adapt dynamically based on the editor’s preferred color scheme.

Note that this change affects only the backend editor interface. The display of RTE content in the frontend remains unaffected.

Impact 

The previously fixed light mode user interface of the CKEditor RTE is now context-aware, displaying content in light or dark mode according to the editor’s system preference.

Affected installations 

TYPO3 installations that rely on a fixed light mode presentation of CKEditor RTE instances in the backend are affected.

Migration 

Installations with custom CKEditor modifications should review their contents.css file. If the TYPO3 Core default stylesheet was previously used, and a fixed light mode appearance is desired, this can be enforced in the RTE YAML configuration:

EXT:my_extension/Configuration/RTE/MyCKPreset.yaml
editor.config.contentsCss:
  - "EXT:my_extension/Resources/Public/Css/CustomContents.css"
Copied!

Breaking: #106972 - TCA control option searchFields removed 

See forge#106972

Description 

The TCA control option $GLOBALS['TCA'][$table]['ctrl']['searchFields'] has been removed.

With the introduction of the Schema API and the \TYPO3\CMS\Core\Schema\SearchableSchemaFieldsCollector component, the handling of fields included in searches has changed. By default, all fields of suitable types, such as input or text, are now automatically considered searchable.

To manually define searchable fields, use the new searchable field configuration option within a field's TCA configuration. See the full list of supported field types here. Unsupported field types (such as file, inline, etc.) are not considered searchable and do not support the searchable option.

Impact 

The searchFields option is no longer evaluated. It is automatically removed at runtime through a TCA migration, and a deprecation log entry is generated to highlight where adjustments are required.

If suitable fields are detected that were not listed in the removed searchFields option, they are automatically set to searchable => false to preserve previous behavior.

Affected installations 

All installations that define searchFields in their TCA configuration.

Migration 

Remove the searchFields option from the ctrl section of your TCA configuration.

If needed, use the searchable option in individual field definitions to control which fields are included in the search functionality.

Breaking: #106976 - Removal of TCA search field configuration options 

See forge#106976

Description 

The following TCA field-level search configuration options have been removed:

  • search.case
  • search.pidonly
  • search.andWhere

These options were originally intended to customize backend record search behavior but have proven to be of little practical value:

  • They were not used in the TYPO3 Core,
  • They were not used in common third-party extensions,
  • Their behavior was unclear and inconsistently supported,
  • They were insufficiently documented and hard to validate.

This removal is part of the ongoing effort to simplify and streamline TCA configuration and reduce unnecessary complexity for integrators.

Impact 

These options are no longer evaluated. They are automatically removed at runtime through a TCA migration, and a deprecation log entry is generated to highlight where adjustments are required.

Affected installations 

Any installation or extension that defines one or more of these options in its TCA field configuration:

Example of removed TCA options
 'my_field' => [
     'config' => [
         'type' => 'input',
-        'search' => [
-            'case' => true,
-            'pidonly' => true,
-            'andWhere' => '{#CType}=\'text\'',
-        ],
     ],
 ],
Copied!

Migration 

Remove the obsolete search options from your TCA field configurations.

Breaking: #107047 - Remove pointer field functionality of TCA flex 

See forge#107047

Description 

One of the main features of TCA is the concept of record types. This allows using a single table for different purposes and in different contexts. The most well-known examples are the "Page Types" of the pages table and the "Content Types" of the tt_content table. For every specific type, it is possible to define which fields to display and to customize their configuration.

A special case historically has been plugin registration, which for a long time used the so-called subtypes feature of TCA. This was an additional layer below record types, configured using subtype_value_field (commonly list_type), and optionally subtypes_addlist and subtypes_excludelist to add or remove fields depending on the selected subtype.

FlexForms attached to such subtypes were configured using ds_pointerField (typically pointing to list_type,CType). This came in combination with the corresponding ds configuration, which was an array with keys combining the pointer fields, for example:

'ds_pointerField' => 'list_type,CType',
'ds' => [
    'news_pi1,list' => 'FILE:EXT:news/Configuration/FlexForm.xml',
    'default' => 'FILE:...'
],
Copied!

Over recent TYPO3 versions, this approach has been deprecated in favor of using record types exclusively for plugin registration via the CType field, making configuration cleaner and easier to understand.

The special plugin content element (CType=list) and the corresponding plugin subtype field list_type have been deprecated in Deprecation: #105076 - Plugin content element and plugin sub types and removed in Breaking: #105377 - Deprecated functionality removed. See also Important: #105538 - Plugin subtypes removed: Changes to configurePlugin() and TCA handling for related information about ExtensionUtility::configurePlugin() and ExtensionManagementUtility::addTcaSelectItemGroup().

With this change, support for ds_pointerField and the multi-entry ds array format has now been removed. The ds option now points to a single FlexForm, either directly or via a FILE: reference.

FlexForms must instead be assigned via standard types configuration using columnsOverrides.

This also affects the data structure identifier, which in the commonly used tca type is the dataStructureKey. It is now set to default if the table does not support record types or no record type-specific configuration exists. Otherwise, the dataStructureKey is set to the corresponding record type value, for example textpic.

This change affects the following PSR-14 events:

  • AfterFlexFormDataStructureIdentifierInitializedEvent
  • AfterFlexFormDataStructureParsedEvent
  • BeforeFlexFormDataStructureIdentifierInitializedEvent
  • BeforeFlexFormDataStructureParsedEvent

A fallback for TYPO3 v14 resolves comma-separated dataStructureKey values (for example, list_type,CType) to CType.

To address circular dependencies during schema building, FlexFormTools now supports both TCA Schema objects and raw TCA configuration arrays as input. The following methods accept a union type array|TcaSchema for the new $schema parameter:

  • getDataStructureIdentifier()
  • parseDataStructureByIdentifier()
  • cleanFlexFormXML()

Previously, these methods relied on $GLOBALS['TCA'] internally, which caused architectural issues. They now operate directly on the provided schema.

All calls to these methods should provide the $schema parameter with either a TcaSchema instance or a raw TCA configuration array. Since data structure resolution can be customized by extensions, the parameter is not strictly mandatory, but it is strongly recommended to provide it in most cases. An InvalidTcaSchemaException will be thrown if schema resolution is required but no schema is passed.

This change also enables components like RelationMapBuilder to use FlexFormTools during schema building, even when only raw TCA is available.

For further details on the enhanced FlexFormTools functionality, see Feature: #107047 - FlexForm enhancements: Direct plugin registration and raw TCA support.

The following class has been removed as it is no longer required:

  • \InvalidCombinedPointerFieldException

Impact 

FlexForm Pointer Field Removal

Any TCA definition that still uses ds_pointerField or a ds array with multiple entries (for example news_pi1,list) will no longer work and might cause rendering errors.

FlexFormTools Schema Parameter

All code calling FlexFormTools methods ( getDataStructureIdentifier(), parseDataStructureByIdentifier(), cleanFlexFormXML()) must be updated to provide the required $schema parameter.

Affected installations 

FlexForm Pointer Field Removal

All installations using ds_pointerField (as the pointer field functionality has been removed entirely) or an array-like structure for ds in their TCA field type flex configuration.

FlexFormTools Schema Parameter

All installations with custom code that directly call FlexFormTools methods without providing the $schema parameter. This includes custom extensions or TYPO3 Core patches using these methods.

A TCA migration automatically converts single-entry ds arrays. Multi-entry definitions require manual migration, as they must be aligned with the correct record type configuration, which may require additional configuration changes beforehand.

Example for single-entry migration:

Before:

Migration of single-entry ds configuration (before)
'ds' => [
    'default' => '<T3DataStructure>...',
],
Copied!

After:

Migration of single-entry ds configuration (after)
'ds' => '<T3DataStructure>...',
Copied!

Migration 

FlexForm Pointer Field Migration

Before:

'ds_pointerField' => 'list_type,CType',
'ds' => [
    'news_pi1,list' => 'FILE:EXT:news/Configuration/FlexForm.xml',
    'default' => '<T3DataStructure>...',
],
Copied!

After:

'columns' => [
    'pi_flexform' => [
        'config' => [
            'ds' => '<T3DataStructure>...',
        ],
    ],
],
'types' => [
    'news_pi1' => [
        'columnsOverrides' => [
            'pi_flexform' => [
                'config' => [
                    'ds' => 'FILE:EXT:news/Configuration/FlexForm.xml',
                ],
            ],
        ],
    ],
],
Copied!

If no columnsOverrides is defined, the default ds value of the field configuration will be used as before.

FlexFormTools Schema Parameter Migration

Before:

use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
$identifier = $flexFormTools->getDataStructureIdentifier(
    $fieldTca,
    'tt_content',
    'pi_flexform',
    $row
);
Copied!

After:

use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);

// Option 1: Using TCA Schema object (recommended for normal usage)
$tcaSchemaFactory = GeneralUtility::makeInstance(TcaSchemaFactory::class);
$tcaSchema = $tcaSchemaFactory->get('tt_content');
$identifier = $flexFormTools->getDataStructureIdentifier(
    $fieldTca,
    'tt_content',
    'pi_flexform',
    $row,
    $tcaSchema
);

// Option 2: Using raw TCA array (for schema building contexts)
$rawTca = $fullTca['tt_content'];
$identifier = $flexFormTools->getDataStructureIdentifier(
    $fieldTca,
    'tt_content',
    'pi_flexform',
    $row,
    $rawTca
);
Copied!

Breaking: #107229 - Removed support for annotations in Extbase 

See forge#107229

Description 

Extbase no longer supports PHP annotations for models, data transfer objects (DTOs), and controller actions. Use PHP attributes (for example #[Extbase\Validate]) instead. Attributes provide the same functionality with better performance and native language support.

Extbase previously relied on annotation parsing, typically detected when the annotation namespace was imported, for example:

use TYPO3\CMS\Extbase\Annotation as Extbase;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class MyModel extends AbstractEntity
{
    /**
     * @Extbase\Validate("NotEmpty")
     */
    protected string $foo = '';
}
Copied!

Since TYPO3 now requires PHP 8.2 as the minimum version, the use of native PHP attributes is preferred. All Extbase-related annotations have been available as PHP attributes since TYPO3 v12.

Impact 

By dropping support for Extbase annotations, only PHP attributes are now supported. This provides more type safety and better integration with PHP’s language features. Developers benefit from a cleaner, faster, and more reliable implementation without the need for the deprecated third-party annotation parser.

Affected installations 

All Extbase models, Data Transfer Objects (DTOs), and controllers that use Extbase annotations are affected. This includes built-in annotations such as Cascade, Lazy, and Validate, as well as any custom annotation implementations.

Migration 

Switch from Extbase annotations to native PHP attributes.

Before (with annotations) 

use TYPO3\CMS\Extbase\Annotation as Extbase;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class MyModel extends AbstractEntity
{
    /**
     * @Extbase\Validate("NotEmpty")
     */
    protected string $foo = '';
}
Copied!

After (with PHP attributes) 

use TYPO3\CMS\Extbase\Attribute as Extbase;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class MyModel extends AbstractEntity
{
    #[Extbase\Validate(['validator' => 'NotEmpty'])]
    protected string $foo = '';
}
Copied!

Breaking: #107310 - Remove FreezableBackendInterface 

See forge#107310

Description 

The interface \TYPO3\CMS\Core\Cache\Backend\FreezableBackendInterface has been removed from the TYPO3 Core.

It previously defined the following methods:

  • freeze() — Freezes the cache backend.
  • isFrozen() — Returns whether the backend is frozen.

Impact 

Any code implementing or referencing \FreezableBackendInterface will now trigger a PHP fatal error.

Since this interface was never implemented in the TYPO3 Core and had no known real-world usage, the overall impact is expected to be minimal.

Affected installations 

Installations with custom extensions that implement or reference the \FreezableBackendInterface are affected.

Migration 

Remove any references to \FreezableBackendInterface from your extension code.

If you require freeze functionality, implement the desired behavior directly in your custom cache backend class.

Breaking: #107323 - Workspace "Freeze Editing" removed 

See forge#107323

Description 

The workspace record flag Freeze Editing has been removed without replacement, effectively removing this feature.

The functionality had only been partially implemented and exhibited several usability and conceptual issues. It was therefore decided to remove it entirely in favor of more reliable and flexible workspace configuration options.

Impact 

"Freezing" a workspace to prevent editing of records is no longer possible.

Affected installations 

Since Freeze Editing did not provide any visible feedback to editors, it was likely used only rarely.

During the database compare, the field freeze will be removed from the table sys_workspace, effectively "unfreezing" all previously frozen workspaces.

Migration 

A more robust alternative to Freeze Editing is to configure a custom workspace stage and assign responsible persons. In combination with the following workspace options:

  • Publish only content in publish stage
  • Restrict publishing to workspace owners

you can create a workflow in which only designated members or groups are permitted to move records into the Ready to publish stage and/or perform publishing actions.

While other members can still edit records, such edits will automatically reset the workspace stage back to Editing, restarting the review cycle for those changes.

Breaking: #107324 - Streamline PSR-7 Response Header Handling 

See forge#107324

Description 

The handling of PSR-7 response headers in TYPO3 Core and Extbase has been unified. Previously, different mechanisms caused inconsistent behavior:

  • Extbase only kept the last value of a header, discarding all previous values (e.g. only one Set-Cookie header was possible).
  • Core allowed multiple Set-Cookie headers, but merged all other headers with multiple values into a single comma-separated string. According to RFC 9110, this is only valid for headers that explicitly support comma-separated lists.

With this change, TYPO3 now preserves multiple header values by default. Each value is emitted as a separate header line, while single values remain a single-line header.

Impact 

  • Multiple header values are now always emitted as multiple header lines.
  • Extbase and Core responses can now properly emit multiple headers with the same name (e.g. Set-Cookie, WWW-Authenticate, Link, xkey).
  • Extensions that relied on the old merging or overwriting behavior may need to be adapted.

Affected installations 

Installations are affected if they:

  • Relied on headers being merged into a comma-separated string.
  • Relied on only the last header value being retained in Extbase responses.

Migration 

If your use case requires merged header values, you must now implement this explicitly:

use TYPO3\CMS\Core\Http\Response;

$response = new Response();
$values = ['foo', 'bar', 'baz'];
$response = $response->withHeader('X-Foo-Bar', implode(', ', $values));
Copied!

If your use case requires that only the last header value is retained, you must also handle this explicitly in your code:

use TYPO3\CMS\Core\Http\Response;

$response = new Response();
$values = ['foo', 'bar', 'baz'];
$response = $response->withHeader('X-Foo-Bar', end($values));
Copied!

Note: There is another edge case not affected by this change. Multiple Extbase plugins still cannot set multiple header values with the same name (for example, two Extbase plugins both setting a Set-Cookie header). In this case, the latter will override the former. Installations affected by this scenario should resolve it by adding their own middleware.

Breaking: #107343 - Removed "beforeFormCreate" hook 

See forge#107343

Description 

The hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeFormCreate'] has been removed in favor of the PSR-14 event BeforeFormIsCreatedEvent , which provides a more powerful and flexible way to influence form creation.

Impact 

Any hook implementation registered under $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeFormCreate'] is no longer executed in TYPO3 v14.0 and later.

Affected installations 

TYPO3 installations with custom extensions using this hook are affected.

The Extension Scanner will report such usages as a weak match.

Migration 

The hook has been removed without prior deprecation. This allows extensions to remain compatible with both TYPO3 v13 (using the hook) and v14+ (using the new event) simultaneously.

Use the PSR-14 event BeforeFormIsCreatedEvent to extend or modify form creation behavior.

Breaking: #107356 - Use Record API in List Module 

See forge#107356

Description 

The Content > List backend module has been refactored to use the \RecordInterface and related Record API internally instead of working with raw database row arrays.

This modernization introduces stricter typing and improves data consistency. As a result, several public method signatures have been updated.

The following public methods in DatabaseRecordList have changed their signatures:

  • renderListRow() now expects a RecordInterface object instead of an array as the second parameter.
  • makeControl() now expects a RecordInterface object instead of an array as the second parameter.
  • makeCheckbox() now expects a RecordInterface object instead of an array as the second parameter.
  • languageFlag() now expects a RecordInterface object instead of an array as the second parameter.
  • makeLocalizationPanel() now expects a RecordInterface object instead of an array as the second parameter.
  • linkWrapItems() now expects a RecordInterface object instead of an array as the fourth parameter.
  • getPreviewUriBuilder() now expects a RecordInterface object instead of an array as the second parameter.
  • isRecordDeletePlaceholder() now expects a RecordInterface object instead of an array.
  • isRowListingConditionFulfilled() has dropped the first parameter $table and now expects a RecordInterface object instead of an array.

These changes enable the List module to operate on structured Record objects, providing better type safety, consistency, and a foundation for further modernization of the backend record handling.

Impact 

Code that calls these methods directly must be updated to pass \RecordInterface objects instead of database row arrays.

Affected installations 

TYPO3 installations with custom extensions that:

  • Extend or XCLASS DatabaseRecordList and override any of the affected methods.
  • Call the affected methods directly with array-based record data.

Migration 

When calling affected methods, use the Record API to create a Record object from a database row:

Before:

Migrating from array-based record handling to the Record API (before)
$databaseRecordList->renderListRow($table, $rowArray, $indent, $translations, $enabled);
Copied!

After:

Migrating from array-based record handling to the Record API (after)
use TYPO3\CMS\Backend\RecordList\DatabaseRecordList;
use TYPO3\CMS\Backend\Record\RecordFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$recordFactory = GeneralUtility::makeInstance(RecordFactory::class);
$record = $recordFactory->createResolvedRecordFromDatabaseRow($table, $rowArray);
$databaseRecordList->renderListRow($table, $record, $indent, $translations, $enabled);
Copied!

Breaking: #107380 - Removed "beforeFormDuplicate" hook 

See forge#107380

Description 

The hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeFormDuplicate'] has been removed in favor of the more powerful PSR-14 event BeforeFormIsDuplicatedEvent .

Impact 

Implementations of the removed hook are no longer executed in TYPO3 v14.0 and later.

Affected installations 

TYPO3 installations with custom extensions using this hook. The extension scanner reports any usage as a weak match.

Migration 

The hook has been removed without deprecation to allow extensions to remain compatible with both TYPO3 v13 (using the hook) and TYPO3 v14+ (using the new event). When implementing the event as well, no further deprecations will occur.

Use the PSR-14 Event to achieve the same or greater functionality.

Breaking: #107382 - Removed "beforeFormDelete" hook 

See forge#107382

Description 

The hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeFormDelete'] has been removed in favor of the more powerful PSR-14 event \TYPO3\CMS\Form\Event\BeforeFormIsDeletedEvent .

Impact 

Implementations of the removed hook are no longer executed in TYPO3 v14.0 and later.

Affected installations 

TYPO3 installations with custom extensions using this hook. The extension scanner reports any usage as a weak match.

Migration 

The hook has been removed without deprecation to allow extensions to remain compatible with both TYPO3 v13 (using the hook) and TYPO3 v14+ (using the new event). When implementing the event as well, no further deprecations will occur.

Use the PSR-14 Event to achieve the same or greater functionality.

Breaking: #107388 - Removed "beforeFormSave" hook 

See forge#107388

Description 

The hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeFormSave'] has been removed in favor of the more powerful PSR-14 event \TYPO3\CMS\Form\Event\BeforeFormIsSavedEvent .

Impact 

Implementations of the removed hook are no longer executed in TYPO3 v14.0 and later.

Affected installations 

TYPO3 installations with custom extensions using this hook. The extension scanner reports any usage as a weak match.

Migration 

The hook has been removed without deprecation to allow extensions to remain compatible with both TYPO3 v13 (using the hook) and TYPO3 v14+ (using the new event). When implementing the event as well, no further deprecations will occur.

Use the PSR-14 Event to achieve the same or greater functionality.

Breaking: #107397 - Circular dependency between ProcessedFile and Task removed 

See forge#107397

Description 

The circular dependency between \TYPO3\CMS\Core\Resource\ProcessedFile and File Processing Task classes has been resolved to improve the architecture and maintainability of the File Abstraction Layer (FAL) processing system.

The following changes have been made to ProcessedFile :

  • The public method getTask() has been removed.
  • The public method generateProcessedFileNameWithoutExtension() has been removed.

The following changes have been made to ProcessedFileRepository :

  • Method add() now requires a TaskInterface parameter.
  • Method update() now requires a TaskInterface parameter.

Additionally, the checksum validation logic has been moved from ProcessedFile to AbstractTask .

Impact 

Any code that calls the following methods will cause PHP fatal errors:

  • ProcessedFile->getTask()
  • ProcessedFile->generateProcessedFileNameWithoutExtension()

Any code that calls ProcessedFileRepository->add() or ProcessedFileRepository->update() without the new TaskInterface parameter will cause PHP fatal errors.

Code that relied on TYPO3CMSCoreResourceProcessedFile objects having Task objects available internally will no longer work, as Task objects are now created externally by FileProcessingService when needed.

Affected installations 

Installations with custom file processing extensions or custom Task implementations that directly interact with the ProcessedFile->getTask() method are affected. The extension scanner will report any usage of ProcessedFile->getTask() and ProcessedFile->generateProcessedFileNameWithoutExtension() as weak matches.

Extensions that manually call ProcessedFileRepository->add() or ProcessedFileRepository->update() are also affected. The extension scanner will not report usages of these methods due to too many weak matches.

Migration 

Replace calls to ProcessedFile->getTask() with direct creation of Task objects through the TaskTypeRegistry :

Before:

$task = $processedFile->getTask();
Copied!

After:

use TYPO3\CMS\Core\Resource\Processing\TaskTypeRegistry;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$taskTypeRegistry = GeneralUtility::makeInstance(TaskTypeRegistry::class);
$task = $taskTypeRegistry->getTaskForType(
    $processedFile->getTaskIdentifier(),
    $processedFile,
    $processedFile->getProcessingConfiguration()
);
Copied!

It is recommended to implement your own alternative to ProcessedFile->generateProcessedFileNameWithoutExtension() if similar logic is still needed.

Update calls to ProcessedFileRepository->add() and ProcessedFileRepository->update() to include the new TaskInterface parameter:

Updated calls to ProcessedFileRepository
use TYPO3\CMS\Core\Resource\Processing\TaskTypeRegistry;
use TYPO3\CMS\Core\Resource\ProcessedFileRepository;
use TYPO3\CMS\Core\Utility\GeneralUtility;

// Before
$processedFileRepository->add($processedFile);
$processedFileRepository->update($processedFile);

// After
$taskTypeRegistry = GeneralUtility::makeInstance(TaskTypeRegistry::class);
$task = $taskTypeRegistry->getTaskForType(
    $processedFile->getTaskIdentifier(),
    $processedFile,
    $processedFile->getProcessingConfiguration()
);

$processedFileRepository = GeneralUtility::makeInstance(ProcessedFileRepository::class);
$processedFileRepository->add($processedFile, $task);
$processedFileRepository->update($processedFile, $task);
Copied!

Breaking: #107403 - Remove LocalPreviewHelper + LocalCropScaleMaskHelper 

See forge#107403

Description 

The helper classes for preview and CropScaleMask (CSM) image generation prevented further unification of the File Abstraction Layer (FAL) image processing API.

The two helper classes \TYPO3\CMS\Core\Resource\Processing\LocalPreviewHelper and \TYPO3\CMS\Core\Resource\Processing\LocalCropScaleMaskHelper have been removed. Their functionality has been merged into \TYPO3\CMS\Core\Resource\Processing\LocalImageProcessor .

These helper classes existed for historical reasons and were never intended to be part of the public API.

Impact 

Any code that extends or references \LocalPreviewHelper or \LocalCropScaleMaskHelper will now trigger PHP fatal errors.

Affected installations 

Installations with custom extensions that extend or reference either \LocalPreviewHelper or \LocalCropScaleMaskHelper are affected and will cause PHP fatal errors.

These helper classes were meant to be internal but were never declared as such. Implementations using them directly instead of the LocalImageProcessor should be very rare.

Migration 

Remove all references to \LocalPreviewHelper or \LocalCropScaleMaskHelper from your code.

Use the LocalImageProcessor directly instead, or implement a custom image processor that executes before this processor to apply additional functionality.

Breaking: #107436 - Localization system architecture changes 

See forge#107436

Description 

The TYPO3 localization system has been migrated to use the Symfony Translation components internally (see Feature: #107436 - Symfony Translation Component integration), which introduces several breaking changes to the internal API and configuration handling.

This change affects only the internal processing of translation ("locallang") files. The public API of the localization system remains unchanged.

Method Signature Changes

The method \TYPO3\CMS\Core\Localization\LocalizationFactory::getParsedData() has a modified signature and behavior:

Before:

public function getParsedData(
    $fileReference,
    $languageKey,
    $_ = null,
    $__ = null,
    $isLocalizationOverride = false
)
Copied!

After:

public function getParsedData(string $fileReference, string $languageKey): array
Copied!

It now only returns the parsed and combined localization data instead of both the default and the localized data. To obtain the default (English) data, call getParsedData($fileReference, 'en').

Language Key Changes

Internally, the fallback language key has been changed from default to en. This does not affect public API usage, where default can still represent any configured language key other than English.

Configuration Changes

Custom parser configuration is no longer supported. The global configuration option $GLOBALS['TYPO3_CONF_VARS']['SYS']['lang']['parser'] has been removed, and any custom parser registered through it will be ignored.

Several configuration options have been moved or renamed:

  • $GLOBALS['TYPO3_CONF_VARS']['SYS']['lang']['parser']
  • $GLOBALS['TYPO3_CONF_VARS']['SYS']['lang']['requireApprovedLocalizations'] $GLOBALS['TYPO3_CONF_VARS']['LANG']['requireApprovedLocalizations']
  • $GLOBALS['TYPO3_CONF_VARS']['SYS']['lang']['format'] $GLOBALS['TYPO3_CONF_VARS']['LANG']['format']
  • $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['lang']['availableLanguages'] $GLOBALS['TYPO3_CONF_VARS']['LANG']['availableLocales']
  • $GLOBALS['TYPO3_CONF_VARS']['SYS']['locallangXMLOverride'] $GLOBALS['TYPO3_CONF_VARS']['LANG']['resourceOverrides']

Impact 

Code calling \TYPO3\CMS\Core\Localization\LocalizationFactory::getParsedData() with the old signature or expecting a multi-dimensional array structure will break:

  • The method now enforces strict parameter types.
  • The method returns en instead of default for English translations.
  • Unused parameters have been removed.

Extensions that register custom localization parsers using $GLOBALS['TYPO3_CONF_VARS']['SYS']['lang']['parser'] will no longer have their parsers executed, potentially leading to missing translations.

Instances using the moved configuration options must update their configuration to the new paths. The old options are no longer recognized and will be ignored.

Affected installations 

Installations that:

  • Call LocalizationFactory::getParsedData() directly.
  • Register custom localization parsers via the removed configuration option.
  • Use any of the renamed or moved configuration options listed above.

Migration 

Method Call Updates

Update all calls to getParsedData() to use the new signature and ensure parameter types are correct:

Before:

Updated getParsedData() usage
$data = $factory->getParsedData(
    $fileReference,
    $languageKey,
    null,
    null,
    false
)[$languageKey];
Copied!

After:

$data = $factory->getParsedData($fileReference, $languageKey);
Copied!

Custom Parser Migration

Replace any custom localization parser with a Symfony Translation loader. See Feature: #107436 - Symfony Translation Component integration for detailed migration instructions to the new loader system.

Breaking: #107438 - Default parseFunc configuration for Fluid Styled Content 

See forge#107438

Description 

Since TYPO3 v13, the basic lib.parseFunc and lib.parseFunc_RTE configurations are always available through EXT:frontend/ext_localconf.php.

The default parseFunc configuration previously provided by EXT:fluid_styled_content has been removed to avoid duplicated and outdated settings. This unifies the parseFunc behavior between EXT:frontend and EXT:fluid_styled_content.

The allowTags syntax is no longer set by default, as HTML sanitization has been handled properly by the HTML Sanitizer for some time. All HTML tags are now allowed by default in frontend output, while the HTML Sanitizer controls which tags and attributes are ultimately permitted.

Note that the allowTags directive itself has not been removed. It can still be set to restrict frontend output where desired.

The conceptual approach is:

  • The backend (RTE and its HTML parser/processing) already cleans unwanted content and controls what is stored in the database.
  • The frontend output (parseFunc) should only add additional restrictions through allowTags when content comes from external or untrusted sources (for example, custom Extbase output).

Impact 

Custom TypoScript configurations using allowTags syntax may no longer work as expected. Specifically:

  • allowTags := addToList(wbr) no longer appends wbr. Instead, it limits allowed tags to only wbr.
  • Default CSS classes on HTML elements (for example <table class="contenttable">) are no longer automatically added by the parseFunc configuration.
  • The parseFunc configuration in EXT:fluid_styled_content no longer provides custom link-handling options such as extTarget and keep.

Affected installations 

TYPO3 installations that use:

  • Custom TypoScript configurations relying on prior default allowTags behavior.
  • Extensions or sites depending on the specific parseFunc configuration previously provided by EXT:fluid_styled_content.
  • Configurations expecting automatic CSS class additions to HTML elements.
  • Sites relying on the old external link-handling behavior from the Fluid Styled Content parseFunc.

Migration 

If you need to allow specific HTML tags, explicitly configure allowTags instead of extending a former default.

Before (no longer works):

lib.parseFunc_RTE.allowTags := addToList(wbr)
Copied!

After:

lib.parseFunc_RTE.allowTags = b,span,i,em,wbr,...
Copied!

For custom CSS classes on HTML elements, use custom CSS or add them through Fluid templates or TypoScript processing instead.

If you require the previous link-handling behavior, configure it explicitly:

lib.parseFunc_RTE {
    makelinks {
        http {
            extTarget = _blank
            keep = path
        }
    }
}
Copied!

Breaking: #107443 - Migrate Modal component from Bootstrap to native dialog 

See forge#107443

Description 

The TYPO3 Modal component has been migrated from Bootstrap's modal implementation to use the native HTML <dialog> element. This improves accessibility, reduces bundle size, and removes the dependency on Bootstrap's JavaScript for modal functionality.

As part of this migration, Bootstrap's native modal events (such as show.bs.modal, shown.bs.modal, hide.bs.modal, and hidden.bs.modal) are no longer dispatched.

Likewise, direct Bootstrap modal API usage (for example new Modal() from Bootstrap or $(element).modal()) is no longer supported.

Impact 

Bootstrap modal events ( *.bs.modal) are no longer available.

Extensions listening to these events must migrate to TYPO3's custom modal events.

The Modal component now uses the native <dialog> element with updated CSS classes and DOM structure.

Any direct manipulation of Bootstrap modal APIs will no longer work.

Extensions using data-bs-toggle="modal", data-bs-content="...", or data-bs-target attributes to trigger modals must migrate to TYPO3's Modal API.

Affected installations 

All installations with custom extensions that:

  • Listen to Bootstrap modal events ( show.bs.modal, shown.bs.modal, hide.bs.modal, hidden.bs.modal)
  • Use Bootstrap's modal JavaScript API directly (for example new bootstrap.Modal())
  • Use jQuery to control modals (for example $(element).modal('show'))
  • Use data-bs-toggle="modal" or data-bs-content="..." attributes
  • Manipulate modal DOM structures or classes expecting Bootstrap markup

Migration 

Event migration 

Replace Bootstrap modal event listeners with TYPO3's custom modal events. Event listeners must be attached to the modal instance returned by the Modal API, not queried from the DOM.

Before:

const modalElement = document.querySelector('.modal');
modalElement.addEventListener('show.bs.modal', (event) => {
  console.log('Modal is about to be shown');
});
modalElement.addEventListener('shown.bs.modal', (event) => {
  console.log('Modal is now visible');
});
modalElement.addEventListener('hide.bs.modal', (event) => {
  console.log('Modal is about to be hidden');
});
modalElement.addEventListener('hidden.bs.modal', (event) => {
  console.log('Modal is now hidden');
});
Copied!

After:

import Modal from '@typo3/backend/modal';
import Severity from '@typo3/backend/severity';

const modal = Modal.show(
  'My Modal Title',
  'This is the modal content',
  Severity.info
);

modal.addEventListener('typo3-modal-show', (event) => {
  console.log('Modal is about to be shown');
});
modal.addEventListener('typo3-modal-shown', (event) => {
  console.log('Modal is now visible');
});
modal.addEventListener('typo3-modal-hide', (event) => {
  console.log('Modal is about to be hidden');
});
modal.addEventListener('typo3-modal-hidden', (event) => {
  console.log('Modal is now hidden');
});
Copied!

Bootstrap API migration 

Replace Bootstrap modal API calls with TYPO3's Modal API.

Do not instantiate Bootstrap modals or manipulate modal DOM elements directly.

Before:

import { Modal } from 'bootstrap';

const modalElement = document.querySelector('.modal');
const bsModal = new Modal(modalElement);
bsModal.show();
bsModal.hide();
Copied!

After:

import Modal from '@typo3/backend/modal';
import Severity from '@typo3/backend/severity';

// Show a simple modal
Modal.show(
  'My Modal Title',
  'This is the modal content',
  Severity.info,
  [
    {
      text: 'Close',
      btnClass: 'btn-default',
      trigger: (event, modal) => modal.hideModal()
    }
  ]
);

// Dismiss the current modal
Modal.dismiss();
Copied!

Data attribute migration 

Replace Bootstrap's data-bs-toggle and data-bs-target attributes with TYPO3's modal trigger API.

Before:

<button type="button"
        data-bs-toggle="modal"
        data-bs-target="#myModal"
        data-bs-content="Are you sure?">
  Open Modal
</button>
Copied!

After:

<button type="button"
        class="t3js-modal-trigger"
        data-title="Confirmation"
        data-content="Are you sure?"
        data-severity="warning"
        data-button-close-text="Cancel"
        data-button-ok-text="Confirm">
  Open Modal
</button>
Copied!

Alternatively, use the JavaScript API directly:

import Modal from '@typo3/backend/modal';
import Severity from '@typo3/backend/severity';

document.querySelector('button').addEventListener('click', (event) => {
  Modal.confirm(
    'Confirmation',
    'Are you sure?',
    Severity.warning
  );
});
Copied!

Breaking: #107473 - TypoScript condition function getTSFE() removed 

See forge#107473

Description 

The TypoScript condition function getTSFE() has been removed.

After various properties like getTSFE().type were already removed in TYPO3 v13, the remaining parts of this functionality have now been removed in TYPO3 v14.

The most common remaining use was accessing the current page ID via getTSFE().id, which can be replaced with request.getPageArguments().getPageId().

Impact 

Conditions using getTSFE() will no longer evaluate to true and must be updated.

Affected installations 

Instances with TypoScript conditions that use the function getTSFE() are affected.

Migration 

Replace getTSFE() with an equivalent condition. For example:

Before:

[getTSFE() && getTSFE().id == 42]
Copied!

After:

[request?.getPageArguments()?.getPageId() == 42]
Copied!

Breaking: #107482 - Environment::getComposerRootPath method removed 

See forge#107482

Description 

The following method in \TYPO3\CMS\Core\Core\Environment has been removed in TYPO3 v14.0:

  • Environment::getComposerRootPath()

Since composer installers v4/v5 (which are required since TYPO3 v12), getComposerRootPath() and getProjectPath() return the same value, because the project path can no longer be changed through configuration.

Therefore, the method Environment::getComposerRootPath() has been removed. It was marked as internal from the beginning.

Impact 

Calling this method will result in a PHP error.

Affected installations 

TYPO3 installations with custom extensions or custom code that directly call the removed method are affected:

  • Environment::getComposerRootPath()

The extension scanner will report any usage as a strong match.

Migration 

Instead of calculating relative paths manually, use absolute paths or the appropriate TYPO3 APIs for path handling.

Use the following replacement:

  • Environment::getProjectPath()

Breaking: #107488 - Scheduler frequency options moved to TCA 

See forge#107488

Description 

The global configuration array $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['frequencyOptions'] , which was used to define frequency options for scheduler tasks, has been removed.

Frequency options are now configured directly in the TCA using the overrideFieldTca mechanism on the tx_scheduler_task.execution_details field.

This change improves consistency with TYPO3’s configuration patterns and provides better extensibility for scheduler task timing options.

Impact 

Extensions that previously added custom frequency options through the global frequencyOptions array will no longer see their custom options in the scheduler task frequency field.

Code that relied on reading the global frequencyOptions configuration will no longer work as expected.

Affected installations 

All installations with extensions providing custom scheduler frequency options through the global configuration array are affected.

Migration 

Extensions should migrate their frequency options from the global configuration to TCA overrides.

Before (no longer working):

// EXT:my_extension/ext_localconf.php

$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['frequencyOptions']['0 2 * * *'] =
    'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:daily_2am';
Copied!

After (recommended approach):

// EXT:my_extension/Configuration/TCA/Overrides/tx_scheduler_task.php

$GLOBALS['TCA']['tx_scheduler_task']['columns']['execution_details']['config']['overrideFieldTca']['frequency']['config']['valuePicker']['items'][] = [
    'value' => '0 2 * * *',
    'label' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:daily_2am',
];
Copied!

Migration for multiple options:

// EXT:my_extension/Configuration/TCA/Overrides/tx_scheduler_task.php

$customFrequencyOptions = [
    [
        'value' => '0 2 * * *',
        'label' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:daily_2am',
    ],
    [
        'value' => '0 */6 * * *',
        'label' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:every_6_hours',
    ],
    [
        'value' => '0 0 1 * *',
        'label' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:monthly_first',
    ],
];

$GLOBALS['TCA']['tx_scheduler_task']['columns']['execution_details']['config']['overrideFieldTca']['frequency']['config']['valuePicker']['items'] = array_merge(
    $GLOBALS['TCA']['tx_scheduler_task']['columns']['execution_details']['config']['overrideFieldTca']['frequency']['config']['valuePicker']['items'] ?? [],
    $customFrequencyOptions
);
Copied!

Breaking: #107507 - Removed EXT:form AbstractFinisher->getTypoScriptFrontendController() 

See forge#107507

Description 

The method \TYPO3\CMS\Form\Domain\Finishers\AbstractFinisher->getTypoScriptFrontendController() has been removed.

Since the entire TypoScriptFrontendController class is being phased out, this abstract helper method has been removed as part of that cleanup.

Impact 

Calling this method in a custom EXT:form finisher will result in a fatal PHP error.

Affected installations 

TYPO3 instances using EXT:form with custom finishers that call this method are affected. The extension scanner is configured to detect such usages.

Migration 

Migration depends on what the finisher previously did with the returned class instance. The TypoScriptFrontendController properties and helper methods have been modernized, with most data now available as request attributes.

For example, accessing the cObj property can be replaced like this:

use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;

$cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$cObj->setRequest($request);
$cObj->start(
    $request->getAttribute('frontend.page.information')->getPageRecord(),
    'pages'
);
Copied!

Breaking: #107518 - Removed "initializeFormElement" hook 

See forge#107518

Description 

The hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['initializeFormElement'] has been removed in favor of the PSR-14 event \TYPO3\CMS\Form\Event\BeforeRenderableIsAddedToFormEvent .

Impact 

Hook implementations registered under initializeFormElement are no longer executed in TYPO3 v14.0 and later.

Affected installations 

TYPO3 installations with custom extensions using this hook are affected. The extension scanner reports any usage as a weak match.

Migration 

The hook was removed without a deprecation phase to allow extensions to work with both TYPO3 v13 (using the hook) and TYPO3 v14+ (using the new event) simultaneously.

Use the PSR-14 event instead to influence form element initialization. Since the event is dispatched at a later point, it allows more extensive modifications than the previous hook.

Breaking: #107528 - Removed "beforeRemoveFromParentRenderable" hook 

See forge#107528

Description 

The hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeRemoveFromParentRenderable'] has been removed in favor of the PSR-14 event \TYPO3\CMS\Form\Event\BeforeRenderableIsRemovedFromFormEvent .

Impact 

Hook implementations registered under beforeRemoveFromParentRenderable are no longer executed in TYPO3 v14.0 and later.

Affected installations 

TYPO3 installations with custom extensions using this hook are affected. The extension scanner reports any usage as a weak match.

Migration 

The hook was removed without a deprecation phase to allow extensions to work with both TYPO3 v13 (using the hook) and TYPO3 v14+ (using the new event) simultaneously.

Use the PSR-14 event instead to allow greater influence over the form processing workflow. Since the event is dispatched at a later point, it allows more extensive modifications than the previous hook.

Breaking: #107537 - Changes in URL generation of system resources 

See forge#107537

Description 

The following changes are considered breaking, although their impact is expected to be very low.

  • TypoScript getData function path previously returned a relative URL and now returns an absolute URL (prepended with absRefPrefix).
  • Access to FAL storages via relative path (fileadmin/templates/main.css) is limited to the default storage defined in $GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir'] .
  • All generated system resource URLs now include cache busting.
  • Adding custom query strings to resource identifiers no longer disables cache busting — both are now applied.

getText "path" in TypoScript 

EXT:my_extension/Configuration/TypoScript/setup.typoscript
page.20 = TEXT
page.20 {
    data = path : EXT:core/Resources/Public/Icons/Extension.svg
}
Copied!
Result (TYPO3 classic mode – note the leading "/")
"path" result before: typo3/sysext/core/Resources/Public/Icons/Extension.svg
"path" result now: /typo3/sysext/core/Resources/Public/Icons/Extension.svg
Copied!
Result (TYPO3 Composer mode – note the leading "/")
"path" result before: _assets/5f237792cbcdc97cfceade1e16ea33d7/Icons/Extension.svg
"path" result now: /_assets/5f237792cbcdc97cfceade1e16ea33d7/Icons/Extension.svg
Copied!

Relative path to FAL Storage 

Referencing resources via relative path does only work for the default FAL storage defined in $GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir'] . This means if you have other local FAL storages configured, you need to use the FAL resource syntax (e.g. FAL:42:/path/to/file.css) to reference files in such storage instead of using relative paths (e.g. my-other-fileadmin/path/to/file.css). It is generally recommended to use explicit resource identifiers (App resources or FAL resources), instead of relative paths.

All generated URLs now contain cache busting 

All generated resource URLs now include cache busting. For example, icon URLs that previously had no cache busting will now contain a cache-busting query string.

Additional query strings applied to the resource identifier 

When adding custom query strings to resource identifiers, TYPO3 previously disabled cache busting.

Now, both the custom query string and the cache-busting parameter are applied. If custom query strings were used as manual cache busters, you can now remove them safely.

EXT:my_extension/Configuration/TypoScript/setup.typoscript
page.20 = TEXT
page.20 {
    data = asset : EXT:core/Resources/Public/Icons/Extension.svg?v=1234
}
Copied!
Result
result before: /typo3/sysext/core/Resources/Public/Icons/Extension.svg?v=1234
result now: /typo3/sysext/core/Resources/Public/Icons/Extension.svg?v=1234&1709051481
Copied!

Impact 

getText "path" in TypoScript 

All usages of path in TypoScript data now resolve to absolute URLs instead of relative ones.

Relative path to FAL Storage 

In installations referencing resources in additional local FAL storages using a relative path syntax, an exception is thrown.

All generated URLs now contain cache busting 

Generated URLs differ slightly from previous TYPO3 versions, especially when cache busting was not applied before.

Additional query strings applied to the resource identifier 

URLs now include both the original query string and the cache-busting parameter, resulting in different output compared to earlier TYPO3 versions.

Affected installations 

getText "path" in TypoScript 

All installations using path in TypoScript data.

Relative path to FAL Storage 

All installations referencing resources in additional local FAL storages using a relative path syntax (e.g. my-other-fileadmin/path/to/file.css).

All generated URLs now contain cache busting 

All installations having third party code, that misuses generated URLs to assume file system paths from them.

Additional query strings applied to the resource identifier 

All installations using resource identifiers with custom query strings, for example:

EXT:foo/Resources/Public/rte.css?v=123

Migration 

Relative path to FAL Storage 

Either convert the relative path to a FAL resource like so:

FAL:1:/path/to/file.css

Alternatively it is possible to convert it to a resource URI:

URI:/my-other-fileadmin/path/to/file.css

The first will add cache busting, the latter will use the URI as is.

Breaking: #107566 - Removed "afterInitializeCurrentPage" hook 

See forge#107566

Description 

The hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterInitializeCurrentPage'] has been removed in favor of the PSR-14 event \TYPO3\CMS\Form\Event\AfterCurrentPageIsResolvedEvent .

Impact 

Hook implementations registered under afterInitializeCurrentPage are no longer executed in TYPO3 v14.0 and later.

Affected installations 

TYPO3 installations with custom extensions using this hook are affected.

The extension scanner reports any usage as a weak match.

Migration 

The hook was removed without a deprecation phase to allow extensions to work with both TYPO3 v13 (using the hook) and TYPO3 v14+ (using the new event) simultaneously.

Use the PSR-14 event instead to allow greater influence over the form rendering process. Since the event is dispatched at a later point, it allows more extensive modifications than the previous hook.

Breaking: #107568 - Removed "afterSubmit" hook 

See forge#107568

Description 

The hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterSubmit'] has been removed in favor of the PSR-14 event \TYPO3\CMS\Form\Event\BeforeRenderableIsValidatedEvent .

Impact 

Hook implementations registered under afterSubmit are no longer executed in TYPO3 v14.0 and later.

Affected installations 

TYPO3 installations with custom extensions using this hook are affected.

The extension scanner reports any usage as a weak match.

Migration 

The hook was removed without a deprecation phase to allow extensions to work with both TYPO3 v13 (using the hook) and TYPO3 v14+ (using the new event) simultaneously.

Use the PSR-14 event instead to allow greater influence over the form submission process. Since the event is dispatched at a later point, it allows more extensive modifications than the previous hook.

Breaking: #107569 - Removed "beforeRendering" hook 

See forge#107569

Description 

The hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeRendering'] has been removed in favor of the PSR-14 event \TYPO3\CMS\Form\Event\BeforeRenderableIsRenderedEvent .

Impact 

Hook implementations registered under beforeRendering are no longer executed in TYPO3 v14.0 and later.

Affected installations 

TYPO3 installations with custom extensions using this hook are affected.

The extension scanner reports any usage as a weak match.

Migration 

The hook was removed without a deprecation phase to allow extensions to work with both TYPO3 v13 (using the hook) and TYPO3 v14+ (using the new event) simultaneously.

Use the PSR-14 event instead to allow greater influence over the rendering process. Since the event is dispatched at a later point, it allows more extensive modifications than the previous hook.

Breaking: #107578 - Event AfterCacheableContentIsGeneratedEvent changed 

See forge#107578

Description 

The frontend rendering event \TYPO3\CMS\Frontend\Event\AfterCacheableContentIsGeneratedEvent has been adjusted due to the removal of the \TypoScriptFrontendController class.

The method getController() has been removed and replaced by two new methods: getContent() and setContent().

Impact 

Event listeners that call getController() will trigger a fatal PHP error and must be adapted.

Affected installations 

This event is commonly used in frontend rendering–related extensions, since it provides an opportunity to access and manipulate the fully rendered response body content at a late point in the rendering chain.

Instances with extensions listening to AfterCacheableContentIsGeneratedEvent may be affected. The extension scanner will detect such usages.

Migration 

In most cases, AfterCacheableContentIsGeneratedEvent->getController() was used within event listeners to get and modify the TypoScriptFrontendController->content property at the end of the rendering process.

The event now provides getContent() and setContent() methods to achieve the same goal more directly.

Before:

#[\TYPO3\CMS\Core\Attribute\AsEventListener('my-extension')]
public function indexPageContent(
    \TYPO3\CMS\Frontend\Event\AfterCacheableContentIsGeneratedEvent $event
): void {
    $tsfe = $event->getController();
    $content = $tsfe->content;
    // ... $content is manipulated here
    $tsfe->content = $content;
}
Copied!

After:

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Frontend\Event\AfterCacheableContentIsGeneratedEvent;

#[AsEventListener('my-extension')]
public function indexPageContent(AfterCacheableContentIsGeneratedEvent $event): void
{
    $content = $event->getContent();
    // ... $content is manipulated here
    $event->setContent($content);
}
Copied!

Version check for TYPO3 v13/v14 compatibility:

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Information\Typo3Version;
use TYPO3\CMS\Frontend\Event\AfterCacheableContentIsGeneratedEvent;

#[AsEventListener('my-extension')]
public function indexPageContent(
    AfterCacheableContentIsGeneratedEvent $event
): void {
    $version = new Typo3Version();
    if ($version->getMajorVersion() < 14) {
        // @todo: Remove if() when TYPO3 v13 compatibility is dropped
        $tsfe = $event->getController();
        $content = $tsfe->content;
    } else {
        $content = $event->getContent();
    }

    // ... $content is manipulated here

    if ($version->getMajorVersion() < 14) {
        // @todo: Remove if() when TYPO3 v13 compatibility is dropped
        $tsfe = $event->getController();
        $tsfe->content = $content;
    } else {
        $event->setContent($content);
    }
}
Copied!

Breaking: #107578 - Prepare EXT:adminpanel DataProviderInterface change 

See forge#107578

Description 

The typo3/cms-adminpanel system extension provides the interface \TYPO3\CMS\Adminpanel\ModuleApi\DataProviderInterface . It can be used by extensions that extend the Admin Panel with custom modules and allows storing additional request-related data in the Admin Panel–specific data store.

The signature of the interface method getDataToStore() has changed.

Impact 

Extension authors may benefit from the additional argument passed with TYPO3 v14, but implementations must be adjusted accordingly.

Affected installations 

Most installations are not affected, as few extensions extend the Admin Panel. Instances with classes implementing DataProviderInterface are affected.

Migration 

Interface until TYPO3 v13:

namespace TYPO3\CMS\Adminpanel\ModuleApi;

use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Adminpanel\ModuleApi\ModuleData;

interface DataProviderInterface
{
    public function getDataToStore(
        ServerRequestInterface $request
    ): ModuleData;
}
Copied!

The getDataToStore() method is called by the Admin Panel after the Response has been created by the TYPO3 Core. Starting with TYPO3 v14, the method receives the ResponseInterface as an additional argument:

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Adminpanel\ModuleApi\ModuleData;

public function getDataToStore(
    ServerRequestInterface $request,
    ResponseInterface $response
): ModuleData;
Copied!

Compatibility example for TYPO3 v13 and v14:

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Adminpanel\ModuleApi\ModuleData;

public function getDataToStore(
    ServerRequestInterface $request,
    ?ResponseInterface $response = null
): ModuleData {
    // TYPO3 v13: $response is null
    // TYPO3 v14: $response is an instance of ResponseInterface
}
Copied!

TYPO3 v13 does not pass the second argument, so it must be nullable, and extensions should not expect to receive an instance of \ResponseInterface .

TYPO3 v14, however, provides the response instance automatically.

Breaking: #107654 - Remove random subpage option of doktype=shortcut 

See forge#107654

Description 

The random subpage option for shortcut pages has been removed from TYPO3 Core.

This option allowed shortcut pages to redirect to a random subpage, which was problematic for caching and resulted in unpredictable behavior - a "random" page was not truly random, as the page linking to this shortcut was cached.

The following changes have been made:

  • The class constant \TYPO3\CMS\Core\Domain\Repository\PageRepository::SHORTCUT_MODE_RANDOM_SUBPAGE has been removed.
  • The method signature of \TYPO3\CMS\Core\Domain\Repository\PageRepository::resolveShortcutPage() has changed from resolveShortcutPage(array $page, bool $resolveRandomSubpages = false, bool $disableGroupAccessCheck = false) to resolveShortcutPage(array $page, bool $disableGroupAccessCheck = false).
  • The TCA configuration for pages.shortcut_mode no longer includes the option Random subpage of selected/current page.

Impact 

Code using the removed constant \TYPO3\CMS\Core\Domain\Repository\PageRepository::SHORTCUT_MODE_RANDOM_SUBPAGE will fail with a PHP fatal error.

Code calling PageRepository::resolveShortcutPage() with three parameters, where the second parameter was $resolveRandomSubpages, will fail. The second parameter is now $disableGroupAccessCheck.

Shortcut pages configured to use random subpage mode will now behave as if they were configured for first subpage mode.

Affected installations 

TYPO3 installations with:

  • Extensions using the constant PageRepository::SHORTCUT_MODE_RANDOM_SUBPAGE
  • Extensions calling PageRepository::resolveShortcutPage() with the $resolveRandomSubpages parameter
  • Extensions extending PageRepository and overriding getPageShortcut()
  • Shortcut pages configured with random subpage mode in the database

Migration 

Remove any usage of PageRepository::SHORTCUT_MODE_RANDOM_SUBPAGE.

Update calls to PageRepository::resolveShortcutPage() to remove the $resolveRandomSubpages parameter:

Before (TYPO3 v13 and lower)
use TYPO3\CMS\Core\Domain\Repository\PageRepository;

$pageRepository = GeneralUtility::makeInstance(PageRepository::class);
$page = $pageRepository->resolveShortcutPage($page, false, true);
Copied!
After (TYPO3 v14+)
use TYPO3\CMS\Core\Domain\Repository\PageRepository;

$pageRepository = GeneralUtility::makeInstance(PageRepository::class);
$page = $pageRepository->resolveShortcutPage($page, true);
Copied!

For shortcut pages configured with random subpage mode, update the database records to use a different shortcut mode (for example, first subpage or a specific target page):

UPDATE pages
   SET shortcut_mode = 1
 WHERE shortcut_mode = 2;
Copied!

Breaking: #107677 - Drop prepend and append modes from TCA value picker 

See forge#107677

Description 

The prepend and append modes of the value picker specified in TCA and used in FormEngine (for example $GLOBALS['TCA']['tx_example']['columns']['example']['config']['valuePicker']['mode'] ) have been removed.

These modes were designed to insert predefined values before or after the existing input value, but they served only niche use cases and caused inconsistent behavior across different input types.

Maintaining these modes introduced unnecessary complexity in both implementation and accessibility. Prepending or appending content dynamically to user input fields could easily lead to unexpected results, break input validation, and interfere with assistive technologies such as screen readers. Additionally, this approach mixed presentation and data logic in ways that are not consistent with modern form handling patterns.

Future improvements to the value picker will focus on a consistent mode=replace behavior and may be implemented as new form input types to provide a more robust and accessible user experience.

Impact 

Any value picker TCA configuration using the mode options prepend or append will no longer have any effect. TYPO3 ignores these settings, and the picker defaults to the standard replace behavior.

Affected installations 

Installations using custom field wizard configurations or integrations that rely on the value picker with mode = prepend or mode = append are affected.

Migration 

There is no direct replacement for the removed modes.

If your implementation depends on prepending or appending content to existing values, implement a custom input type or custom form element to handle this behavior explicitly. This allows you to maintain full control over the user interface, data handling, and accessibility.

For most use cases, it is recommended to replace prepend or append logic with the standard replace value picker behavior or use dedicated UI controls that clearly indicate how values are modified.

If the value picker was used to suggest combinable values, consider listing these elements in the field description instead so that users can copy and paste them manually into the input field.

Breaking: #107712 - New method hasSubmoduleOverview() in ModuleInterface 

See forge#107712

Description 

The interface \TYPO3\CMS\Backend\Module\ModuleInterface has been extended with a new method hasSubmoduleOverview() to support the new card-based submodule overview feature introduced in Feature: #107712 - Introduce card-based submodule overview.

Impact 

All custom implementations of \TYPO3\CMS\Backend\Module\ModuleInterface must now implement the new method hasSubmoduleOverview(): bool.

Existing implementations that do not implement this method will trigger a PHP fatal error.

Affected installations 

TYPO3 installations with custom PHP code that directly implement the ModuleInterface are affected.

This is uncommon, as most backend modules use the provided \TYPO3\CMS\Backend\Module\Module class or extend from \TYPO3\CMS\Backend\Module\BaseModule .

Migration 

Add the hasSubmoduleOverview() method to your custom implementation of ModuleInterface .

The method should typically return the configured value rather than a fixed boolean:

use TYPO3\CMS\Backend\Module\ModuleInterface;

class MyCustomModule implements ModuleInterface
{
    protected array $configuration = [];

    public function hasSubmoduleOverview(): bool
    {
        // Return the configured value, defaulting to false
        return $this->configuration['showSubmoduleOverview'] ?? false;
    }
}
Copied!

This allows the behavior to be controlled through the module's configuration.

Breaking: #107777 - Use strict types in Extbase class Argument 

See forge#107777

Description 

All properties, method arguments, and return types in \TYPO3\CMS\Extbase\Mvc\Controller\Argument are now strictly typed.

Impact 

Classes extending Argument must now ensure that all overridden properties, method arguments, and return types declare strict types accordingly.

Affected installations 

TYPO3 installations with custom classes extending Argument .

Migration 

Ensure all subclasses of Argument use strict type declarations for overridden properties, method parameters, and return types.

Breaking: #107783 - Registration of metadata extractors via registerExtractionService 

See forge#107783

Description 

The method \TYPO3\CMS\Core\Resource\Index\ExtractorRegistry::registerExtractionService() has been removed in favor of automatic registration via the interface \TYPO3\CMS\Core\Resource\Index\ExtractorInterface .

Registration of metadata extractors now happens automatically when a class implements the required interface ExtractorInterface . No further registration is necessary.

Impact 

Any call to ExtractorRegistry::registerExtractionService() is now a no-op and has no effect in TYPO3 v14.0+. Metadata extractors are automatically registered via the interface.

Affected installations 

TYPO3 installations with custom extensions that register metadata extractor classes via the mentioned method in ext_localconf.php.

The extension scanner will report such usages.

Migration 

The method has been removed without deprecation in order to allow extensions to work with TYPO3 v13 (using the registration method) and v14+ (using automatic interface-based registration) without additional deprecations.

Remove the manual registration from ext_localconf.php:

EXT:my_ext/ext_localconf.php
- $extractorRegistry = GeneralUtility::makeInstance(ExtractorRegistry::class);
- $extractorRegistry->registerExtractionService(MyExtractor::class);
Copied!

Since custom extractors already implement the required interface ExtractorInterface , no further changes are required inside the extractor class itself.

Breaking: #107784 - Remove backend layout data provider registration via $GLOBALS 

See forge#107784

Description 

The possibility to register backend layout data providers via $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['BackendLayoutDataProvider'] has been replaced by autoconfiguration using the service tag page_layout.data_provider.

The tag is automatically added when a class implements \TYPO3\CMS\Backend\View\BackendLayout\DataProviderInterface . Manual configuration via Services.yaml remains possible, especially when autoconfiguration is disabled.

Developers need to adapt existing implementations by adding the new method getIdentifier(), as outlined in Feature: #107784 - Autoconfigure backend layout data providers.

Additionally, the possibility to dynamically add backend layout data providers to the global DataProviderCollection via its add() method has been removed. Developers should register their data providers as service definitions in the container as described above.

Impact 

Using the global array $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['BackendLayoutDataProvider'] to register backend layout data providers has no effect in TYPO3 v14.0 and later.

Affected installations 

All installations that use $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['BackendLayoutDataProvider'] for backend layout data provider registration are affected.

This registration is typically done in an ext_localconf.php file.

The extension scanner will report such usages.

Migration 

Migrate existing registrations to the new autoconfiguration-based approach.

Before:

EXT:my_extension/ext_localconf.php
use Vendor\MyExtension\View\BackendLayout\MyLayoutDataProvider;

$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['BackendLayoutDataProvider']['my_provider']
    = MyLayoutDataProvider::class;
Copied!

After:

EXT:my_extension/Classes/View/BackendLayout/MyLayoutDataProvider.php
use TYPO3\CMS\Backend\View\BackendLayout\DataProviderInterface;

final class MyLayoutDataProvider implements DataProviderInterface
{
    // ...

    public function getIdentifier(): string
    {
        return 'my_provider';
    }
}
Copied!

If you need to support multiple TYPO3 versions, you can implement both registration methods (via $GLOBALS and via autoconfiguration).

Ensure that getIdentifier() is implemented, which is backward compatible with older TYPO3 versions even if unused.

Breaking: #107789 - Core TCA and user settings showitem strings use short form references 

See forge#107789

Description 

TYPO3 Core TCA and user settings ( $GLOBALS['TYPO3_USER_SETTINGS']) configurations have been updated to use short form translation reference formats (e.g., core.form.tabs:*) instead of the full LLL:EXT: path format in showitem strings.

This change affects all core TCA showitem definitions that previously used full LLL:EXT: paths for labels. The most prominent updates are tab labels using the --div-- syntax, though this pattern may be applied to other TCA elements in the future.

Examples of changed references in tab labels:

// Before (TYPO3 v13)
'--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general'
'--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access'
'--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language'
Copied!
// After (TYPO3 v14)
'--div--;core.form.tabs:general'
'--div--;core.form.tabs:access'
'--div--;core.form.tabs:language'
Copied!

Impact 

Custom extensions that programmatically manipulate TCA or $GLOBALS['TYPO3_USER_SETTINGS'] showitem strings from core tables and expect the full LLL:EXT: path format will break.

This particularly affects code that:

  • Uses string search/replace operations on showitem strings to find or modify specific labels (tabs, palettes, or other elements)
  • Parses showitem strings using regular expressions expecting the LLL:EXT: pattern
  • Extracts translation keys from TCA configurations for analysis or documentation purposes
  • Builds custom TCA configurations by copying and modifying core showitem strings

Currently, the following label categories have been migrated to short-form:

Tab labels (--div--):

  • LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:*core.form.tabs:*
  • LLL:EXT:filemetadata/Resources/Private/Language/locallang_tca.xlf:tabs.*core.form.tabs:*
  • LLL:EXT:seo/Resources/Private/Language/locallang_tca.xlf:pages.tabs.*core.form.tabs:*

Tab labels (--div--) in TYPO3_USER_SETTINGS:

  • LLL:EXT:setup/Resources/Private/Language/locallang.xlf:personal_datacore.form.tabs:personaldata
  • LLL:EXT:setup/Resources/Private/Language/locallang.xlf:accountSecuritycore.form.tabs:account_security
  • LLL:EXT:setup/Resources/Private/Language/locallang.xlf:openingcore.form.tabs:backend_appearance
  • LLL:EXT:setup/Resources/Private/Language/locallang.xlf:personalizationcore.form.tabs:personalization
  • LLL:EXT:setup/Resources/Private/Language/locallang.xlf:resetTabcore.form.tabs:reset_configuration

Palette labels (palette definitions):

  • LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:be_users.palettes.*core.form.palettes:*
  • LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:be_groups.palettes.*core.form.palettes:*
  • LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.palettes.*core.form.palettes:*
  • LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:palette.*core.form.palettes:*
  • LLL:EXT:filemetadata/Resources/Private/Language/locallang_tca.xlf:palette.*core.form.palettes:*
  • LLL:EXT:seo/Resources/Private/Language/locallang_tca.xlf:pages.palettes.*core.form.palettes:*

Replaced hardcoded palette label:

  • Removed the hardcoded palette name in string --palette--;Capabilities;capabilities in table sys_file_storage in favor of a label attached directly to the palette using the short syntax core.form.palettes:*.

Field label overrides removed in showitem definitions:

Field labels can be overridden in showitem definitions for types or palettes, but should rather be kept in the field definition itself. The following field label overrides have been removed from showitem strings in favor of using the field's own label definition:

  • bodytext;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:bodytext_formlabelbodytext
  • bodytext;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:field.table.bodytextbodytext
  • CType;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:CType_formlabelCType
  • colPos;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:colPos_formlabelcolPos
  • header;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:header_formlabelheader
  • header_layout;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:header_layout_formlabelheader_layout
  • header_link;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:header_link_formlabelheader_link
  • header_position;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:header_position_formlabelheader_position
  • subheader;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:subheader_formlabelsubheader
  • date;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:date_formlabeldate
  • file_collections;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:file_collections.ALT.uploads_formlabelfile_collections
  • filelink_size;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:filelink_size_formlabelfilelink_size
  • image_zoom;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:image_zoom_formlabelimage_zoom
  • imageborder;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:palette.mediaAdjustments.imageborderimageborder
  • image*;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:imageborder_formlabelfrontend.db.tt_content:imageborder
  • imagecols;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:imagecols_formlabelimagecols
  • imageorient;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:imageorient_formlabelimageorient
  • imageheight;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:palette.mediaAdjustments.imageheightimageheight
  • imagewidth;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:palette.mediaAdjustments.imagewidthimagewidth
  • frame_class;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:frame_class_formlabelframe_class
  • starttime;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.starttime_formlabelstarttime
  • endtime;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.endtime_formlabelendtime
  • fe_group;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.fe_group_formlabelfe_group
  • media;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:media.ALT.uploads_formlabelmedia
  • sectionIndex;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:sectionIndex_formlabelsectionIndex
  • linkToTop;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:linkToTop_formlabellinkToTop
  • layout;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:layout_formlabellayout
  • space_before_class;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:space_before_class_formlabelspace_before_class
  • space_after_class;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:space_after_class_formlabelspace_after_class
  • doktype;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.doktype_formlabeldoktype
  • shortcut_mode;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.shortcut_mode_formlabelshortcut_mode
  • shortcut;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.shortcut_formlabelshortcut
  • mount_pid_ol;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.mount_pid_ol_formlabelmount_pid_ol
  • mount_pid;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.mount_pid_formlabelmount_pid
  • url;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.url_formlabelurl
  • title;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.title_formlabeltitle
  • nav_title;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.nav_title_formlabelnav_title
  • subtitle;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.subtitle_formlabelsubtitle
  • nav_hide;LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:pages.nav_hide_toggle_formlabelnav_hide
  • extendToSubpages;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.extendToSubpages_formlabelextendToSubpages
  • abstract;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.abstract_formlabelabstract
  • keywords;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.keywords_formlabelkeywords
  • author;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.author_formlabelauthor
  • author_email;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.author_email_formlabelauthor_email
  • lastUpdated;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.lastUpdated_formlabellastUpdated
  • newUntil;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.newUntil_formlabelnewUntil
  • backend_layout;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.backend_layout_formlabelbackend_layout
  • backend_layout_next_level;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.backend_layout_next_level_formlabelbackend_layout_next_level
  • module;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.module_formlabelmodule
  • content_from_pid;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.content_from_pid_formlabelcontent_from_pid
  • cache_timeout;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.cache_timeout_formlabelcache_timeout
  • l18n_cfg;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.l18n_cfg_formlabell18n_cfg
  • is_siteroot;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.is_siteroot_formlabelis_siteroot
  • no_search;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.no_search_formlabelno_search
  • php_tree_stop;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.php_tree_stop_formlabelphp_tree_stop
  • editlock;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.editlock_formlabeleditlock
  • media;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.media_formlabelmedia
  • tsconfig_includes;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.tsconfig_includestsconfig_includes
  • TSconfig;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.TSconfig_formlabelTSconfig

Field label overrides changed in palette definitions:

  • hidden;LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:pages.hidden_toggle_formlabelhidden;core.db.pages:hidden
  • hidden;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:field.default.hiddenhidden;frontend.db.tt_content:hidden
  • hidden;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.hidden_formlabelhidden;core.db.pages:hidden
  • starttime;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:starttime_formlabelstarttime;core.db.general:starttime
  • endtime;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:endtime_formlabelendtime;core.db.general:endtime
  • fe_group;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:fe_group_formlabelfe_group;core.db.general:fe_group
  • target;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:pages.target_formlabeltarget;core.db.pages:link.target

Affected installations 

Installations with custom extensions that:

  • Programmatically read and manipulate TCA showitem strings from $GLOBALS['TCA'] or $GLOBALS['TYPO3_USER_SETTINGS'].
  • Override core TCA or TYPO3_USER_SETTINGS by copying and modifying existing showitem configurations
  • Perform string operations on showitem definitions expecting specific LLL:EXT: path formats
  • Generate documentation or analysis tools based on TCA label path references

The extension scanner will not detect these usages, as they involve runtime string manipulation rather than direct PHP API usage.

Note: Additional TCA elements beyond tab labels may follow this pattern in future TYPO3 versions, further extending the use of short-form references in showitem strings.

Migration 

TCA Migration 

Extension developers should review their Configuration/TCA/Overrides/ files and any PHP code that manipulates TCA showitem strings programmatically.

Option 1: Support both formats in string operations

Update your code to handle both the old LLL:EXT: path format and the new short-form references:

// Before - hardcoded search for old format
if (str_contains($showitem, 'LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general')) {
    // Will not work in TYPO3 v14+
}
Copied!
// After - handle new format
if (str_contains($showitem, 'core.form.tabs:general') ||
    str_contains($showitem, 'LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general')) {
    // Works in both versions
}
Copied!

Option 2: Use the TCA API instead of string manipulation

Rather than manipulating showitem strings directly, use TYPO3's TCA manipulation APIs:

// Instead of string manipulation
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

// Add fields using the API
ExtensionManagementUtility::addToAllTCAtypes(
    'tx_myext_domain_model_foo',
    'my_field',
    '',
    'after:title'
);
Copied!

Recommended action for all extension developers

Scan your extension's Configuration/TCA/Overrides/ directory and any PHP code that interacts with showitem strings for patterns such as:

  • str_contains(), str_replace(), preg_match() or similar string functions operating on showitem values
  • String operations looking for 'LLL:EXT:' patterns in TCA configurations
  • Custom parsing of $GLOBALS['TCA'] showitem strings expecting specific path formats

This review is especially important since future TYPO3 versions may further expand the use of short-form references across additional TCA elements.

TYPO3_USER_SETTINGS migrations 

Update your code to handle the new short form references:

Before:

EXT:my_extension/ext_tables.php
// Before - hardcoded search for old format
$showitem = $GLOBALS['TYPO3_USER_SETTINGS']['showitem'];
if (str_contains($showitem, 'LLL:EXT:setup/Resources/Private/Language/locallang.xlf:personal_data')) {
    // Will not work in TYPO3 v14+
}
Copied!

After:

EXT:my_extension/ext_tables.php
// After - handle new format
$showitem = $GLOBALS['TYPO3_USER_SETTINGS']['showitem'];
if (str_contains($showitem, 'core.form.tabs:personaldata')
    || str_contains($showitem, 'LLL:EXT:setup/Resources/Private/Language/locallang.xlf:personal_data')) {
    // Works in both versions
}
Copied!

Breaking: #107791 - Report interfaces removed 

See forge#107791

Description 

The Reports module has been refactored to use native backend submodules instead of dynamic service-based report registration. Individual reports are now registered as proper backend submodules in Configuration/Backend/Modules.php.

As a result, the following public interfaces have been removed:

  • \TYPO3\CMS\Reports\ReportInterface
  • \TYPO3\CMS\Reports\RequestAwareReportInterface

Impact 

Extensions that register custom reports by implementing \TYPO3\CMS\Reports\ReportInterface or \TYPO3\CMS\Reports\RequestAwareReportInterface will no longer work.

These reports will no longer appear in the backend Reports module.

Affected installations 

TYPO3 installations with custom extensions that provide reports by implementing \ReportInterface or \RequestAwareReportInterface.

Migration 

Custom reports must be migrated to backend submodules.

Register as a submodule under `system_reports`:

EXT:my_extension/Configuration/Backend/Modules.php
use Vendor\MyExtension\Controller\MyReportController;

return [
    'system_reports_myreport' => [
        'parent' => 'system_reports',
        'access' => 'admin',
        'path' => '/module/system/reports/myreport',
        'iconIdentifier' => 'module-reports',
        'labels' => [
            'title' => 'my_extension.messages:myreport.title',
            'description' => 'my_extension.messages:myreport.description',
        ],
        'routes' => [
            '_default' => [
                'target' => MyReportController::class . '::handleRequest',
            ],
        ],
    ],
];
Copied!

The controller should implement a standard PSR-7 request handler that returns a \Psr\Http\Message\ResponseInterface instance.

Alternatively, you can create a standalone module with showSubmoduleOverview enabled if you need to group multiple reports under your own container module.

Breaking: #107823 - Strict typing and API cleanup in backend template components 

See forge#107823

Description 

The backend template components system (buttons, dropdown items, and menus) has been modernized with strict type hints, consistent return types, and improved architecture to enhance type safety and developer experience.

Impact 

Extensions that implement or extend backend template components must verify their type declarations and update any usage of changed methods.

New ComponentInterface 

A new \TYPO3\CMS\Backend\Template\Components\ComponentInterface has been introduced as the parent interface for both \ButtonInterface and \DropDownItemInterface. This unifies the common contract for all renderable backend components.

Both interfaces now extend ComponentInterface , which defines:

  • isValid(): bool
  • getType(): string
  • render(): string

Custom implementations of \ButtonInterface or \DropDownItemInterface will now trigger a \TypeError if these return types are missing.

PositionInterface enforced 

The \TYPO3\CMS\Backend\Template\Components\PositionInterface now enforces strict type hints:

  • getPosition(): string
  • getGroup(): int

This interface allows buttons to define their own fixed position and group, which automatically override the position and group parameters passed to ButtonBar::addButton().

Icon nullability 

Icons are now consistently nullable across all button types. The AbstractButton::$icon property and related getter/setter methods now use ?Icon instead of Icon.

This affects classes extending AbstractButton , including LinkButton , InputButton , and SplitButton .

Method signature changes 

Several methods now have stricter parameter types or modified signatures:

  • MenuItem::isValid() and Menu::isValid() no longer accept parameters
  • AbstractDropDownItem::render() now declares a string return type
  • Various setter methods now require strict type hints for their parameters

Return type consistency 

Abstract classes now use static return types for better inheritance support, while concrete implementations may use self or static depending on extensibility requirements.

SplitButton API improvement 

The SplitButton::getButton() method has been replaced with getItems(), which returns a type-safe SplitButtonItems DTO instead of an untyped array.

Old (removed):

public function getButton(): array  // Returns array with magic keys 'primary' and 'options'
Copied!

New:

public function getItems(): SplitButtonItems  // Returns typed DTO
Copied!

The SplitButtonItems DTO provides:

  • public readonly AbstractButton $primary - The primary action button
  • public readonly array $options - Array of option buttons

This change improves type safety and prevents runtime errors from accessing non-existent array keys.

Affected installations 

All TYPO3 instances with custom backend components, such as buttons, menus, or dropdown items, that extend or implement the affected interfaces are impacted.

Migration 

Extension authors should:

  1. Verify custom button implementations have correct return types on interface methods.
  2. Check custom classes extending abstract buttons use proper strict types.
  3. Update `isValid()` calls on MenuItem and Menu objects (remove the parameter).
  4. Handle nullable icons when working with getIcon() methods.

Example: implementing ButtonInterface 

use TYPO3\CMS\Backend\Template\Components\ButtonInterface;

// Before
class CustomButton implements ButtonInterface {
    public function isValid() { ... }
    public function render() { ... }
    public function getType() { ... }
}
Copied!
use TYPO3\CMS\Backend\Template\Components\ButtonInterface;

// After
class CustomButton implements ButtonInterface {
    public function isValid(): bool { ... }
    public function render(): string { ... }
    public function getType(): string { return static::class; }
}
Copied!

Example: working with MenuItem/Menu 

// Before
if ($menuItem->isValid($menuItem)) { ... }
Copied!
// After
if ($menuItem->isValid()) { ... }
Copied!

Example: nullable icons 

// Handle nullable icon return
$icon = $button->getIcon();  // Now returns ?Icon
$html = $icon?->render() ?? '';
Copied!

Example: using SplitButton with typed DTO 

If you were directly accessing the getButton() method:

// Before
$items = $splitButton->getButton();
$primary = $items['primary'];  // Magic string key
$options = $items['options'];  // Magic string key
Copied!
// After
$items = $splitButton->getItems();
$primary = $items->primary;  // Type-safe property access
$options = $items->options;  // Type-safe property access
Copied!

Breaking: #107831 - AfterCachedPageIsPersistedEvent no longer receives TypoScriptFrontendController 

See forge#107831

Description 

The frontend rendering related event \TYPO3\CMS\Frontend\Event\AfterCachedPageIsPersistedEvent has been modified due to the removal of the class \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController.

The method getController() has been removed.

Impact 

Event listeners that call getController() will now trigger a fatal PHP error and and must be adapted.

Affected installations 

Instances with extensions listening for the event AfterCachedPageIsPersistedEvent may be affected.

The extension scanner will detect and report such usages.

Migration 

In most cases, data that was previously retrieved from the \TYPO3\CMS\Frontend\Controller\`TypoScriptFrontendController instance can now be accessed through the request object, available via $event->getRequest().

See Breaking: #102621 - Most TSFE members marked internal or read-only for further details about accessing frontend-related data via the PSR-7 request.

Breaking: #107831 - Removed TypoScriptFrontendController 

See forge#107831

Description 

All remaining properties have been removed from \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController, making the class a readonly internal service used by the TYPO3 Core only.

The class will be fully removed in a later TYPO3 v14 release.

The following instance access patterns have been removed:

  • $GLOBALS['TSFE']
  • $request->getAttribute('frontend.controller')
  • $contentObjectRenderer->getTypoScriptFrontendController()

All API methods that returned an instance of \TypoScriptFrontendController - usually named getTypoScriptFrontendController() or similar - have been removed as well.

Impact 

Any remaining direct or indirect usage of \TypoScriptFrontendController will now result in a fatal PHP error.

Affected installations 

Extensions that still relied on properties or methods of \TypoScriptFrontendController are affected.

The class was already marked internal and breaking in TYPO3 v13.

In particular, extensions that used AbstractContentObject->getTypoScriptFrontendController() can now access the relevant data from the PSR-7 request object, for example:

$pageInformation = $request->getAttribute('frontend.page.information');
Copied!

Migration 

See Breaking: #102621 - Most TSFE members marked internal or read-only for a detailed list of removed properties and their replacements.

As a specific example, old code that added additional header data like this:

$frontendController = $request->getAttribute('frontend.controller');
$frontendController->additionalHeaderData[] = $myAdditionalHeaderData;
Copied!

should now use:

use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
$pageRenderer->addHeaderData($myAdditionalHeaderData);
Copied!

The same approach applies to additionalFooterData.

Breaking: #107856 - DataHandler: Remove internal property copyWhichTablesand properties neverHideAtCopyand copyTree 

See forge#107856

Description 

The following public properties of the PHP class TYPO3\CMS\Core\DataHandling\DataHandler have been removed:

  • copyWhichTables
  • neverHideAtCopy
  • copyTree

Impact 

Accessing or setting the properties will throw a PHP warning and have no effect anymore.

Affected installations 

Any installation working with the public properties in a third-party extension.

Migration 

The configuration values neverHideAtCopy and copyTree are directly read from the backend user BE_USER object. To modify them, use the following values instead:

// Before
DataHandler->neverHideAtCopy
// After
DataHandler->BE_USER->uc['neverHideAtCopy']

// Before
DataHandler->copyTree
// After
DataHandler->BE_USER->uc['copyLevels']
Copied!

To retain database consistency, the list of tables to be copied now only relied on permissions of the given backend user. If the user has admin access, all tables will be copied if needed. If not, all tables with access will be copied.

Breaking: #107871 - Remove backend avatar provider registration via $GLOBALS 

See forge#107871

Description 

Registering backend avatar providers via $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['backend']['avatarProviders'] has been replaced by autoconfiguration using the backend.avatar_provider service tag. This tag is added automatically when the PHP attribute #[AsAvatarProvider] is applied. Manual configuration in Services.yaml is still possible, particularly if autoconfiguration is disabled.

Impact 

Using the $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['backend']['avatarProviders'] array has no effect in TYPO3 v14.0 and later.

Affected installations 

All installations that use $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['backend']['avatarProviders'] to register backend avatar providers are affected. This registration is typically performed in an ext_localconf.php file. The extension scanner will report any such usages.

Migration 

Migrate existing registrations to the new autoconfiguration-based approach.

Before:

EXT:my_extension/ext_localconf.php
use MyVendor\MyExtension\Backend\Avatar\MyAvatarProvider;

$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['backend']
    ['avatarProviders']['my_provider'] = [
        'provider' => MyAvatarProvider::class,
        'before' => ['provider-1'],
        'after' => ['provider-2'],
    ];
Copied!

After:

EXT:my_extension/Classes/Backend/Avatar/MyAvatarProvider.php
use TYPO3\CMS\Backend\Attribute\AsAvatarProvider;
use TYPO3\CMS\Backend\Backend\Avatar\AvatarProviderInterface;

#[AsAvatarProvider(
    'my_provider',
    before: ['provider-1'],
    after: ['provider-2']
)]
final class MyAvatarProvider implements AvatarProviderInterface
{
    // ...
}
Copied!

If you need to support multiple TYPO3 Core versions simultaneously, ensure that both registration methods are implemented: the legacy $GLOBALS based registration as well as the new tag-based approach.

Breaking: #107884 - Rework actions to use Buttons API with Components 

See forge#107884

Description 

The record list and file list action system (the "button bar" in every row of the table-like display) has been completely reworked to use the Buttons API, utilizing proper component objects instead of plain HTML strings.

This modernization improves type safety, provides better extensibility, and enables more structured manipulation of action buttons through PSR-14 events.

The following components have been affected by this change:

  • \TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListRecordActionsEvent
  • \TYPO3\CMS\Filelist\Event\ProcessFileListActionsEvent
  • \TYPO3\CMS\Backend\RecordList\DatabaseRecordList::makeControl()

Buttons can now be placed into ActionGroups, which are identified by the PHP enum \TYPO3\CMS\Backend\Template\Components\ActionGroup and distinguish between a "primary" and a "secondary" group.

In addition, \TYPO3\CMS\Backend\Template\Components\ComponentGroup enhances the ability to group multiple Button API Components into a single data object and manage their state.

Impact 

Extensions that listen to the ModifyRecordListRecordActionsEvent or ProcessFileListActionsEvent to modify record or file actions need to be updated.

The events no longer work with HTML strings but with ComponentInterface objects (see forge#107823).

Extensions that directly call DatabaseRecordList::makeControl() must update their code, as the $table parameter has been removed.

ModifyRecordListRecordActionsEvent 

The method setAction() now requires a ComponentInterface object, and getAction() now returns either null or a ComponentInterface instance.

The following methods now expect an ActionGroup enum value as the $group parameter:

  • hasAction()
  • getAction()
  • removeAction()
  • getActionGroup()

The method getRecord() no longer returns a raw array but an instance of the Record API.

A new method getRequest() has been added to access the current PSR-7 request context.

Removed methods:

  • getActions()
  • setActions()
  • getTable()

ProcessFileListActionsEvent 

The \TYPO3\CMS\Filelist\Event\ProcessFileListActionsEvent has received identical API changes to ModifyRecordListRecordActionsEvent , allowing manipulation of items in both supported ActionGroup contexts (primary and secondary).

New methods:

  • setAction()
  • getAction()
  • removeAction()
  • moveActionTo()
  • getActionGroup()
  • getRequest()

Buttons can now also be repositioned or inserted at specific before/after locations within an action group.

Removed methods:

  • getActionItems()
  • setActionItems()

Affected installations 

TYPO3 installations with custom PHP code that modifies record or file list actions, or utilizes the mentioned PSR-14 events, are affected.

Migration 

DatabaseRecordList::makeControl() 

// Before
public function makeControl($table, RecordInterface $record): string

// After
public function makeControl(RecordInterface $record): string
Copied!

The $table parameter has been removed, as the table name can now be retrieved from the RecordInterface via $record->getMainType().

Adjust calls accordingly:

EXT:my_extension/Classes/ViewHelper/MyControlViewHelper.php
 // ...
 public function render(): string
 {
     $row = BackendUtility::getRecord($table, $someRowUid);
     $databaseRecordList = GeneralUtility::makeInstance(DatabaseRecordList::class);
-    return $databaseRecordList->makeControl($table, $row);
+    return $databaseRecordList->makeControl($row);
 }
Copied!

ProcessFileListActionsEvent 

Event listeners must now compose buttons via the Button API and add each component using the event’s setAction() method.

Internally, buttons are placed into an ActionGroup container, retrieved via ActionGroup::primary or ActionGroup::secondary.

The previous getActionItems() logic is replaced with getActionGroup() to fetch the corresponding button group.

Instead of manipulating raw HTML, you must now create components using the ComponentFactory .

// Before
use TYPO3\CMS\Filelist\Event\ProcessFileListActionsEvent;

class ProcessFileListActionsEventListener
{
    public function __invoke(ProcessFileListActionsEvent $event): void
    {
        $items = $event->getActionItems();
        $items['my-own-action'] = '<a href="..." class="btn btn-default">...</a>';
        unset($items['some-other-action']);
        $event->setActionItems($items);
    }
}
Copied!
// After
use TYPO3\CMS\Backend\Template\Components\ActionGroup;
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Filelist\Event\ProcessFileListActionsEvent;

class ProcessFileListActionsEventListener
{
    public function __construct(
        private readonly ComponentFactory $componentFactory,
        private readonly IconFactory $iconFactory,
    ) {}

    public function __invoke(ProcessFileListActionsEvent $event): void
    {
        $viewButton = $this->componentFactory->createGenericButton()
            ->setIcon($this->iconFactory->getIcon('actions-view'))
            ->setTitle('My title');

        $event->setAction($viewButton, 'my-own-action', ActionGroup::primary);
        $event->removeAction('some-other-action', ActionGroup::primary);
    }
}
Copied!

ModifyRecordListRecordActionsEvent 

This event now behaves identically to the file list event: actions must be created via the Button API and added as ComponentInterface instances using setAction().

The setActions() and getActions() methods are removed and must be replaced by distinct calls to setAction() or use getActionGroup() to access existing actions.

The getRecord() method now returns a Record API object instead of an array. getTable() can be replaced with getRecord()->getMainType().

Modifying actions example:

// Before
use TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListRecordActionsEvent;

class ModifyRecordListRecordActionsEventListener
{
    public function __invoke(ModifyRecordListRecordActionsEvent $event): void
    {
        $items = $event->getActions();
        unset($items['my-own-action']);
        $items['my-own-action'] = '<a href="..." class="btn btn-default">...</a>';
        unset($existing['some-other-action']);
        $event->setActions($items);

        $event->setAction('<button ...></button>', 'my-other-own-action', 'secondary');
    }
}
Copied!
// After
use TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListRecordActionsEvent;
use TYPO3\CMS\Backend\Template\Components\ActionGroup;
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
use TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListRecordActionsEvent;
use TYPO3\CMS\Core\Imaging\IconFactory;

class ModifyRecordListRecordActionsEventListener
{
    public function __construct(
        private readonly ComponentFactory $componentFactory,
        private readonly IconFactory $iconFactory,
    ) {}

    public function __invoke(ModifyRecordListRecordActionsEvent $event): void
    {
        $viewButton = $this->componentFactory->createGenericButton()
            ->setIcon($this->iconFactory->getIcon('actions-view'))
            ->setTitle('My title');

        $event->setAction($viewButton, 'my-own-action', ActionGroup::primary);
        $event->removeAction('some-other-action', ActionGroup::primary);

        $inputButton = $this->componentFactory->createInputButton()
            ->setTitle('My Button');

        $event->setAction($inputButton, 'my-other-own-action', ActionGroup::secondary);
    }
}
Copied!

Accessing groups 

// Before
$event->getAction('my-button', 'primary');
$event->hasAction('my-button', 'primary');
$event->removeAction('my-button', 'primary');
$event->getActionGroup('primary');
Copied!
// After
use TYPO3\CMS\Backend\Template\Components\ActionGroup;

$event->getAction('my-button', ActionGroup::primary);
$event->hasAction('my-button', ActionGroup::primary);
$event->removeAction('my-button', ActionGroup::primary);
$event->getActionGroup(ActionGroup::primary);
Copied!

Accessing record 

// Before
$uid = $event->getRecord()['uid'];
$title = $event->getRecord()['title'];
Copied!
// After
$uid = $event->getRecord()->getUid();
$title = $event->getRecord()->getRawRecord()['title'];
Copied!

Dual-version compatibility 

To support both TYPO3 v13 and v14, extensions can use a version check within event listeners:

use TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListRecordActionsEvent;
use TYPO3\CMS\Backend\Template\Components\ActionGroup;
use TYPO3\CMS\Core\Information\Typo3Version;

class ModifyRecordListRecordActionsEventListener
{
    public function __invoke(ModifyRecordListRecordActionsEvent $event): void
    {
        if ((new Typo3Version())->getMajorVersion() >= 14) {
            $viewButton = $this->componentFactory->createGenericButton()
                ->setIcon($this->iconFactory->getIcon('actions-view'))
                ->setTitle('My title');
            $event->setAction($viewButton, 'my-own-action', ActionGroup::primary);
            $event->removeAction('some-other-action', ActionGroup::primary);
        } else {
            $items = $event->getActions();
            unset($items['my-own-action']);
            $items['my-own-action'] = '<a href="..." class="btn btn-default">...</a>';
            unset($existing['some-other-action']);
            $event->setActions($items);
        }
    }
}
Copied!

Breaking: #107927 - Remove "external" property / option from TypoScript and AssetRenderer 

See forge#107927

Description 

The resource property external in the PAGE properties includeCSS, includeCSSLibs, includeJS, includeJSFooter, includeJSFooterlibs and includeJSLibs is now obsolete.

This also obsoletes the External option in AssetRenderer.

Both are removed in favor of the new unified URI resource definition.

Instead of marking URIs as URIs with an additional option, prefix the URI, that shall be used with URI:, or simply use absolute URLs starting with http(s)://, where the prefix is not required.

This URI resource definition will work across the system, not only in TypoScript or AssetCollector/ AssetRenderer.

Impact 

Using the external property in TypoScript or the external option in AssetCollector will have no effect any more. If absolute URLs have been used as resources, everything will work as before. Relative URIs must be prefixed with URI: from now on, otherwise an exception is thrown.

Additionally, the string after the URI: keyword must be a valid URI, otherwise an exception is thrown as well. Before this change, invalid URIs (marked as external) would have been rendered to HTML, without any obvious feedback for developers or integrators. Browsers then ignored such invalid references.

Affected installations 

TYPO3 installations using the external property in TypoScript or the external option in AssetCollector.

Migration 

TypoScript before:

page = PAGE
page.includeCSS {
    main = https://example.com/styles/main.css
    main.external = 1
    other = /styles/main.css
    other.external = 1
}
Copied!

TypoScript after:

page = PAGE
page.includeCSS {
    main = https://example.com/styles/main.css
    other = URI:/styles/main.css
}
Copied!

PHP Code before:

$assetCollector->addStyleSheet(
    'myCssFile',
    '/styles/main.css',
    [],
    ['external' => true]
);
Copied!

PHP Code after:

$assetCollector->addStyleSheet(
    'myCssFile',
    'URI:/styles/main.css',
);
Copied!

Breaking: #107943 - Frontend and backend HTTP response compression removed 

See forge#107943

Description 

The TYPO3 frontend and backend applications previously allowed compressing their HTTP responses using the configuration options $GLOBALS['TYPO3_CONF_VARS']['FE']['compressionLevel'] and $GLOBALS['TYPO3_CONF_VARS']['BE']['compressionLevel'] .

This feature, which was always disabled by default, has now been removed.

TYPO3 will no longer compress its HTTP responses itself.

Response compression should be handled by the web server rather than the application layer.

Removing this feature avoids potential conflicts when both TYPO3 and the web server attempt to compress responses and allows modern web servers to use advanced compression algorithms such as brotli or zStandard when supported by the client.

Impact 

TYPO3 can no longer compress its HTTP responses.

This responsibility is now fully delegated to the web server.

HTTP response compression had to be explicitly enabled before, so most installations will not notice a change unless they relied on this setting.

Affected installations 

Instances that configured $GLOBALS['TYPO3_CONF_VARS']['FE']['compressionLevel'] or $GLOBALS['TYPO3_CONF_VARS']['BE']['compressionLevel'] to non-zero values are affected.

Administrators should verify that the web server applies HTTP compression by checking for a response header such as:

Content-Encoding: gzip

when requesting frontend or backend documents with a header like:

Accept-Encoding: gzip, deflate

All commonly used web servers enable this feature by default.

Migration 

The configuration toggles for the backend $GLOBALS['TYPO3_CONF_VARS']['BE']['compressionLevel'] and the frontend $GLOBALS['TYPO3_CONF_VARS']['FE']['compressionLevel'] are obsolete, existing settings in settings.php configuration files are actively removed when first using the install tool after upgrade to TYPO3 v14.

Breaking: #107944 - Frontend CSS file processing no longer removes comments and whitespaces 

See forge#107944

Description 

When the TYPO3 frontend was configured to compress included CSS assets, it also attempted to minify CSS by removing comments and certain whitespace characters.

This behavior has now been removed. The previous implementation was brittle, especially with modern CSS syntax, and provided no measurable performance benefit in either file transfer or client-side parsing.

Impact 

CSS asset files included in frontend pages may become slightly larger if they contain many comments. TYPO3’s internal CSS parsing was disabled by default and only active when explicitly enabled using $GLOBALS['TYPO3_CONF_VARS']['FE']['compressionLevel'] along with additional TypoScript configuration.

In most cases, this change has minimal or no practical impact.

Affected installations 

Instances that actively used TYPO3's built-in CSS parsing feature for frontend asset management are affected.

Migration 

If minimizing CSS file size is important, consider one of the following options:

  • Optimize or minify CSS files manually during deployment.
  • Accept that comments and whitespace are retained (usually negligible impact).
  • Preferably, integrate a dedicated frontend build chain to handle CSS and JavaScript minification.

Modern frontend build tools provide many additional advantages, such as linting, syntax validation, and advanced optimizations, which are beyond the scope of the TYPO3 Core.

Breaking: #107945 - Class FlexFormService merged into FlexFormTools 

See forge#107945

Description 

The class \TYPO3\CMS\Core\Service\FlexFormService has been merged into \TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools .

The following methods are affected:

  • FlexFormService->convertFlexFormContentToArray($flexFormContent, $languagePointer = 'lDEF', $valuePointer = 'vDEF'): array is now FlexFormTools->convertFlexFormContentToArray(string $flexFormContent): array. The method name is unchanged, but the method signature has been simplified.
  • FlexFormService->convertFlexFormContentToSheetsArray(string $flexFormContent, string $languagePointer = 'lDEF', string $valuePointer = 'vDEF'): array is now FlexFormTools->convertFlexFormContentToSheetsArray(string $flexFormContent): array. Again, the name is identical, but the parameters have been reduced.
  • The helper method FlexFormService->walkFlexFormNode() has been made a private method within FlexFormTools.

Impact 

Instantiating or injecting FlexFormService remains possible in TYPO3 v14 due to a maintained class alias for backward compatibility.

This alias will be removed in TYPO3 v15.

Affected installations 

Any extensions or TYPO3 installations using FlexFormService are affected.

The extension scanner will automatically detect these usages.

Migration 

Extensions typically did not use the now internal helper method walkFlexFormNode().

In the unlikely case this private method was used, its functionality must now be implemented within the consuming extension.

The methods convertFlexFormContentToArray() and convertFlexFormContentToSheetsArray() have lost their second and third arguments.

These parameters ( lDEF and vDEF) were already fixed internally in TYPO3 and could no longer be changed, so their removal has no functional impact.

To continue using these methods, extensions should inject FlexFormTools instead of FlexFormService.

For extensions that need to remain compatible with both TYPO3 v13 and v14, it is still possible to use FlexFormService for now.

However, when adding compatibility for TYPO3 v15 (and dropping TYPO3 v13), extensions must switch fully to FlexFormTools .

Breaking: #108054 - Enforce explicit opt-in for TypoScript/TSconfig callables 

See forge#108054

Description 

To strengthen TYPO3's security posture and implement defense-in-depth principles, a new hardening mechanism has been introduced that requires explicit opt-in for methods and functions that can be invoked through TypoScript configuration.

The new PHP attribute #[\TYPO3\CMS\Core\Attribute\AsAllowedCallable] must be applied to any method that should be callable via:

  • TypoScript userFunc processing (including the USER and USER_INT content objects)
  • TypoScript stdWrap functions preUserFuncInt, postUserFunc and postUserFuncInt
  • TypoScript constant comment user functions
  • TSconfig renderFunc in suggest wizard configuration

This security enhancement implements strong defaults through explicit configuration, following the principle of least privilege.

Implementation details:

  • New \TYPO3\CMS\Core\Attribute\AsAllowedCallable PHP attribute
  • New \TYPO3\CMS\Core\Security\AllowedCallableAssertion service for validation
  • Enhanced \TYPO3\CMS\Core\Utility\GeneralUtility::callUserFunction()
  • Enhanced \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::callUserFunction()

Impact 

Extension code that provides custom processing methods callable from TypoScript or TSconfig will fail with a \TYPO3\CMS\Core\Security\AllowedCallableException if the target method is not explicitly marked with the #[AsAllowedCallable] attribute.

The error message will be:

Attribute TYPO3\CMS\Core\Attribute\AsAllowedCallable required for
callback reference: ["VendorName\\ExtensionName\\ClassName","methodName"]
Copied!

Affected installations 

Scenarios using:

  • custom processing via TypoScript userFunc
  • custom processing via TypoScript constant comments
  • custom suggest wizard rendering via TSconfig renderFunc

Migration 

Add the #[AsAllowedCallable] attribute to all methods that should be callable from TypoScript or TSconfig.

TypoScript userFunc example:

EXT:my_extension/Classes/UserFunc/CustomProcessor.php
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;

class CustomProcessor
{
    #[AsAllowedCallable]
    public function process(
        string $content,
        array $conf
    ): string {
        return $content;
    }
}
Copied!

The attribute may be applied to:

  • public instance methods
  • public static methods
  • public __invoke() methods
  • custom functions in the global namespace

Native PHP functions in the global namespace must be wrapped explicitly.

Example for custom functions in the global namespace:

namespace {
    use TYPO3\CMS\Core\Attribute\AsAllowedCallable;

    #[AsAllowedCallable]
    function customGlobalUserFunction(): string
    {
        return '...';
    }

    #[AsAllowedCallable]
    function nativePhpHashWrapper(
        string $algo,
        string $data,
        bool $binary = false
    ): string {
        return \hash($algo, $data, $binary);
    }
}
Copied!

Breaking: #108055 - Removed frontend asset concatenation and compression 

See forge#108055

Description 

Introduction 

The implementation of CSS and JavaScript asset concatenation and pre-compression has been removed from the TYPO3 Core in v14.

The following TypoScript options are now obsolete:

  • Config property config.compressCss
  • Config property config.compressJs
  • Config property config.concatenateCss
  • Config property config.concatenateJs
  • The resource properties disableCompression and excludeFromConcatenation in the PAGE properties includeCSS, includeCSSLibs, includeJS, includeJSFooter, includeJSFooterlibs and includeJSLibs.

    Example:

    page = PAGE
    page.includeCSS {
        main = EXT:site_package/Resources/Public/Css/main.css
        # obsolete
        main.disableCompression = 1
        # obsolete
        main.excludeFromConcatenation = 1
    }
    Copied!

The configuration option $GLOBALS['TYPO3_CONF_VARS']['FE']['compressionLevel'] is obsolete in combination with Breaking: #107943 - Frontend and backend HTTP response compression removed. Existing settings in settings.php configuration files are automatically removed when the install tool is first used after upgrading to TYPO3 v14.

The PHP class \TYPO3\CMS\Core\Resource\ResourceCompressor has been removed.

Feature rundown 

In TYPO3 versions prior to v14, the system included a built-in mechanism to compile multiple registered CSS and JavaScript files into single files and to prepare compressed versions of those concatenated files for direct delivery by the web server.

This functionality had to be explicitly enabled by setting the global configuration option $GLOBALS['TYPO3_CONF_VARS']['FE']['compressionLevel'] and the TypoScript options config.concatenate* and optionally config.compress*. CSS and JavaScript files registered in the frontend using the PAGE.include* options were then concatenated and optionally compressed into a single file.

The implementation also concatenated external asset resources referenced via http and https, which were fetched server-side using GeneralUtility::getUrl() and merged with local resources.

Concatenation was designed to be as transparent as possible: hashes were created from all referenced file names and their contents (including external content) for each TYPO3 frontend page request, generating unique file names that changed whenever an included file or its content was modified. The resulting concatenated files were then referenced in the generated HTML page instead of the original resource links.

Pre-compression operated on top of concatenation: if enabled, a gzip- compressed .gz file was created and referenced in the HTML page response.

Configuration was handled via TypoScript, while asset registration could be performed using TypoScript or PHP with the PageRenderer. The Fluid asset ViewHelpers f:asset.css and f:asset.script register assets using the AssetCollector, introduced in TYPO3 v10. This implementation never supported concatenation or compression, as it was developed independently from the PageRenderer's asset handling and did not use the ResourceCompressor.

Concatenation and compression removal reasoning 

A closer look at the concatenation and compression functionality reveals several reasons for its removal:

  • HTTP/2 and HTTP/3: Modern HTTP versions allow multiple resource requests in parallel ("multiplexing"), making server-side asset concatenation obsolete. These versions also provide significant performance improvements on both the server and client sides compared to HTTP/1.1 with concatenation. HTTP/2 and HTTP/3 are only available via SSL (HTTPS), which is now the standard for all serious websites. All major web servers and browsers have supported at least HTTP/2 for years.
  • Fragile implementation: Concatenating multiple CSS files within the application caused path and encoding issues. The resulting files had to be stored in writable public directories, and relative paths in CSS files (such as those in @import rules) had to be parsed and adjusted. Additionally, the CSS statement @charset had to be parsed since it must appear only once per CSS file, leading to potential collisions that have never been resolved.
  • Parallel systems: Concatenation and compression were supported only for assets registered via TypoScript page.include* or the PageRenderer in PHP. The Fluid ViewHelpers f:asset.css and f:asset.script operated independently and never supported these features. Removing concatenation and compression simplifies future unification of both asset systems.
  • Performance issues with external assets: To create as few asset resources as possible, external assets (example: https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js) were by default (if excludeFromConcatenation = 1 was not explicitly set) fetched by TYPO3 when creating the page. The hashed resource path and the file content were part of the created concatenation filename. This ensured that the server-side created file was always current. TYPO3 therefore fetched external resources for each uncached page request. Worse, it fetched those external resources for each request that contained a non-cached content element ("INT" elements), and also for each request when an instance enabled CSP handling. This could easily lead to severe performance degradation.
  • Compression dependency: Pre-compression (gzip) was only available when concatenation was enabled, adding further complexity and limitations.
  • HTTP violations with caching: When TYPO3 cached a page that referenced pre-compressed assets, it stored the version based on the client’s Accept-Encoding header. Subsequent requests from clients not supporting gzip would still receive references to compressed assets, violating HTTP standards.
  • Double compression risk: Web servers might automatically re- compress already compressed files. TYPO3 tried to prevent this with an Apache-specific .htaccess configuration, but other servers like nginx required custom setups, often confusing integrators.
  • Modern compression standards: Modern web servers and browsers support more efficient algorithms such as Brotli and Zstandard, which TYPO3 never implemented due to the complexity of its existing system.
  • Minimal performance gain: The benefit of pre-compression was minor. Modern web servers can compress small assets (like typical CSS or JS files) on the fly with negligible overhead. As a rough ballpark estimation, a single CPU core can compress hundreds of MB per second, while a large JavaScript library like jQuery is usually below 100 KB. CPU time is rarely the bottleneck.

In summary, asset concatenation and pre-compression are no longer needed and should not be handled at the application level. The implementation was riddled with issues over the years and integrators struggled to reach working solutions. The Core team closed issues in this area for years with "Won't fix, use a different solution."

Alternatives 

Most TYPO3 instances can operate perfectly well without the removed asset concatenation and compression features. Modern browsers, web servers, and HTTP protocol versions provide efficient alternatives that make the previous TYPO3-internal implementation unnecessary.

If your instance still relies on concatenated or pre-built asset bundles for specific use cases, consider the following alternatives:

  • Use a modern bundler: Tools such as Vite or Webpack can handle asset concatenation, minification, and optimization during the build process. A TYPO3-specific integration is available as an extension: Vite AssetCollector.
  • Consider the `sgalinski/scriptmerger` extension: This community extension provides an alternative approach to script and stylesheet merging and compression. It may be useful for projects that cannot yet switch to build-time bundling.
  • Enable HTTP/2 or HTTP/3: Modern HTTP versions support multiplexing, allowing browsers to download multiple assets simultaneously from a single connection, eliminating the need for server-side concatenation.

    Example Apache configuration:

    <IfModule http2_module>
        Protocols h2 http/1.1
    </IfModule>
    
    # Optional: enable pre-compressed asset delivery
    AddEncoding gzip .gz
    AddType "text/javascript" .js.gz
    AddType "text/css" .css.gz
    <FilesMatch "\.(js|css)\.gz$">
        ForceType text/plain
        Header set Content-Encoding gzip
    </FilesMatch>
    Copied!

    Example nginx configuration:

    # Enable HTTP/2 on your SSL virtual host
    server {
        listen 443 ssl http2;
        server_name example.com;
    
        ssl_certificate /etc/ssl/certs/example.crt;
        ssl_certificate_key /etc/ssl/private/example.key;
    
        # Serve pre-compressed assets if available
        gzip_static on;
        # Optionally also enable on-the-fly compression
        gzip on;
        gzip_types text/css application/javascript;
    
        [...]
    }
    Copied!

    Both configurations ensure that compressed versions of static assets (e.g., .js.gz or .css.gz) are automatically delivered to clients that support gzip encoding.

    Both Apache and nginx can also cache the compressed output in memory or on disk to avoid runtime overhead. The keywords to look for are mod_deflate with mod_cache for Apache, and proxy_cache for nginx.

  • Use a Content Delivery Network (CDN): For TYPO3 instances experiencing heavy frontend traffic or high asset load, a CDN can serve static resources such as CSS, JavaScript, and images directly from distributed edge servers. This offloads delivery from the main web server, reduces latency, and improves caching efficiency.

In summary, most TYPO3 setups can safely rely on HTTP/2, modern build pipelines, and proper web server or CDN configuration to achieve optimal frontend performance without any TYPO3-internal concatenation or compression.

Impact 

The configuration toggles mentioned above are now obsolete, and TYPO3 will no longer concatenate or compress included assets.

Affected installations 

Instances that configured $GLOBALS['TYPO3_CONF_VARS']['FE']['compressionLevel'] to non-zero values and enabled the TypoScript settings described above for asset concatenation or compression are affected.

Migration 

Consider one or more of the alternatives outlined above.

Breaking: #108084 - Allow rootless paths in URI implementation 

See forge#108084

Description 

Previously, the TYPO3 implementation of \UriInterface always prefixed rootless paths (paths without a preceding slash) with a slash. With this normalization in place, it was impossible to represent rootless paths. This has now changed so that a slash is only prepended to the path when an authority (host name) is present.

Example 

Input: rootless/path/

Examples with different URIs
use TYPO3\CMS\Core\Http\Uri;

$uri = new Uri('rootless/path/');
$uriAsString = (string)$uri;
// before: /rootless/path/
// after: rootless/path/

// Same behavior with authority
$uri = (new Uri('https://example.com'))->withPath('rootless/path/');
$uriAsString = (string)$uri;
// before: https://example.com/rootless/path/
// after: https://example.com/rootless/path/

// Colon in first path segment
$uri = new Uri('rootless:path/to/resource');
$uriAsString = (string)$uri;
// before: /rootless:path/to/resource/
// after: ./rootless:path/to/resource/
Copied!

Impact 

Regarding top level TYPO3 API and functionality, nothing has changed. Required TYPO3 code has been adapted.

Third party code that uses the Uri class directly will get different results when representing rootless paths without authority. Code that relied on the normalization done by TYPO3 before is likely to break.

Since TYPO3 is always dealing with absolute paths, due to URL rewriting in the backend and the frontend, it is unlikely that much third party code relies on relative paths, so the impact is expected to be low.

Affected installations 

Third party code that is using the Uri class directly and that is representing rootless paths without authority.

Breaking: #108093 - Add respectSubmittedDataValue argument in password ViewHelper 

See forge#108093

Description 

The <f:form.password> ViewHelper now provides the argument respectSubmittedDataValue, which allows configuration of whether a submitted field value will be put into the HTML response of the form on validation errors after submission. The default value of the new argument is set to false, resulting in a submitted field value being cleared on validation errors of the form.

Impact 

A submitted password will not remain as the value for the password field if form validation fails.

Affected installations 

TYPO3 instances using the <f:form.password> ViewHelper.

Migration 

If the submitted field value of the f:form.password ViewHelper must remain on validation errors of the form, users must adapt the password ViewHelper usage as shown below:

<f:form.password name="myPassword" respectSubmittedDataValue="1" />
Copied!

Breaking: #108097 - MailMessage->send() removed 

See forge#108097

Description 

Class \TYPO3\CMS\Core\Mail\MailMessage is a data object that should not contain service methods like send(). The following methods have been removed from this class:

  • send()
  • isSent()

Impact 

Using the removed methods on instances of this class will raise fatal PHP errors.

Affected installations 

Instances that create MailMessage objects and call send() or isSent() are affected. The extension scanner is not configured to find affected code since the method names are too generic.

Migration 

The service (usually a controller class) that sends emails should be reconfigured to get an instance of MailerInterface injected and should use that service to call send().

Example before:

use TYPO3\CMS\Core\Mail\MailMessage;

final readonly class MyController
{
    public function sendMail()
    {
        $email = new MailMessage();
        $email->subject('Some subject');
        $email->send();
    }
}
Copied!

Example after:

use TYPO3\CMS\Core\Mail\MailMessage;
use TYPO3\CMS\Core\Mail\MailerInterface;

final readonly class MyController
{
    public function __construct(
        private MailerInterface $mailer
    ) {}

    public function sendMail()
    {
        $email = new MailMessage();
        $email->subject('Some subject');
        $this->mailer->send($email);
    }
}
Copied!

Breaking: #108113 - Globals _GET and _POST not reset to current Request data anymore 

See forge#108113

Description 

The frontend and backend application chain roughly splits like this:

  1. Bootstrap
  2. Create Request object from globals
  3. Start application
  4. Run middleware chain
  5. Run RequestHandler to create a Response by calling controllers (backend) or ContentObjectRenderer (frontend)

There was old compatibility code in RequestHandler that reset the PHP global variables _GET, _POST, HTTP_GET_VARS and HTTP_POST_VARS to values that may have been written to their Request object counterparts by middlewares.

This backwards compatibility layer has been removed.

Additionally, in frontend rendering, the global variable $GLOBALS['TYPO3_REQUEST'] is no longer populated within the PrepareTypoScriptFrontendRendering middleware. It is now set later in RequestHandler . $GLOBALS['TYPO3_REQUEST'] itself is another compatibility layer that the TYPO3 Core aims to phase out over time.

Impact 

The impact is twofold:

  • Some TYPO3 Core middlewares manipulate the Request object's "GET" parameter list ( $request->getQueryParams()) to, for example, resolve the frontend slug into the page uid. This is itself a backwards compatibility layer. Frontend-related code can no longer expect these manipulated variables to exist in the globals _GET, _POST, HTTP_GET_VARS and HTTP_POST_VARS.
  • Middlewares that are executed after PrepareTypoScriptFrontendRendering (middleware key typo3/cms-frontend/prepare-tsfe-rendering) can no longer rely on $GLOBALS['TYPO3_REQUEST'] being set.

Affected installations 

Instances running code that relies on the removed compatibility layers may fail or lead to unexpected results.

Migration 

Middlewares receive the Request object directly and should use it instead of fetching it from $GLOBALS['TYPO3_REQUEST']. Services triggered by middlewares that rely on the Request should have it passed in explicitly. One example frequently used in middlewares is ContentObjectRenderer :

use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;

$cor = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$cor->setRequest($request);
$result = $cor->doSomething();
Copied!

Code should in general never rely on the globals _GET, _POST, HTTP_GET_VARS and HTTP_POST_VARS. Request-related state should always be fetched from the Request object. Note that the helper method GeneralUtility::getIndpEnv() will also be phased out once the TYPO3 Core has removed its last remaining usages.

Breaking: #108148 - CDATA sections in Fluid templates no longer removed 

See forge#108148

Description 

Previous versions of Fluid and TYPO3 removed code wrapped in <![CDATA[ ]]> from template files altogether. This meant that it was possible to use CDATA to comment out template code. This is no longer possible, since CDATA sections are now interpreted by Fluid in a different way; see Feature: #108148 - Alternative Fluid Syntax for CDATA Sections.

Impact 

<![CDATA[ ]]> can no longer be used to comment out code in Fluid template files.

Affected installations 

Installations that contain Fluid templates using <![CDATA[ ]]> to comment out code are affected. A deprecation has been written to the deprecation log since TYPO3 13.4.21 if this construct is encountered in a Fluid template during rendering.

Migration 

To comment out code in Fluid templates, the Comment ViewHelper <f:comment> should be used. Since TYPO3 v13, potential Fluid syntax errors are ignored by this ViewHelper, which allows commenting out invalid Fluid syntax safely.

Breaking: #108148 - Fluid 5.0 

See forge#108148

Description 

Fluid 5.0 removes pre-announced deprecations that were introduced with Fluid 2.x and 4.x.

Impact 

Installations that use methods that were deprecated with Fluid 2.x or Fluid 4.x will now encounter PHP errors.

Affected installations 

Breaking changes are listed in the Fluid documentation:

Changelog 5.x

Migration 

Noteworthy deprecations and mitigations have already been communicated with TYPO3 changelog entries in 13.x:

Deprecation items in Fluid changelogs might contain additional hints:

Breaking: #108148 - Disallow Fluid variable names with underscore prefix 

See forge#108148

Description 

With Fluid 5, it is no longer possible to define custom template variables that start with an underscore (_). These variable names are reserved for future internal use by Fluid itself, similarly to the already existing {_all}.

Impact 

This change affects ViewHelpers that define new variables, such as:

It also affects Fluid's PHP APIs, namely $view->assign() and $view->assignMultiple().

Affected installations 

Installations with Fluid templates that use custom variable names starting with an underscore (_) will encounter exceptions when such a template is rendered. A deprecation has been written to the deprecation log since TYPO3 13.4.21 if this is encountered in a Fluid template during rendering.

For template files already using the new *.fluid.* file extension, the built-in template analyse command will discover affected template files:

vendor/bin/typo3 fluid:analyse
Copied!

For each affected template, the command will output an error like this:

[ERROR] packages/myext/Resources/Private/Templates/Test.fluid.html: Variable identifiers cannot start with a "_": _myvariable
Copied!

Migration 

The following examples no longer work with Fluid 5:

<f:variable name="_temp" value="a temporary value" />
{_temp}
Copied!
<f:for each="{myArray}" as="_item">
    {_item}
</f:for>
Copied!
<f:render partial="Footer" arguments="{_data: myData}" />
Copied!
$view->assign('_data', $myData);
$view->assignMultiple([
    '_data' => $myData,
]);
Copied!

All examples lead to the following exception:

#1756622558 TYPO3Fluid\Fluid\Core\Variables\InvalidVariableIdentifierException
Variable identifiers cannot start with a "_": _myVariable
Copied!

In all cases, the variable name must be changed to no longer start with an underscore (_).

Note that this only affects variable names, not property names in objects or array keys that are accessed within a Fluid template. The following examples are not affected by this change:

{myArray._myKey}
{myObject._myProperty}
Copied!

Also note that the existing {_all} (and any further internal variables added by Fluid) are not affected. This code will continue to work:

<f:render partial="Footer" arguments="{_all}"/>
Copied!

Breaking: #108148 - Strict Types in Fluid ViewHelpers 

See forge#108148

Description 

With Fluid 5, various changes have been made to use stricter types in the context of ViewHelpers. This has consequences in three areas:

  • Validation of arguments passed to ViewHelpers (see #1194 on GitHub)
  • Passing null values to ViewHelpers that generate a HTML tag, also known as tag-based ViewHelpers (see #1233 on GitHub)
  • Required type declarations for custom ViewHelper implementations (see #1219 on GitHub)

Impact 

ViewHelper argument validation 

Fluid ViewHelpers now use stricter validation for their arguments by default. The previous argument validation had numerous blind spots, which meant that ViewHelper implementations couldn't really rely on the type requirements specified in the ViewHelper's API. The new implementation performs a stricter validation, which means that Fluid might reject arguments passed to ViewHelpers that were previously considered valid (but which the ViewHelper in question usually didn't know how to handle). The new implementation does however deal with simple type conversions automatically, so that a ViewHelper that requires a string still can receive an int as input.

For integrators, this change might reject certain ViewHelper arguments that were previously valid, but not covered by the ViewHelper's specified API.

For developers of custom ViewHelpers, this change allows to get rid of custom validation logic that was previously necessary due to Fluid's spotty validation.

Note that the Argument ViewHelper <f:argument>, which can be used to define an API for a template, is not affected by this change, as it already used the improved validation from the beginning.

Passing null to tag-based ViewHelpers 

Previously, Fluid's TagBuilder class, which is used to create HTML tags in tag-based ViewHelpers, treated null values as empty strings, leading to an HTML tag with an empty HTML attribute. With Fluid 5, null values lead to the HTML attribute being omitted from the resulting HTML tag.

Example:

<f:form.textfield name="myTextBox" placeholder="{variableThatMightBeNull}" />
Copied!

If the variable is null (the PHP value), Fluid 4 and below generated the following output:

<input type="text" name="myTextBox" placeholder="" />
Copied!

Fluid 5 omits the placeholder="":

<input type="text" name="myTextBox" />
Copied!

In most cases, the impact of this change is non-existent. However, there are some edge cases where this change is relevant. In TYPO3 Core, the <f:image> ViewHelper needed to be adjusted to always render the alt attribute, even if its internal value is null, to match the previous output and to produce valid HTML code.

TYPO3 Core ships with the following tag-based ViewHelpers:

  • <f:media> and <f:image>
  • <f:asset.css> and <f:asset.script>
  • <f:form> and <f:form.*>
  • <f:link.*>, except for <f:link.typolink>, which uses TypoScript internally
  • <f:be.link>
  • <be:link.*>
  • <be:thumbnail>

Type declarations in ViewHelper classes 

Fluid's ViewHelperInterface now requires proper return types for all ViewHelper methods. Thus, custom ViewHelper implementations need to be adjusted accordingly. This is backwards-compatible to previous TYPO3 versions.

Affected installations 

All installations need to verify that

  • ViewHelpers aren't called with invalid argument types
  • null values passed to tag-based ViewHelpers don't lead to unexpected HTML output
  • Custom ViewHelper implementations specify proper return types

Migration 

Custom ViewHelper implementations need to make sure that they declare proper return types in the ViewHelper class to conform to Fluid 5's interface changes, for example:

  • initializeArguments() must specify void as return type
  • render() should specify a return type other than void. Even though a specific type is recommended, it is not required, and mixed can be used as well.

Note that properties in ViewHelper classes are not affected. The following example doesn't need to be adjusted, no types can/should be specified for these properties:

class MyViewHelper extends AbstractViewHelper
{
    protected $escapeOutput = false;
    protected $escapeChildren = false;
}
Copied!

Unfortunately, the other changes concern runtime characteristics of Fluid templates, as they depend on the concrete values of variables that are passed to a template. Thus, it is not possible to scan for affected templates automatically.

However, the majority of issues these changes in Fluid might uncover in existing projects would have already been classified as a bug (in the template or extension code) before this Fluid change, such as passing an array to a ViewHelper that expects a string.

Breaking: #108277 - Remove superfluous CacheHashCalculator public methods 

See forge#108277

Description 

The following superfluous public methods have been removed:

  • \TYPO3\CMS\Frontend\Page\CacheHashCalculator::setConfiguration()
  • \TYPO3\CMS\Frontend\Page\CacheHashConfiguration::with()

Impact 

Calling the removed methods will result in a fatal PHP error.

Both methods were only used internally for testing purposes and were not part of the public API contract.

Affected installations 

TYPO3 installations that used these methods directly in custom code are affected. However, this is highly unlikely as they were intended for internal testing only.

Migration 

Instead of modifying configuration after instantiation using the removed setConfiguration() method, merge configuration arrays before creating the CacheHashConfiguration instance, then pass it to the CacheHashCalculator constructor.

Example:

// Before (removed approach):
$subject = new CacheHashCalculator(
    new CacheHashConfiguration($baseConfiguration),
    $hashService
);
$subject->setConfiguration([
    'cachedParametersWhiteList' => ['whitep1', 'whitep2'],
]);

// After (correct approach):
$configuration = new CacheHashConfiguration(
    array_merge($baseConfiguration, [
        'cachedParametersWhiteList' => ['whitep1', 'whitep2'],
    ])
);
$subject = new CacheHashCalculator($configuration, $hashService);
Copied!

Breaking: #108304 - Populate extension title from composer.json 

See forge#108304

Description 

To avoid reading the legacy ext_emconf.php file even in Composer mode, the extension title is now optionally pulled from the composer.json description. If the character sequence - (space, dash, space) is present in description field in composer.json, then everything before this sequence is used as title of the extension and the second part is used as extension description. Note that only the first occurrence of this sequence is evaluated, so it remains possible to have this inside the extension description if required.

Impact 

Extensions not having their title incorporated in the composer.json description field, will be shown with the full description in extension manager and from command line with typo3 extension:list command.

Affected installations 

Installations having custom extensions, where the title is not part of the description in composer.json

Migration 

Put the desired extension title into the composer.json description field and separate it from the description with - (space, dash, space), or use the extension manager "Composer Support of Extensions" to get a suggestion for updating composer.json files accordingly.

All TYPO3 core extensions have set their description in composer.json accordingly already.

Example of description with title in composer.json:

ext_emconf.php
<?php

$EM_CONF[$_EXTKEY] = [
    'title' => 'TYPO3 CMS Backend User',
    'description' => 'TYPO3 backend module System>Backend Users for managing backend users and groups.',
    // ...
];
Copied!
composer.json
{
    "name": "typo3/cms-beuser",
    "type": "typo3-cms-framework",
    "description": "TYPO3 CMS Backend User - TYPO3 backend module System>Backend Users for managing backend users and groups.",
}
Copied!

Breaking: #108310 - Require composer.json in classic mode 

See forge#108310

Description 

Extension detection in classic mode now requires a valid composer.json file instead of ext_emconf.php. The composer.json file must include "type": "typo3-cms-*" and the extension key in extra.typo3/cms.extension-key.

Impact 

Extensions without a valid composer.json are no longer detected and loaded in classic mode installations.

Affected installations 

All classic mode installations must verify that every extension contains a composer.json with:

  • "type" starting with "typo3-cms-"
  • "extra.typo3/cms.extension-key" containing the extension key

Composer-based installations are not affected.

Migration 

Extension authors must ensure their extensions include a valid composer.json. TER extensions have required this since 2021.

Example composer.json:

{
    "name": "vendor/extension-name",
    "type": "typo3-cms-extension",
    "extra": {
        "typo3/cms": {
            "extension-key": "extension_name"
        }
    }
}
Copied!

Feature: #92760 - Configurable timezone for DateViewHelper 

See forge#92760

Description 

A new option timezone has been added to the \TYPO3Fluid\Fluid\ViewHelpers\Format\DateViewHelper to render a date with a provided time zone.

<f:format.date format="d.m.Y g:i a" date="1640995200" /><br>
<f:format.date format="d.m.Y g:i a" date="1640995200" timezone="America/Phoenix" /><br>
<f:format.date format="d.m.Y g:i a" date="1640995200" timezone="Indian/Mayotte" />
Copied!

will render:

01.01.2022 12:00 am
31.12.2021 5:00 pm
01.01.2022 3:00 am
Copied!

Impact 

Using the new timezone option, it is now possible to set a specific time zone for the DateViewHelper.

Feature: #93334 - Translation Domain Mapping 

See forge#93334

Description 

Translation domains have been introduced as a shorter alternative to file-based references for label resources (.xlf XLIFF files). The syntax uses the format package[.subdomain...].resource and is fully backward compatible with existing LLL:EXT: references. Package refers to the extension key, such as "backend" for "EXT:backend".

This syntax is designed to improve readability, remove explicit references to file extensions, and provide convenience for new developers and integrators. The previous locallang.xlf convention has been replaced with a more generic "messages" resource name, following common conventions in other localization systems (for example Symfony). This is also where the term translation domain originates.

Example:

// Domain-based reference
$languageService->sL('backend.toolbar:save');

// Equivalent file-based reference (still supported)
$languageService->sL(
    'LLL:EXT:backend/Resources/Private/Language/locallang_toolbar.xlf:save'
);
Copied!

Translation Domain Format 

The format defines two parts: the package part (extension key) and the resource part, separated by a dot.

The resource part omits historical namings such as locallang.xlf and the locallang_ prefix. The actual label identifier is separated by a colon.

Format 

Example usage of "package.resource:identifier"
$languageService->sL('backend.toolbar:save');
// Resolves to: EXT:backend/Resources/Private/Language/locallang_toolbar.xlf
// and returns the translated "save" identifier.
Copied!

Domain Resolution 

Deterministic File-Based Mapping 

Translation domains are resolved deterministically by scanning the file system. When a domain is first requested for a package:

  1. All label files in Resources/Private/Language/ are discovered.
  2. A domain name is generated from each file name.
  3. The domain-to-file mapping is cached in cache.l10n.
  4. Subsequent requests use the cached mapping.

This ensures that domain names always correspond to existing files and avoids speculative file system lookups.

When there are filename conflicts such as locallang_db.xlf and db.xlf, then locallang_db.xlf will be ignored.

Performance Characteristics 

The implementation reduces file system operations compared to traditional file-based lookups, as all label files within an extension are discovered once.

Domain Generation Rules 

Domain names are generated from file paths using these transformation rules:

  1. The base path Resources/Private/Language/ is omitted.
  2. Standard filename patterns:

    • locallang.xlf.messages
    • locallang_toolbar.xlf.toolbar
    • locallang_sudo_mode.xlf.sudo_mode
  3. Subdirectories use dot notation:

    • Form/locallang_tabs.xlf.form.tabs
  4. Site Set labels receive the .sets prefix:

    • Configuration/Sets/Felogin/labels.xlf.sets.felogin
  5. Case conversion:

    • UpperCamelCase → snake_case (SudoModesudo_mode)
    • snake_case → preserved (sudo_modesudo_mode)
  6. Locale prefixes are ignored for domain name generation but properly evaluated for locale-specific translations:

    • (de.locallang.xlfmessages)
    • (de-AT.tabs.xlftabs)

Examples:

File Path                                     → Domain
────────────────────────────────────────────────────────────
EXT:backend/.../locallang.xlf                 → backend.messages
EXT:backend/.../locallang_toolbar.xlf         → backend.toolbar
EXT:core/.../Form/locallang_tabs.xlf          → core.form.tabs
EXT:felogin/Configuration/Sets/.../labels.xlf → felogin.sets.felogin
Copied!

Usage 

The translation domain system integrates with the existing \TYPO3\CMS\Core\Localization\LanguageService API. Both domain-based and file-based references are supported:

use TYPO3\CMS\Core\Localization\LanguageService;

$languageService = $this->languageServiceFactory->createFromSiteLanguage(
    $request->getAttribute('language')
);

// Domain-based reference
$label = $languageService->sL('backend.toolbar:menu.item');

// Another domain-based reference
$label = $languageService->sL('backend.messages:button.save');

// Traditional file reference (still supported)
$label = $languageService->sL(
    'LLL:EXT:backend/Resources/Private/Language/locallang.xlf:button.save'
);
Copied!

Domain-based references are shorter and reveal less implementation detail than full file paths.

CLI Command 

The development command bin/typo3 language:domain:list lists all available translation domains along with their available translations and label counts:

# List domains in active extensions
php bin/typo3 language:domain:list

# Filter by extension
php bin/typo3 language:domain:list --extension=backend
Copied!

Output:

+--------------------+---------------------------------------+----------+
| Translation Domain | Label Resource                        | # Labels |
+--------------------+---------------------------------------+----------+
| backend.messages   | EXT:backend/.../locallang.xlf         | 84       |
| backend.toolbar    | EXT:backend/.../locallang_toolbar.xlf | 42       |
+--------------------+---------------------------------------+----------+
Copied!

The Labels column displays the number of translatable labels within the English source file.

On top of this, the development command bin/typo3 language:domain:search can be used to search for specific label contents. Both commands are provided in the EXT:lowlevel extension.

PSR-14 Event 

The event \TYPO3\CMS\Core\Localization\Event\BeforeLabelResourceResolvedEvent is dispatched after domain generation, allowing customization of domain names.

The event provides these public properties:

  • $packageKey — The extension key (read-only).
  • $domains — An associative array mapping domain names to label files (modifiable): array<string, string>.

Example 

Event listener implementation:

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Localization\Event\BeforeLabelResourceResolvedEvent;

final readonly class CustomTranslationDomainResolver
{
    #[AsEventListener(identifier: 'my-extension/custom-domain-names')]
    public function __invoke(BeforeLabelResourceResolvedEvent $event): void
    {
        if ($event->packageKey !== 'my_extension') {
            return;
        }

        // Use file my_messages.xlf even if locallang.xlf is found
        $event->domains['my_extension.messages'] =
            'EXT:my_extension/Resources/Private/Language/my_messages.xlf';
    }
}
Copied!

Backend modules 

Previously, backend module labels (including their title and description) were defined in a file like this:

EXT:my_extension/Resources/Private/Language/locallang_mod.xlf
<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
    <file source-language="en" datatype="plaintext" original="EXT:my_extension/Resources/Private/Language/locallang_mod.xlf" date="2038-10-28T13:37:37Z" product-name="mymodule">
        <header/>
        <body>
            <trans-unit id="mlang_labels_tablabel">
                <source>My module</source>
            </trans-unit>
            <trans-unit id="mlang_labels_tabdescr">
                <source>Shows my module.</source>
            </trans-unit>
            <trans-unit id="mlang_tabs_tab">
                <source>My label</source>
            </trans-unit>
        </body>
    </file>
</xliff>
Copied!

and utilized via the module definition:

EXT:my_extension/Configuration/Backend/Modules.php
<?php
return [
    'my_module' => [
        'parent' => 'web',
        'position' => ['after' => 'web_list'],
        'access' => 'user',
        'path' => '/module/my-module',
        'iconIdentifier' => 'my-module-icon',
        'labels' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang_mod.xlf',
        'aliases' => ['web_MyModule'],
        'routes' => [
            '_default' => [
                'target' => MyController::class . '::handleRequest',
            ],
        ],
    ],
];
Copied!

Now, labels can use more speaking identifiers:

EXT:my_extension/Resources/Private/Language/Module/mymodule.xlf
<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
    <file source-language="en" datatype="plaintext" original="EXT:my_extension/Resources/Private/Language/Modules/mymodule.xlf" date="2026-11-05T16:22:37Z" product-name="mymodule">
        <header/>
        <body>
            <trans-unit id="short_description">
                <source>My module</source>
            </trans-unit>
            <trans-unit id="description">
                <source>Shows my module.</source>
            </trans-unit>
            <trans-unit id="title">
                <source>My label</source>
            </trans-unit>
        </body>
    </file>
</xliff>
Copied!
EXT:my_extension/Configuration/Backend/Modules.php
<?php
return [
    'my_module' => [
        'parent' => 'web',
        'position' => ['after' => 'web_list'],
        'access' => 'user',
        'path' => '/module/my-module',
        'iconIdentifier' => 'my-module-icon',
        'labels' => 'my_extension.modules.my_module',
        'aliases' => ['web_MyModule'],
        'routes' => [
            '_default' => [
                'target' => MyController::class . '::handleRequest',
            ],
        ],
    ],
];
Copied!

The naming for the short-hand translation domain for modules should follow the following pattern as best practice:

  • <extensionkey>.modules.<modulename> - when multiple modules exist for an extension. Both extensionKey and modulename should use lower snake case ("some_long_module_name"), ideally without underscores (qrcode.modules.generator is more readable than qrcode.modules.backend_image_generator for example). Files are put into EXT:extensionkey/Resources/Private/Languages/Modules/modulename.xlf.
  • <extensionkey>.module - single backend module only The file is saved as EXT:extensionkey/Resources/Private/Languages/module.xlf.

To summarize, the key changes are:

  1. Use a speaking XLIFF file inside /Resources/Private/Languages/Modules (best practice, could be any sub-directory)
  2. Use understandable XLIFF identifiers: - "title" instead of "mlang_tabs_tab" - "short_description" instead of "mlang_labels_tablabel" - "description" instead of "mlang_labels_tabdescr"
  3. Use short-form identifiers ("my_extension.modules.my_module" instead of "LLL:EXT:my_extension/Resources/Private/Language/locallang_mod.xlf") inside the Backend/Modules.php registration.

All TYPO3 Core backend modules that used the old label identifiers have been migrated to the new syntax, the utilized files are now deprecated, see deprecation. TYPO3 Core also uses singular module language containers like workspaces.module instead of workspaces.modules.workspaces.

Impact 

Translation domains provide a shorter, more readable alternative to file-based label references. The implementation uses deterministic file-system scanning with per-package caching to reduce lookups.

All existing LLL:EXT: file references continue to work. Translation domains are optional and can be adopted incrementally. Both syntaxes can coexist in the same codebase. This affects TypoScript, Fluid <f:translate> usages, TCA configuration, and PHP code using the LanguageService API.

TYPO3 Core will gradually migrate internal references to translation domains over time, increasing readability—especially in Fluid templates or TCA definitions.

Technical components:

\TYPO3\CMS\Core\Localization\TranslationDomainMapper
Maps domains to file paths and manages the cache.
\TYPO3\CMS\Core\Localization\LabelFileResolver
Discovers label files and handles locale resolution.
\TYPO3\CMS\Core\Localization\LocalizationFactory
Integrates domain resolution transparently.

The TranslationDomainMapper automatically detects EXT: file references and passes them through unchanged.

Feature: #93981 - Specify default image conversion processing 

See forge#93981

Description 

Image processing in TYPO3 can now configure default and specific formats to use when images are rendered or converted in the frontend, for example in Fluid:

Example of image rendering in Fluid
<f:image src="{asset.path}" width="200" />
<f:image src="fileadmin/someFile.jpg" width="200" />
<f:image image="{someExtbaseObject.someAsset}" width="200" />
Copied!

Depending on the TYPO3 version, processed images were rendered with .png (or earlier, .gif or .png) file extensions, as long as the fileExtension parameter with a fixed output format was not specified.

This default solution had two major drawbacks:

  1. The default file format was hardcoded and not configurable.
  2. Utilizing new file formats (like webp and avif) required code changes.

This has now been changed with the new configuration option $GLOBALS['TYPO3_CONF_VARS']['GFX']['imageFileConversionFormats'] .

This variable defaults to:

$GLOBALS['TYPO3_CONF_VARS']['GFX']['imageFileConversionFormats'] = [
    'jpg' => 'jpg',
    'jpeg' => 'jpeg',
    'gif' => 'gif',
    'png' => 'png',
    'svg' => 'svg',
    'default' => 'png',
];
Copied!

This means:

  • When resizing or cropping an image with a file extension of jpg, jpeg, gif, png, or svg (and not setting a specific fileExtension target format), those images will retain their respective file formats.
  • Otherwise, the file format png is used.

Related configuration options that still apply:

  • $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'] Current default: gif,jpg,jpeg,tif,tiff,bmp,pcx,tga,png,pdf,ai,svg,webp,avif
  • $GLOBALS['TYPO3_CONF_VARS']['SYS']['textfile_ext'] Current default: txt,ts,typoscript,html,htm,css,tmpl,js,sql,xml,csv,xlf,yaml,yml
  • $GLOBALS['TYPO3_CONF_VARS']['SYS']['mediafile_ext'] Current default: gif,jpg,jpeg,bmp,png,webp,pdf,svg,ai,mp3,wav,mp4,ogg,flac,opus,webm, youtube,vimeo,avif

These still define, per installation, which files can be uploaded or used as images.

If a new format like heic becomes supported by the used graphics engine (for example GraphicsMagick or ImageMagick), system maintainers can add the file extension to imagefile_ext. TYPO3 will then recognize this format and convert it to the default target format (png by default) when processing images.

If the format should also be available as a target format, add 'heic' => 'heic' to $GLOBALS['TYPO3_CONF_VARS']['GFX']['imageFileConversionFormats'] . Otherwise, heic images can be selected, but processing will still produce png.

If all image processing (resizing, thumbnails, PDF previews, etc.) should use heic, set 'default' => 'heic' and remove other entries.

Currently, the option cannot be configured via System > Settings > Configure options ... because it uses an array syntax. It can be changed manually in settings.php. GUI support may be added in a future release.

The array notation allows defining a target (thumbnail) file extension for each original file extension individually using the format {originalExtension} => {targetExtension}.

Example:

Individual thumbnail formats per file extension
$GLOBALS['TYPO3_CONF_VARS']['GFX']['imageFileConversionFormats'] = [
    'jpg' => 'jpg',
    'svg' => 'svg',
    'ai' => 'png',
    'heic' => 'webp',
    'default' => 'avif',
];
Copied!

This configuration would produce the following behavior:

Fluid image rendering results
 <f:image src="somefile.jpg" width="80">
  -> renders an 80 px thumbnail from "somefile.jpg" to "somefile.jpg"
     (rule: "jpg => jpg")

<f:image src="somefile.gif" width="80">
  -> renders an 80 px thumbnail from "somefile.gif" to "somefile.avif"
     (rule: "default => avif")

<f:image src="somefile.png" width="80">
  -> renders an 80 px thumbnail from "somefile.png" to "somefile.avif"
     (rule: "default => avif")

<f:image src="somefile.svg" width="80">
  -> renders the original SVG at 80 px width
     (rule: "svg => svg")

<f:image src="somefile.pdf" width="80">
  -> renders an 80 px PDF thumbnail to "somefile.avif"
     (rule: "default => avif")

<f:image src="somefile.heic" width="80">
  -> renders an 80 px thumbnail from "somefile.heic" to "somefile.webp"
     (rule: "heic => webp")
Copied!

Impact 

TYPO3 is now more future-proof regarding new image formats and allows modern file formats to be used by configuration.

Projects can now specify precisely how each format should be converted or which default format should be used for processed images.

Feature: #97559 - Support property-based configuration for Extbase attributes 

See forge#97559

Description 

PHP attributes in the Extbase context can now be configured using properties instead of an array of configuration values. This resolves a limitation that existed since the introduction of Extbase annotations in TYPO3 v9, where annotation configuration was restricted: all available options needed to be defined in a single array.

Since annotations were removed with forge#107229 in favor of PHP attributes, configuration options can now be defined in a more flexible and type-safe way.

Example usage 

use TYPO3\CMS\Extbase\Attribute\FileUpload;
use TYPO3\CMS\Extbase\Attribute\Validate;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Domain\Model\FileReference;

class MyModel extends AbstractEntity
{
    #[Validate(validator: 'NotEmpty')]
    protected string $foo = '';

    #[FileUpload(
        validation: [
            'required' => true,
            'maxFiles' => 1,
            'fileSize' => ['minimum' => '0K', 'maximum' => '2M'],
            'allowedMimeTypes' => ['image/jpeg', 'image/png'],
        ],
        uploadFolder: '1:/user_upload/files/',
    )]
    protected ?FileReference $bar = null;
}
Copied!

Impact 

This patch serves as a follow-up to forge#107229 and aims to improve the attribute configuration mechanism by using constructor property promotion in combination with strictly typed properties.

To maintain backwards compatibility, the first property of each attribute still accepts an array of configuration options. However, this behavior is deprecated and will be removed in TYPO3 v15.0 (see deprecation notice).

Developers are advised to migrate to single-property configuration when using PHP attributes in Extbase.

Feature: #98239 - PSR-14 Event to modify form after being built 

See forge#98239

Description 

A new PSR-14 event \TYPO3\CMS\Form\Event\AfterFormIsBuiltEvent has been introduced which serves as an improved replacement for the now removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterBuildingFinished'] .

The event provides the $form public property.

Example 

An example event listener could look like the following:

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Form\Event\AfterFormIsBuiltEvent;

class MyEventListener
{
    #[AsEventListener(
        identifier: 'my-extension/after-form-is-built',
    )]
    public function __invoke(AfterFormIsBuiltEvent $event): void
    {
        $event->form->setLabel('foo');
    }
}
Copied!

Impact 

With the new AfterFormIsBuiltEvent, it is now possible to modify the form definition after it has been built.

Feature: #99065 - Detail view for backend user groups in 'Users' module 

See forge#99065

Description 

The Administration > Users module has been extended with a new detail view for backend user groups, complementing the existing detail view for individual backend users. This comprehensive view provides administrators with complete visibility into backend user group configurations and their calculated properties.

The detail view displays:

  • Basic information about the backend user group (title, description, and so on)
  • All assigned subgroups and the full inheritance chain
  • A complete overview of permissions and access rights from the group and all inherited subgroups
  • Calculated and processed TSconfig settings showing the final effective configuration
  • Database mount points and file mount access permissions
  • Module access permissions and workspace restrictions

Impact 

TYPO3 administrators can now efficiently analyze backend user group configurations without manually tracing through complex inheritance structures. This enhanced visibility simplifies permission troubleshooting, security auditing, and group management by providing a consolidated view of all calculated permissions and settings in one place, similar to the existing backend user detail functionality.

Feature: #99409 - New PSR-14 BeforeLiveSearchFormIsBuiltEvent 

See forge#99409

Description 

A new PSR-14 event \TYPO3\CMS\Backend\Search\Event\BeforeLiveSearchFormIsBuiltEvent has been added.

To modify the live search form data, the following methods are available:

  • addHint(): Adds a single hint.
  • addHints(): Adds one or multiple hints.
  • setHints(): Sets hints and can be used to reset or overwrite the current ones.
  • getHints(): Returns all current hints.
  • getRequest(): Returns the current PSR-7 request.
  • getSearchDemand(): Returns the SearchDemand used by the live search.
  • setSearchDemand(): Sets a custom SearchDemand object.
  • getAdditionalViewData(): Returns the additional view data set to be used in the template.
  • setAdditionalViewData(): Sets the additional view data to be used in the template.

Example 

The corresponding event listener class:

<?php

namespace MyVendor\MyPackage\Backend\Search\EventListener;

use TYPO3\CMS\Backend\Search\Event\BeforeLiveSearchFormIsBuiltEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

final class BeforeLiveSearchFormIsBuiltEventListener
{
    #[AsEventListener(
        identifier: 'my-package/backend/search/modify-live-search-form-data'
    )]
    public function __invoke(BeforeLiveSearchFormIsBuiltEvent $event): void
    {
        $event->addHints(...[
            'my-extension.messages:identifier',
        ]);
    }
}
Copied!

Impact 

With the new PSR-14 event BeforeLiveSearchFormIsBuiltEvent, it is now possible to modify the form data for the backend live search.

Feature: #99459 - Respect record type while creating new records 

See forge#99459

Description 

The "Create new record" component in the backend, which is accessible in the Content > List module, has been enhanced to automatically detect and display all available record types for tables that support sub-schemas (record types). This improvement simplifies creating specific record types and eliminates the need to change the type afterward in the editing form, which could previously lead to invalid record states being stored in the database.

The interface now features automatic record type detection. Tables with multiple record types are automatically expanded to show all available types in a collapsible dropdown interface.

The options to create new pages now also show the different page types ( doktype) as expandable options when creating new pages "inside" or "after" an existing page.

To disable direct creation of a specific record type, a new TCA option ['creationOptions']['enableDirectRecordTypeCreation'] is available at the record type level:

EXT:my_extension/Configuration/TCA/Overrides/pages.php
use TYPO3\CMS\Core\Domain\Repository\PageRepository´;

// Disable direct creation of shortcuts
$GLOBALS['TCA']['pages']['types'][(string)PageRepository::DOKTYPE_SHORTCUT]['creationOptions']['enableDirectRecordTypeCreation'] = false;
Copied!

Individual titles for each record type are automatically picked up from the type-specific title configuration in the types section (see Feature #108027), falling back to the select item label if no type-specific title is defined.

Additionally, the new PSR-14 event \TYPO3\CMS\Backend\Controller\Event\ModifyNewRecordCreationLinksEvent allows for complete customization of the creation links.

Impact 

For tables that support sub-schemas (multiple record types), the new record wizard automatically detects all available types and displays them in a collapsible interface. This includes:

  • All tables with TCA type fields (such as sys_file_collection or index_config)
  • The pages table with its different doktype values
  • Extension tables with custom record types

The ModifyNewRecordCreationLinksEvent provides complete control over the creation link structure, allowing extensions to:

  • Add custom record creation options
  • Modify existing groups and items
  • Override icons, labels, and URLs
  • Create entirely custom wizard interfaces

Data Structure 

The event works with a nested array structure representing grouped creation links:

[
    'content' => [
        'title' => 'Content',
        'icon' => '<img src="..." />',
        'items' => [
            'sys_file_collection' => [
                'label' => 'File Collection',
                'icon' => '<typo3-backend-icon ...>',
                'types' => [
                    'static' => [
                        'url' => '/typo3/record/edit?edit[sys_file_collection][1]=new&defVals[sys_file_collection][type]=static',
                        'icon' => '<typo3-backend-icon ...>',
                        'label' => 'Static File Collection'
                    ],
                    'folder' => [
                        'url' => '/typo3/record/edit?edit[sys_file_collection][1]=new&defVals[sys_file_collection][type]=folder',
                        'icon' => '<typo3-backend-icon ...>',
                        'label' => 'Folder from Storage'
                    ]
                ]
            ]
        ]
    ],
    'pages' => [
        'title' => 'Create New Page',
        'icon' => '<typo3-backend-icon ...>',
        'items' => [
            'inside' => [
                'label' => 'Page (inside)',
                'icon' => '<typo3-backend-icon ...>',
                'types' => [
                    '1' => [
                        'url' => '/typo3/record/edit?edit[pages][1]=new&defVals[pages][doktype]=1',
                        'icon' => '<typo3-backend-icon ...>',
                        'label' => 'Standard Page'
                    ],
                    '254' => [
                        'url' => '/typo3/record/edit?edit[pages][1]=new&defVals[pages][doktype]=254',
                        'icon' => '<typo3-backend-icon ...>',
                        'label' => 'Folder'
                    ]
                ]
            ]
        ]
    ]
]
Copied!

Event Listener Example 

The event provides access to:

  • $event->groupedCreationLinks - The complete structure of creation links
  • $event->pageTS - The current page's TSconfig array
  • $event->pageId - The current page ID
  • $event->request - The current server request object

This allows for comprehensive customization while maintaining backward compatibility with existing setups.

EXT:my_extension/Classes/EventListener/CustomizeNewRecordCreationLinksEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Backend\Controller\Event\ModifyNewRecordCreationLinksEvent;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;

final readonly class CustomizeNewRecordWizardEventListener
{
    public function __construct(
        private IconFactory $iconFactory,
        private UriBuilder $uriBuilder,
    ) {}

    #[AsEventListener]
    public function __invoke(ModifyNewRecordCreationLinksEvent $event): void
    {
        // Add a custom creation group
        $customGroup = [
            'title' => 'Custom Records',
            'icon' => $this->iconFactory->getIcon('apps-pagetree-category')->render(),
            'items' => [
                'tx_myext_domain_model_item' => [
                    'url' => (string)$this->uriBuilder->buildUriFromRoute('record_edit', [
                        'edit' => ['tx_myext_domain_model_item' => [$event->pageId => 'new']],
                        'returnUrl' => $event->request->getAttribute('normalizedParams')->getRequestUri(),
                    ]),
                    'icon' => $this->iconFactory->getIconForRecord('tx_myext_domain_model_item', []),
                    'label' => 'Custom Item',
                ]
            ]
        ];

        // Add the custom group to the existing structure
        $event->groupedCreationLinks['custom'] = $customGroup;

        // Modify existing groups, for example, remove specific items
        if (isset($event->groupedCreationLinks['system']['items']['sys_template'])) {
            unset($event->groupedCreationLinks['system']['items']['sys_template']);
        }

        // Add custom types to an existing table
        if (isset($event->groupedCreationLinks['content']['items']['sys_note'])) {
            $event->groupedCreationLinks['content']['items']['sys_note']['types'] = [
                'important' => [
                    'url' => (string)$this->uriBuilder->buildUriFromRoute('record_edit', [
                        'edit' => ['sys_note' => [$event->pageId => 'new']],
                        'defVals' => ['sys_note' => ['category' => '1']],
                        'returnUrl' => $event->request->getAttribute('normalizedParams')->getRequestUri(),
                    ]),
                    'icon' => $this->iconFactory->getIcon('status-dialog-warning', IconSize::SMALL),
                    'label' => 'Important Note',
                ],
                'info' => [
                    'url' => (string)$this->uriBuilder->buildUriFromRoute('record_edit', [
                        'edit' => ['sys_note' => [$event->pageId => 'new']],
                        'defVals' => ['sys_note' => ['category' => '0']],
                        'returnUrl' => $event->request->getAttribute('normalizedParams')->getRequestUri(),
                    ]),
                    'icon' => $this->iconFactory->getIcon('status-dialog-information', IconSize::SMALL),
                    'label' => 'Information Note',
                ]
            ];
        }
    }
}
Copied!

Feature: #99911 - New TCA type "country" 

See forge#99911

Description 

A new TCA field type called country has been added to TYPO3 Core. Its main purpose is to use the newly introduced Country API to provide a country selection in the backend and use the stored representation in Extbase or TypoScript output.

TCA Configuration 

The new TCA type displays all filtered countries including the configurable name and the corresponding flag.

Configuration/TCA/tx_myextension_mymodel.php
'country' => [
    'label' => 'Country',
    'config' => [
        'type' => 'country',
        // available options: name, localizedName, officialName, localizedOfficialName, iso2, iso3
        'labelField' => 'localizedName',
        // countries which are listed before all others
        'prioritizedCountries' => ['AT', 'CH'],
        // sort by the label
        'sortItems' => [
            'label' => 'asc'
        ],
        'filter' => [
            // restrict to the given country ISO2 or ISO3 codes
            'onlyCountries' => ['DE', 'AT', 'CH', 'FR', 'IT', 'HU', 'US', 'GR', 'ES'],
            // exclude by the given country ISO2 or ISO3 codes
            'excludeCountries' => ['DE', 'ES'],
        ],
        'default' => 'HU',
        // When required=false, an empty selection ('') is possible
        'required' => false,
    ],
],
Copied!

Note that extra items / countries should be added via the new PSR-14 event BeforeCountriesEvaluatedEvent.

FlexForm Configuration 

Similar keys work for FlexForms:

Configuration/FlexForms/example.xml
<settings.country>
    <label>My Label</label>
    <config>
        <type>country</type>
        <labelField>officialName</labelField>
        <prioritizedCountries>
            <numIndex index="0">AT</numIndex>
            <numIndex index="1">CH</numIndex>
        </prioritizedCountries>
        <filter>
            <onlyCountries>
                <numIndex index="0">DE</numIndex>
                <numIndex index="1">AT</numIndex>
                <numIndex index="2">CH</numIndex>
                <numIndex index="1">FR</numIndex>
                <numIndex index="3">IT</numIndex>
                <numIndex index="4">HU</numIndex>
                <numIndex index="5">US</numIndex>
                <numIndex index="6">GR</numIndex>
                <numIndex index="7">ES</numIndex>
            </onlyCountries>
            <excludeCountries>
                <numIndex index="0">DE</numIndex>
                <numIndex index="1">ES</numIndex>
            </excludeCountries>
        </filter>
        <sortItems>
            <label>asc</label>
        </sortItems>
        <default>HU</default>
        <required>1</required>
    </config>
</settings.country>
Copied!

Available config keys 

The TCA type country features the following column configuration:

  • filter (array): onlyCountries (array), excludeCountries (array) - filter/reduce specific countries
  • prioritizedCountries (array) - items put first in the list
  • default (string) - default value
  • labelField (string) - display label (one of localizedName, name, iso2, iso3, officialName, localizedOfficialName)
  • sortItems (string) - sort order (asc, desc)
  • required (bool) - whether an empty selection can be made or not

Extbase usage 

When using Extbase Controllers to fetch Domain Models containing properties declared with the Country type, these models can be used with their usual getters, and passed along to Fluid templates as usual.

Extbase Domain Model example
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Core\Country\Country;

class SomeDomainModel extends AbstractEntity
{
    protected ?Country $country = null;

    public function setCountry(?Country $country): void
    {
        $this->country = $country;
    }

    public function getCountry(): ?Country
    {
        return $this->country;
    }
}
Copied!
Extbase Controller usage
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Core\Country\Country;
use TYPO3\CMS\Core\Country\CountryProvider;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

class ItemController extends ActionController {
    // ...

    public function __construct(
        private readonly CountryProvider $countryProvider,
    ) {}

    public function singleAction(SomeDomainModel $model): ResponseInterface
    {
        // Do something in PHP, using the Country API
        if ($model->getCountry()->getAlpha2IsoCode() == 'DE') {
            $this->loadGermanLanguage();
        }
        $this->view->assign('model', $model);

        // You can access the `CountryProvider` API for additional country-related
        // operations, too (ideally use Dependency Injection for this):
        $this->view->assign('countries', $this->countryProvider->getAll());

        return $this->htmlResponse();
    }
}
Copied!
Fluid Template example
Country: {model.country.flag}
 - <span title="{f:translate(key: model.country.localizedOfficialNameLabel)}">
     {model.country.alpha2IsoCode}
   </span>
Copied!

You can use any of the getXXX() methods available from the Country API via the Fluid {model.country.XXX} accessors.

If you use common Extbase CRUD (Create/Read/Update/Delete) with models using a Country type, you can utilize the existing ViewHelper f:form.countrySelect within your <f:form> logic.

Please keep in mind that Extbase by default has no coupling (in terms of validation) to definitions made in the TCA for the properties, as with other types like file uploads or select items.

That means, if you restrict the allowed countries via filter.onlyCountries on the backend (TCA) side, you also need to enforce this in the frontend.

It is recommended to use Extbase Validators for this task. If you want to share frontend-based validation and TCA-based validation non-redundantly, you could use data objects (DO/DTO) or ENUMs for returning the list of allowed countries:

EXT:my_extension/Classes/Domain/Validator/CountryValidator.php
namespace MyExtension\Domain\Validator;

use TYPO3\CMS\Extbase\Validation\Error;
use TYPO3\CMS\Extbase\Validation\Validator\AbstractValidator;

class ItemValidator extends AbstractValidator
{
    /**
     * @param MyModel $value
     */
    protected function isValid(mixed $value): void
    {
        if ($value->getCountry() === null) {
            $error = new Error('Valid country (alpha2) must be set.', 4815162343);
            $this->result->forProperty('country')->addError($error);
        } else {
            $allowedCountries = ['DE', 'EN'];
            if (!in_array($value->getCountry()->getAlpha2IsoCode(), $allowedCountries)) {
                $error = new Error('Country ' . $value->getCountry()->getAlpha2IsoCode() . ' not allowed.', 4815162344);
                $this->result->forProperty('country')->addError($error);
            }
        }
    }
}
Copied!
EXT:my_extension/Classes/Controller/ItemController.php (excerpt)
namespace MyExtension\Controller;

use TYPO3\CMS\Extbase\Annotation\Validate;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
use MyExtension\Domain\Model\Item;
use MyExtension\Domain\Validator\ItemValidator;

final class ItemController extends ActionController
{
    // Excerpt ...

    #[Validate([
        'param' => 'item',
        'validator' => CountryValidator::class,
    ])]
    public function createAction(Item $item): ResponseInterface
    {
        $this->itemRepository->add($item);
        return $this->htmlResponse();
    }

    // ...
}
Copied!

A full working example, including an Extbase CRUD setup, can be found in EXT:tca_country_example Demo Extension.

Extbase / Fluid localization 

The type Country does not point to a real Extbase model, and thus has no inherent localization or query logic based on real records. It is just a pure PHP data object with some getters, and a magic __toString() method returning a language label (LLL:...) translation key for the name of the country ( Country->getLocalizedNameLabel()).

Here are some examples how to access them and provide localization:

EXT:my_extension/Resources/Private/Templates/Show.html
<f:comment>Will show something like "AT" or "DE"</f:comment>
Country ISO2:
    {item.country.alpha2IsoCode}

<f:comment>Will show something like "CHE"</f:comment>
Country ISO3:
    {item.country.alpha3IsoCode}

<f:comment>Will show something a flag (UTF-8 character)</f:comment>
Country flag:
    {item.country.flag}

<f:comment>Will show something like "LLL:EXT:core/Resources/Private/Language/Iso/countries.xlf:AT.name"</f:comment>
Country LLL label:
    {item.country}
Actual localized country:
    <f:translate key="{item.country}" />

<f:comment>Will show something like "LLL:EXT:core/Resources/Private/Language/Iso/countries.xlf:AT.official_name"</f:comment>
Country LLL label:
    {item.country.localizedOfficialNameLabel}
Actual localized official country name:
    <f:translate key="{item.country.localizedOfficialNameLabel}" />

<f:comment>Will show something like "Germany" (always English)</f:comment>
    {item.country.name}
Copied!

You can use the Extbase \TYPO3\CMS\Extbase\Utility\LocalizationUtility in PHP scope (Controllers, Domain Model) to create a custom getter in your Domain Model to create a shorthand method:

EXT:my_extension/Domain/Model/Item.php
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
use TYPO3\CMS\Core\Country\Country;

class SomeDomainModel extends AbstractEntity
{
    protected ?Country $country = null;

    public function setCountry(?Country $country): void
    {
        $this->country = $country;
    }

    public function getCountry(): ?Country
    {
        return $this->country;
    }

    // Special getter to easily access `{item.localizedCountry}` in Fluid
    public function getLocalizedCountry(): string
    {
        return (string) LocalizationUtility::translate(
            (string) $this->getCountry()?->getLocalizedNameLabel()
        );
    }
}
Copied!

Extbase Repository access 

As mentioned above, since Country has no database-record relations. The single-country relation always uses the 2-letter ISO alpha2 key (respectively custom country keys, when added via the PSR-14 event BeforeCountriesEvaluatedEvent ). Thus, queries need to utilize them as string comparisons:

EXT:my_extension/Classes/Domain/Repository/ItemRepository.php
namespace MyExtension\Domain\Repository;

use TYPO3\CMS\Extbase\Persistence\QueryResultInterface;
use TYPO3\CMS\Extbase\Persistence\Repository;

class ItemRepository extends Repository
{
    public function findByGermanMarkets(): QueryResultInterface {
        $query = $this->createQuery();
        $query->matching(
            $query->in('country', ['DE', 'AT', 'CH'])
        );
        return $query->execute();
    }
}
Copied!

The default Extbase repository magic method $repository->findBy(['country' => 'DE']) will work, too.

TypoScript rendering usage via record-transformation 

Database records using country type fields can be rendered with the TypoScript-based record-transformation rendering (data processor).

You can specify how a field containing a country is rendered in the output (using the name, the flag icon, specific ISO keys) with regular Fluid logic then:

Step 1: TypoScript utilizing record-transformation, defining a Homepage.html Fluid template
page = PAGE
page {
  # Just an example basic template for your site. The important section starts with `dataProcessing`!
  100 = FLUIDTEMPLATE
  100 {
    templateName = Homepage
    templateRootPaths {
      0 = EXT:myextension/Resources/Private/Templates/
    }
    dataProcessing {
      10 = database-query
      10 {
        as = mainContent
        # This table holds for example a TCA type=country definition for a field "country"
        table = tx_myextension_domain_model_mycountries
        # An extra boolean field "show_on_home_page" would indicate whether these
        # records are fetched and displayed on the home page
        where = show_on_home_page=1
        # Depending on your table storage you may need to set a proper pidInList constraint.
        #pidInList = 4711
        dataProcessing {
          # Makes all records available as `{mainContent.[0..].myRecord}` in the
          # Fluid file EXT:myextension/Resources/Private/Templates/Homepage.html
          10 = record-transformation
          10 {
            as = myRecord
          }
        }
      }
    }
  }
}
Copied!
Step 2: Fluid template EXT:myextension/Resources/Private/Templates/Homepage.html
<f:if condition="{mainContent}">
  <f:for each="{mainContent}" as="element">
    <!-- given that your 'tx_myextension_domain_model_mycountries' has a TCA field called "storeCountry":
    Selected Country:
      <f:translate key="{element.myRecord.storeCountry.localizedOfficialNameLabel}" />
  </f:for>

  <!-- note that you can access any transformed record type object via 'element', also multiple country
       elements could be contained in 'element.myRecord'. -->
</f:if>
Copied!

Impact 

It is now possible to use a dedicated TCA type for storing a relation to a country in a record.

Using the new TCA type, corresponding database columns are added automatically. Country-annotated properties of Extbase Domain Models can be evaluated in Extbase and via TypoScript.

Feature: #101059 - Allow install tool sessions without shared file system 

See forge#101059

Description 

It is now possible to store Install Tool sessions in Redis or configure the session storage path for file-based Install Tool sessions.

As a shipped session handler, Redis can now be configured via options in $GLOBALS['TYPO3_CONF_VARS'] for host, port, database, and authentication.

Example 

To configure an alternative session handler for Install Tool sessions, set the required options in your settings.php or additional.php file:

File-based session handler in config/system/settings.php
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Install\Service\Session\FileSessionHandler;

return [
    'BE' => [
        'installToolSessionHandler' => [
            'className' => FileSessionHandler::class,
            'options' => [
                'sessionPath' => Environment::getVarPath() . '/session',
            ],
        ],
    ],
];
Copied!
Redis session handler in config/system/settings.php
use TYPO3\CMS\Install\Service\Session\RedisSessionHandler;

return [
    'BE' => [
        'installToolSessionHandler' => [
            'className' => RedisSessionHandler::class,
            'options' => [
                'host' => '127.0.0.1',
                'port' => 6379,
                'database' => 0,
                'authentication' => [
                    'user' => 'redis',
                    'pass' => 'redis',
                ],
            ],
        ],
    ],
];
Copied!

Impact 

The default file-based session handling for the Install Tool remains unchanged. If no alternative session handler for the Install Tool is configured, the default behavior is used.

Custom session handlers can be created by implementing PHP's \SessionHandlerInterface.

Feature: #103258 - Language filter for list module 

See forge#103258

Description 

The list module now provides a language filter in the document header, similar to the existing language selector in the page module. This allows backend users to filter records by language, making it easier to focus on content in a specific language when working with multilingual websites.

The language filter appears as a dropdown button in the document header toolbar and provides the following options:

  • All available site languages that have page translations
  • "All languages" (if translations exist on the current page)

Key behaviors 

Language selection persistence 

The selected language is stored in the backend user's module data and persists across page navigation. When switching between pages, the previously selected language remains active if available on the new page.

Automatic fallback 

If navigating to a page where the selected language is not available (no translation exists), the list module automatically falls back to:

  • "All languages" mode (if the page has any translations)
  • The default language (if the page has no translations at all)

This fallback is temporary and does not overwrite the language preference. When navigating to a page with the selected language translation, it will automatically be restored.

Display behavior 

When a specific language is selected:

  • Records in the selected language are displayed
  • Default language records (language 0) are always included as fallback
  • Records with the "all languages" flag (-1) are included

Example: If French is selected, one will see French translations, default language content, and content marked for "all languages".

Localization restrictions 

The localization panel respects page translation availability. When a language is selected or when viewing in "all languages" mode, the list module only offers localization options for languages where the page has an existing translation.

This ensures data integrity by preventing the creation of records in languages where the parent page does not exist.

Impact 

Backend users can now efficiently filter list module records by language, improving the workflow when managing multilingual content.

The language selection persists across page navigation, reducing the need to repeatedly select the same language. The intelligent fallback mechanism ensures the list module always displays relevant content, even when switching between pages with different translation availability.

This enhancement brings the list module's language handling in line with that of the page module, providing a consistent user experience across TYPO3's backend.

Feature: #103740 - Language selection for backend module "Status - Pagetree Overview" 

See forge#103740

Description 

The backend module Content > Status > Pagetree Overview has been enhanced with a language selection option.

This change makes it possible to switch the displayed page tree to the selected language and adjust all labels, as well as edit and view links, accordingly.

The language selection dropdown is located next to the other filter options (recursion depth, information type) and complements the Content > Status > Localization Overview module by providing a page- and record-focused view.

Impact 

The Content > Status backend module is now more useful for sites with multiple languages, offering a quick overview of information for the selected page and its subpages in the chosen language.

Feature: #104054 - Add cache flush tags command 

See forge#104054

Description 

A new command cache:flushtags has been introduced to allow flushing cache entries by tag.

Multiple tags can be flushed by passing a comma-separated list of tags. It is also possible to flush tags for a specific cache group by using the --groups or -g option. If no group is specified, all cache groups are considered.

Note that certain combinations of groups and tags do not make sense, specifically the di and system cache groups.

Examples 

Example command usage (Composer mode projects)
vendor/bin/typo3 cache:flushtags pageId_123
vendor/bin/typo3 cache:flushtags pages_100,pages_200
vendor/bin/typo3 cache:flushtags tx_news -g pages
Copied!

Impact 

It is now possible to flush cache entries for specific tag and group combinations directly from the command line.

Feature: #104058 - Introduce install:password:set command 

See forge#104058

Description 

The Install Tool password can now also be set via the TYPO3 command line interface instead of only via direct file access. This allows for better automation and easier hash generation without manual steps.

Usage 

Interactively create and write an Install Tool password hash:

vendor/bin/typo3 install:password:set
Copied!

Return the generated password hash without writing to the configuration:

vendor/bin/typo3 install:password:set --dry-run
Copied!

Run without interaction (generates a random password):

vendor/bin/typo3 install:password:set --no-interaction
Copied!

Options can be combined as needed:

vendor/bin/typo3 install:password:set --dry-run --no-interaction
Copied!

The last variation can, for example, be used in CI/CD automation to generate a suitable password, process the output (the generated password needs to be persisted separately), and store it in vaults or environment variables for later use.

Impact 

It is now possible to set the hashed Install Tool password using the TYPO3 command line interface.

Feature: #105549 - Support qualified and unqualified ISO8601 dates in DataHandler 

See forge#105549

Description 

The DataHandler API has been extended to support both qualified and unqualified ISO 8601 date formats, correctly handling supplied timezone offsets when provided.

Qualified ISO 8601
Includes an explicit timezone offset (for example, 1999-12-11T10:09:00+01:00 or 1999-12-11T10:09:00Z)
Unqualified ISO 8601
Omits timezone offsets, representing LOCALTIME (for example, 1999-12-11T10:09:00)

The DataHandler now accepts five different formats:

Format Examples
Unqualified ISO 8601 (LOCALTIME) 'Y-m-d\\TH:i:s' 1999-11-11T11:11:11
Qualified ISO 8601 'Y-m-d\\TH:i:sP'

1999-11-11T10:11:11Z

1999-11-11T11:11:11+01:00

DateTime objects \DateTimeInterface

new \DateTime('yesterday')

new \DateTimeImmutable()

SQL-flavored dates (internal use) 'Y-m-d H:i:s' 1999-11-11 11:11:11
Unix timestamps (internal use) 'U' 942315071

The ISO 8601 variants and \DateTimeInterface objects are intended for use in the public API. The SQL-flavored variant and Unix timestamps are primarily intended for internal operations such as copy or import processes involving native DATETIME and INT timestamp database fields.

Passing datetime data via the DataHandler PHP API
$myDate = new \DateTime('yesterday');
$this->dataHandler->start([
    'tx_myextension_mytable' => [
        'NEW-1' => [
            'pid' => 2,
            // Format as LOCALTIME
            'mydatefield_1' => $myDate->format('Y-m-d\\TH:i:s'),
            // Format with timezone information
            // (offsets will be normalized to the persistence timezone format,
            // UTC for integer fields, LOCALTIME for native DATETIME fields)
            'mydatefield_2' => $myDate->format('c'),
            // Pass \DateTimeInterface objects directly
            'mydatefield_3' => $myDate,
        ],
    ],
]);
Copied!

Impact 

TYPO3 now provides accurate and consistent handling of ISO 8601 dates, eliminating previous issues related to timezone interpretation and LOCALTIME representation.

Feature: #105624 - PSR-14 event after a backend user password has been reset 

See forge#105624

Description 

A new PSR-14 event \TYPO3\CMS\Backend\Authentication\Event\PasswordHasBeenResetEvent has been introduced. It is dispatched right after a backend user has reset their password and it has been hashed and persisted to the database.

The event contains the corresponding backend user UID.

Example 

The corresponding event listener class:

<?php

namespace Vendor\MyPackage\Backend\EventListener;

use TYPO3\CMS\Backend\Authentication\Event\PasswordHasBeenResetEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

final class PasswordHasBeenResetEventListener
{
    #[AsEventListener('my-package/backend/password-has-been-reset')]
    public function __invoke(PasswordHasBeenResetEvent $event): void
    {
        $userId = $event->userId;
        // Add custom logic for the backend user UID
    }
}
Copied!

Impact 

It is now possible to add custom business logic after a backend user has reset their password using the new PSR-14 event PasswordHasBeenResetEvent .

Feature: #105783 - Notify backend users on failed MFA verification attempts 

See forge#105783

Description 

TYPO3 now notifies backend users by email when a failed MFA (multi-factor authentication) verification attempt occurs. The notification is sent only if an MFA provider is configured and the user has a valid email address in their profile.

Impact 

TYPO3 backend users now benefit from improved security awareness through immediate email notifications about failed MFA verification attempts. This feature is particularly useful in cases where backend accounts with active MFA configuration are targeted by unauthorized access attempts.

Feature: #105833 - Extended page tree filter functionality 

See forge#105833

Description 

The page tree is one of the central components in the TYPO3 backend, particularly for editors. However, in large installations, the page tree can quickly become overwhelming and difficult to navigate. To maintain a clear overview, the page tree can be filtered using basic terms, such as the page title or ID.

To enhance the filtering capabilities, the new PSR-14 event \TYPO3\CMS\Backend\Tree\Repository\BeforePageTreeIsFilteredEvent has been introduced. This event allows developers to extend the filter functionality and process the given search phrase in more advanced ways.

Using this event, it is for example possible to evaluate a given URL or to add additional field matchings, such as filtering pages by their doktype or their configured backend layout.

The event provides the following public properties:

$searchParts:
The search parts to be used for filtering
$searchUids:
The uids to be used for filtering by a special search part, which is added by Core always after listener evaluation
$searchPhrase
The complete search phrase, as entered by the user
$queryBuilder:
The current QueryBuilder instance to provide context and to be used to create search parts

Example 

The following event listener class demonstrates how to add additional conditions to the page tree filter using the PHP attribute #[AsEventListener] for registration.

EXT:my_extension/Classes/EventListener/MyEventListener.php
use TYPO3\CMS\Backend\Tree\Repository\BeforePageTreeIsFilteredEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Database\Connection;

final class MyEventListener
{
    #[AsEventListener]
    public function removeFetchedPageContent(BeforePageTreeIsFilteredEvent $event): void
    {
        // Add an additional UID to the filter
        $event->searchUids[] = 123;

        // Add evaluation of doktypes to the filter
        if (preg_match('/doktype:([0-9]+)/i', $event->searchPhrase, $match)) {
            $doktype = $match[1];
            $event->searchParts = $event->searchParts->with(
                $event->queryBuilder->expr()->eq(
                    'doktype',
                    $event->queryBuilder->createNamedParameter($doktype, Connection::PARAM_INT)
                )
            );
        }
    }
}
Copied!

Impact 

With the new PSR-14 event BeforePageTreeIsFilteredEvent , custom functionality and advanced evaluations can now be added to enhance the page tree filter.

Feature: #106072 - Introduce regex-based replacements for slugs 

See forge#106072

Description 

A second replacement configuration array has been added to support regular expression (regex)-based definitions. This allows defining case-insensitive or wildcard replacements for slug generation.

Impact 

Slug fields now support a new regexReplacements configuration array inside generatorOptions.

Example TCA configuration
$GLOBALS['TCA'][$table]['columns']['slug']['config']['generatorOptions']['regexReplacements'] => [
    // Case-insensitive replacement of Foo, foo, FOO, etc. with "bar"
    '/foo/i' => 'bar',
    // Remove string wrapped in parentheses
    '/\(.*\)/' => '',
    // Same, using a custom regex delimiter
    '@\(.*\)@' => '',
];
Copied!

Feature: #106074 - Show editor information in workspace "Publish" module 

See forge#106074

Description 

The "Last changed" column in the Content > Publish module has been improved. It now displays the username and avatar of the editor who most recently modified the corresponding record.

The column shows the editor's username in a badge, or Unknown if no editor information is available. This helps reviewers quickly identify who last worked on each workspace record.

Impact 

Workspace information now reveals the last editor of a record within the workspace context.

Feature: #106092 - Associative array keys for TCA valuePicker items 

See forge#106092

Description 

It is now possible to define associative array keys for the items configuration of the TCA type valuePicker. The new keys are called label and value.

This follows the change made previously to the items configuration of the TCA types select, radio, and check. See forge#99739.

Impact 

The TCA items configuration can now be defined in a more consistent and readable way using associative array keys. This eliminates ambiguity about whether the label or value comes first.

Optional keys such as icon, group, or description can be used as needed.

Feature: #106232 - Provide record title tag provider 

See forge#106232

Description 

The class \TYPO3\CMS\Core\PageTitle\RecordTitleProvider introduces a new page title provider with the identifier recordTitle. It is executed before the \SeoTitlePageTitleProvider, which uses the TypoScript identifier seo.

This provider can be used by third-party extensions to set the page title programmatically.

EXT:my_extension/Classes/Controller/ItemController.php
use MyVendor\MyExtension\Domain\Model\Item;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
use TYPO3\CMS\Core\PageTitle\RecordTitleProvider;

final class ItemController extends ActionController
{
    public function __construct(
        private readonly RecordTitleProvider $recordTitleProvider
    ) {
    }

    public function showAction(Item $item): ResponseInterface
    {
        $this->recordTitleProvider->setTitle($item->getTitle());
        $this->view->assign('item', $item);
        return $this->htmlResponse();
    }
}
Copied!

Impact 

A dedicated provider is now available for extensions to set page titles without needing to implement their own custom provider.

Feature: #106363 - PSR-14 event for modifying URLs in redirects:checkintegrity 

See forge#106363

Description 

A new PSR-14 event \TYPO3\CMS\Redirects\Event\AfterPageUrlsForSiteForRedirectIntegrityHaveBeenCollectedEvent has been introduced. It allows extensions to register event listeners to modify the list of URLs processed by the CLI command redirects:checkintegrity.

Example 

The following example shows an event listener that uses the PHP attribute #[AsEventListener] to register itself and adds the URLs found in a site's XML sitemap to the list of URLs checked by the redirects integrity command.

EXT:my_extension/Classes/EventListener/MyEventListener.php
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Http\RequestFactory;
use TYPO3\CMS\Redirects\Event\AfterPageUrlsForSiteForRedirectIntegrityHaveBeenCollectedEvent;

final readonly class MyEventListener
{
    public function __construct(
        private RequestFactory $requestFactory,
    ) {}

    #[AsEventListener]
    public function __invoke(AfterPageUrlsForSiteForRedirectIntegrityHaveBeenCollectedEvent $event): void
    {
        $pageUrls = $event->getPageUrls();

        $additionalOptions = [
            'headers' => ['Cache-Control' => 'no-cache'],
            'allow_redirects' => false,
        ];

        $site = $event->getSite();

        foreach ($site->getLanguages() as $siteLanguage) {
            $sitemapIndexUrl = rtrim((string)$siteLanguage->getBase(), '/') . '/sitemap.xml';
            $response = $this->requestFactory->request(
                $sitemapIndexUrl,
                'GET',
                $additionalOptions,
            );
            $sitemapIndex = simplexml_load_string($response->getBody()->getContents());

            foreach ($sitemapIndex as $sitemap) {
                $sitemapUrl = (string)$sitemap->loc;
                $response = $this->requestFactory->request(
                    $sitemapUrl,
                    'GET',
                    $additionalOptions,
                );
                $sitemap = simplexml_load_string($response->getBody()->getContents());
                foreach ($sitemap as $url) {
                    $pageUrls[] = (string)$url->loc;
                }
            }
        }

        $event->setPageUrls($pageUrls);
    }
}
Copied!

Feature: #106405 - TypolinkBuilderInterface 

See forge#106405

Description 

A new interface \TYPO3\CMS\Frontend\Typolink\TypolinkBuilderInterface has been introduced to provide a more flexible way to generate links in TYPO3.

The interface defines a buildLink() method that replaces the previous build() method approach used when extending from AbstractTypolinkBuilder .

All Core TypolinkBuilder implementations now implement this interface and use dependency injection for improved service composition and testability.

The interface method signature is as follows:

use TYPO3\CMS\Frontend\Typolink;

public function buildLink(
    array $linkDetails,
    array $configuration,
    ServerRequestInterface $request,
    string $linkText = ''
): LinkResultInterface;
Copied!

Impact 

  • All implementations of TypolinkBuilderInterface are automatically configured as public services in the dependency injection container, removing the need for manual service configuration.
  • TypolinkBuilder classes can now use proper dependency injection through their constructors, improving testability and aligning with TYPO3's architectural best practices.
  • The \ServerRequestInterface is now passed directly, providing access to the request context without relying on global state.
  • The new interface introduces a cleaner separation of concerns and more explicit parameter passing.

Example usage 

Creating a custom TypolinkBuilder using the new interface:

EXT:my_extension/Classes/Typolink/MyCustomLinkBuilder.php
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Frontend\Typolink\LinkResult;
use TYPO3\CMS\Frontend\Typolink\LinkResultInterface;
use TYPO3\CMS\Frontend\Typolink\TypolinkBuilderInterface;

final readonly class MyCustomLinkBuilder implements TypolinkBuilderInterface
{
    public function __construct(
        private MyCustomService $customService,
        private AnotherService $anotherService,
    ) {}

    public function buildLink(
        array $linkDetails,
        array $configuration,
        ServerRequestInterface $request,
        string $linkText = ''
    ): LinkResultInterface {
        // Access ContentObjectRenderer from the request
        $contentObjectRenderer = $request->getAttribute('currentContentObject');

        // Use injected services
        $processedData = $this->customService->process($linkDetails);

        // Build and return link result
        return new LinkResult($processedData['url'], $linkText);
    }
}
Copied!

Registering the TypolinkBuilder class is still necessary via $GLOBALS['TYPO3_CONF_VARS'] .

Feature: #106415 - Add stdWrap to config.htmlTag.attributes.[attr] 

See forge#106415

Description 

Each attribute within the TypoScript option config.htmlTag.attributes.[attr] now supports all stdWrap properties.

This option controls the attributes of the single <html> element of a rendered page.

Impact 

It is now possible to use userFunc, override, or getData within TypoScript:

Using override in TypoScript
config.htmlTag.attributes{
    my-attribute = 123
    my-attribute.override = 456
}
Copied!
Using userFunc in TypoScript
config.htmlTag.attributes {
    my-attribute = 123
    my-attribute.userFunc = MyVendor\\MyExtension\\HtmlTagEnhancer->overrideMyAttribute
}
Copied!

Feature: #106477 - Allow YAML imports and placeholder processing when creating new forms 

See forge#106477

Description 

The FormManagerController now uses the YamlFileLoader when creating new forms from templates. This change enables the processing of placeholders within template files, such as environment variables in the format %env(ENV_NAME)%, as well as the import of other YAML files.

This enhancement allows for more flexible form templates that can adapt to different environments through environment variable substitution and YAML imports.

Examples 

EXT:my_extension/Resources/Private/Backend/Templates/FormEditor/Yaml/NewForms/ContactForm.yaml
imports:
  - { resource: 'User.yaml' }
identifier: contactForm
label: 'Contact Form %env(ENV_NAME)%'
type: Form
renderables:
  -
    type: Page
    identifier: message
    label: Message
    renderables:
      -
        defaultValue: ''
        type: Text
        identifier: text-1
        label: Subject
Copied!
EXT:my_extension/Resources/Private/Backend/Templates/FormEditor/Yaml/NewForms/User.yaml
renderables:
  -
    type: Page
    identifier: user
    label: User data
    renderables:
      -
        defaultValue: ''
        type: Text
        identifier: username
        label: Username
Copied!

Impact 

Form templates can now contain environment variable placeholders using the %env(ENV_NAME)% syntax and import other YAML files. These placeholders and imports are automatically resolved when new forms are created from the template.

Feature: #106510 - Add PSR-14 events to Extbase Backend::getObjectCountByQuery method 

See forge#106510

Description 

The class \TYPO3\CMS\Extbase\Persistence\Generic\Backend is the central entity for retrieving data from the database within the Extbase persistence framework.

Since 2013, the getObjectDataByQuery() method has supported events (previously signals) to allow modification of data retrieval.

In many use cases, especially when used together with \QueryResult, another key method is involved: getObjectCountByQuery(). This method is frequently used in combination with Fluid templates.

Until now, extensions or other code using the existing events for data retrieval could not ensure consistent modification of queries between data retrieval and counting operations, resulting in mismatched query results.

The getObjectCountByQuery() method has now been enhanced with new PSR-14 events, enabling extensions to modify all aspects of query processing within Extbase's generic Backend to achieve consistent results.

The new events are:

  • ModifyQueryBeforeFetchingObjectCountEvent
    Allows modification of the query before it is passed to the storage backend.
  • ModifyResultAfterFetchingObjectCountEvent
    Allows adjustment of the result after the query has been executed.

Typically, an extension should implement these events pairwise:

  • ModifyQueryBeforeFetchingObjectCountEvent together with ModifyQueryBeforeFetchingObjectDataEvent
  • ModifyResultAfterFetchingObjectCountEvent together with ModifyResultAfterFetchingObjectDataEvent

Feature: #106637 - Implement accessible combobox pattern 

See forge#106637

Description 

A new ARIA 1.2–compliant combobox web component has been introduced, replacing the legacy value picker select pattern. The implementation follows the W3C accessibility guidelines and provides complete keyboard navigation support.

FormEngine elements, including EmailElement , InputTextElement , and NumberElement , have been updated to use the new combobox component instead of the previous value picker implementation. The link browser components have also been adapted to use the combobox pattern.

Impact 

The new combobox component offers full keyboard navigation using the arrow keys, Enter, Tab, and Escape.

It includes visual selection indicators with checkmarks and a clear button for resetting the input value, improving accessibility and overall usability in the TYPO3 backend.

Feature: #106686 - Enhance file browsers with column selector 

See forge#106686

Description 

The file browsers used in the file selector (for example, in file fields) and the file link browser (for example, for RTE links) have been enhanced to support the column selector component.

This allows backend users to customize which columns are displayed (for example, "alternative", "timestamp", and so on), improving usability and aligning the interface with the standard File List module.

This enhancement is particularly useful in large file storages, making it easier to locate recently updated files or identify large files more quickly.

Impact 

The file selector and link browser interfaces now include a column selector. This improves consistency across TYPO3 backend modules and provides users with greater control over the visibility of file metadata.

No additional configuration is required for this feature.

Whether the column selector is shown is still determined by the user TSconfig option options.file_list.displayColumnSelector.

Feature: #106739 - Scheduler tasks as native TCA table 

See forge#106739

Description 

For historical reasons, the TYPO3 system extension typo3/cms-scheduler used a special mechanism for persisting data to the database. This was achieved by serializing the entire task object into a single field of the database table tx_scheduler_task.

In TYPO3 v14, this behavior has been reworked. The logic for custom fields has been split into a dedicated database field parameters of type json and a new database field tasktype that stores the scheduler task name or CLI command.

An upgrade wizard automatically migrates all existing scheduler tasks to the new database structure.

Impact 

With this change, TCA is now defined for the tx_scheduler_task table in TYPO3. This provides several advantages over the previous implementation:

  • The editing interface is now handled via FormEngine, making it more flexible and extensible.
  • Changes are stored to the database via DataHandler, which allows customization of persistence operations. The history and audit functionality is now also available for scheduler tasks.
  • Database entries of tx_scheduler_task can now be exported and imported using the standard import/export functionality.
  • Deleted tasks can be restored via the recycler module.

Additional functionality such as support for automated database restrictions and the TCA schema is available as well.

Feature: #106752 - Add password hashing option to SaveToDatabase finisher 

See forge#106752

Description 

A new option hashed has been added to the \SaveToDatabaseFinisher of the system extension typo3/cms-form .

When saving form data to a database table, setting hashed: true for a field causes the value to be hashed using the default frontend password hashing mechanism before it is written to the database.

This improves security by preventing passwords from being stored in plain text.

Example usage in a form definition:

EXT:my_extension/Configuration/Form/ExampleForm.form.yaml
- identifier: SaveToDatabase
  options:
    table: 'fe_users'
    elements:
      password:
        mapOnDatabaseColumn: 'password'
        hashed: true
Copied!

Impact 

Integrators can now ensure secure password storage when saving form data with the \SaveToDatabaseFinisher, without implementing custom logic.

Feature: #106839 - Introduce shell auto-completion for the typo3 command 

See forge#106839

Description 

A new CLI command vendor/bin/typo3 completion has been added to the typo3 CLI dispatcher script.

This command enables shell auto-completion for supported shells, allowing developers to use the Tab key to trigger command and option suggestions.

The completion command is provided by the symfony/console package and is not a custom implementation.

This ensures compatibility with ongoing improvements in the Symfony ecosystem and benefits from a broad user base and community support.

Supported shells 

The command reports unsupported shells and lists available ones:

# bin/typo3 completion shell
Detected shell "shell", which is not supported by Symfony shell completion
(supported shells: "bash", "fish", "zsh").
Copied!

Installation modes 

The command supports two installation modes — static and dynamic. Run vendor/bin/typo3 completion --help to see detailed usage instructions and the supported shells (bash, fish, zsh).

Static installation 

Dump the completion script to a file and source it manually or install it globally, for example:

vendor/bin/typo3 completion bash | sudo tee /etc/bash_completion.d/typo3
Copied!

Or dump the script to a local file and source it:

bin/typo3 completion bash > completion.sh
source completion.sh
Copied!

To make it permanent, add the following line to your " /.bashrc" file:

 /.bashrc
source /path/to/completion.sh
Copied!

Dynamic installation 

Add an eval line to your shell configuration file (for example ~/.bashrc):

eval "$(/var/www/html/vendor/bin/typo3 completion bash)"
Copied!

Impact 

The typo3 CLI dispatcher now supports shell auto-completion, improving the user experience without affecting existing command usage. This also lays the foundation for further enhancements such as improved auto-completion for command options and arguments.

The following existing commands already provide completion for their arguments:

  • redirects:cleanup
  • redirects:checkintegrity
  • styleguide:generate

Example 

The following example shows how to add auto-completion support to a custom Symfony console command:

EXT:my_extension/Classes/Command/GreetCommand.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Input\InputArgument;

#[AsCommand(
    name: 'myextension:greet',
)]
class GreetCommand extends Command
{
    protected function configure(): void
    {
        $this
            ->addArgument(
                'names',
                InputArgument::IS_ARRAY,
                'Who do you want to greet (separate multiple names with a space)?',
                null,
                function (CompletionInput $input): array {
                    // Value already typed by the user, e.g. "myextension:greet Fa"
                    // before pressing Tab — this will contain "Fa"
                    $currentValue = $input->getCompletionValue();

                    // Example of available usernames
                    $availableUsernames = ['jane', 'jon'];

                    return $availableUsernames;
                }
            );
    }
}
Copied!

For more details, see Symfony Console Adding Argument Option Value Completion and also for testing purpose see Testing the Completion script.

Feature: #106934 - Add recently used records to record wizards 

See forge#106934

Description 

A new dynamic category "Recently used" has been added to the record creation wizards for the following components:

  • Content elements
  • Form elements
  • Dashboard widgets

This category lists the record or form types that a user has recently selected in the respective wizard. It remains hidden until at least one record has been created, keeping the interface uncluttered for new users.

This enhancement allows backend users to quickly access and reuse frequently used record types, improving workflow efficiency and usability.

Impact 

All record wizards now feature a "Recently used" section. This feature is enabled by default and requires no additional configuration. Users can manage the display of this category in their personal backend settings.

This improvement streamlines access to frequently used records, enhancing the overall editing experience for editors and integrators.

Example 

When creating a new content element, users will now see a "Recently used" section listing previously used content types, making them accessible with a single click.

Feature: #106945 - Allow usage of Symfony validators in Extbase 

See forge#106945

Description 

Extbase models and controllers now support the use of Symfony Validators. Validators are based on Symfony Constraints, which can be added as attributes to domain model properties and controller methods.

Once a constraint attribute is detected while reflecting properties or methods, it is decorated by the new ConstraintDecoratingValidator class, which is compatible with Extbase's \ValidatorInterface.

Decorated constraints may include localizable messages. If a message contains valid LLL: syntax, the label will be translated automatically. The decorating validator also handles message parameters by converting named parameters such as {{ value }} into sprintf-compatible placeholders like %1$s.

Example 

EXT:my_extension/Classes/Domain/Model/MyModel.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Model;

use Symfony\Component\Validator\Constraints as Assert;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class MyModel extends AbstractEntity
{
    #[Assert\WordCount(max: 200, maxMessage: 'Biography must not exceed 200 words.')]
    protected string $biography = '';

    #[Assert\CssColor(message: 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:validator.avatarColor.error')]
    protected string $avatarColor = '';

    #[Assert\Iban]
    protected string $iban = '';

    public function getBiography(): string
    {
        return $this->biography;
    }

    public function setBiography(string $biography): void
    {
        $this->biography = $biography;
    }

    public function getAvatarColor(): string
    {
        return $this->avatarColor;
    }

    public function setAvatarColor(string $avatarColor): void
    {
        $this->avatarColor = $avatarColor;
    }

    public function getIban(): string
    {
        return $this->iban;
    }

    public function setIban(string $iban): void
    {
        $this->iban = $iban;
    }
}
Copied!

Impact 

A wide range of Symfony validators can now be used directly in Extbase. This provides a more flexible and standardized validation workflow without the need to implement custom validators, as Symfony already ships with a large number of predefined constraints.

Feature: #106972 - Configure searchable fields 

See forge#106972

Description 

TYPO3 now automatically includes all fields of suitable types in backend search operations, e.g., in the List module.

This eliminates the need for the previously used TCA ctrl option searchFields, which has been removed.

Instead, a new per-field configuration option searchable has been introduced. It allows integrators to fine-tune whether a specific field should be included in backend search queries.

By default, all fields of supported types are considered searchable. To exclude a field from being searchable, set the following in the field’s TCA configuration:

'my_field' => [
    'config' => [
        'type' => 'input',
        'searchable' => false,
    ],
],
Copied!

Note that until searchFields is manually removed from your TCA, the automatic TCA migration sets all suitable fields, which are not included in the searchFields configuration, to searchable => false to keep current behavior.

Supported Field Types 

The following TCA field types support the searchable option and are automatically considered in searches unless explicitly excluded:

  • color
  • datetime (when not using a custom dbType)
  • email
  • flex
  • input
  • json
  • link
  • slug
  • text
  • uuid

Unsupported field types such as file, inline, password or group are excluded from search and do not support the searchable option.

Impact 

  • Backend search becomes more consistent and automatic.
  • No need to manually maintain a searchFields list in TCA.
  • Integrators have more granular control over search behavior on a field level.
  • Custom fields can easily be excluded from search using the searchable option.

Migration 

If your extension previously relied on the searchFields TCA option, remove it from the ctrl section and instead define 'searchable' => false on fields that should be excluded from search results.

No action is needed if the default behavior (search all suitable fields) is acceptable.

Example 

return [
    'columns' => [
        'title' => [
            'config' => [
                'type' => 'input',
                'searchable' => true, // optional, true by default
            ],
        ],
        'notes' => [
            'config' => [
                'type' => 'text',
                'searchable' => false, // explicitly excluded
            ],
        ],
    ],
];
Copied!

Feature: #106992 - Remember last opened category in record wizards 

See forge#106992

Description 

Following forge#106934, which introduced the dynamic Recently used category in record wizards, the component has been extended to store the last selected category. When opening a record wizard, for example, the wizard to create new content elements, it will now automatically preselect the category that was last used.

This enhancement improves usability and consistency, especially in installations with many categories, including those added by third-party extensions.

Impact 

Record wizards now automatically preselect the last used category when opened again.

No migration or configuration is required. The feature is enabled by default.

Feature: #107036 - Configurable dashboard widgets 

See forge#107036

Description 

Dashboard widgets can now be configured on a per-instance level using the Settings API. This allows widget authors to define settings that editors can modify directly from the dashboard interface, making widgets more flexible and adaptable to different use cases.

Typical configuration options include URLs for RSS feeds, limits on displayed items, or categories for filtering content.

Each widget instance maintains its own configuration, enabling multiple instances of the same widget type with different settings on the same or different dashboards.

Impact 

For editors

  • Dashboard widgets now display a settings (cog) icon when configuration is supported.
  • Clicking the icon opens a modal dialog with configurable options.
  • Settings are applied immediately after saving, and the widget refreshes automatically.
  • Each widget instance is configured independently per user.

For integrators

  • Dashboard widgets can now expose user-configurable options.
  • Settings are validated using the existing Settings API type system.
  • Widget instances maintain independent configurations, allowing flexible layouts.
  • No extra configuration is required—configurable widgets automatically show the settings icon.

For widget authors

  • Widgets can migrate from WidgetInterface to WidgetRendererInterface to define settings and use configured values during rendering.
  • Settings are validated and processed automatically via the Settings API.
  • The widget context provides a Settings object containing the current configuration.
  • All existing Settings API types are supported (string, int, bool, url, etc.).

Currently configurable widgets 

The following core widgets now support configuration:

RSS widget
  • Label – custom title for the widget instance
  • Feed URL – RSS feed URL to display (supports URL validation)
  • Limit – number of RSS items to show (default: 5)
  • Lifetime – cache duration in seconds for the RSS feed
Pages with internal note widget
  • Category – filter notes by category (All, Default, Instructions, Template, Notes, Todo)
  • Includes an upgrade wizard to migrate existing widgets of this type to the new format.

Example 

Widget authors can implement configurable widgets by migrating from the current interface WidgetInterface to the new renderer interface WidgetRendererInterface , which allows defining settings in the widget renderer:

EXT:my_extension/Classes/Widgets/ConfigurableWidget.php
<?php

use TYPO3\CMS\Core\Settings\SettingDefinition;
use TYPO3\CMS\Dashboard\Widgets\WidgetContext;
use TYPO3\CMS\Dashboard\Widgets\WidgetRendererInterface;
use TYPO3\CMS\Dashboard\Widgets\WidgetResult;

class ConfigurableWidget implements WidgetRendererInterface
{
    public function getSettingsDefinitions(): array
    {
        return [
            new SettingDefinition(
                key: 'title',
                type: 'string',
                default: 'Default Title',
                label: 'my_extension.my_widget:settings.label',
                description: 'my_extension.my_widget:settings.description.label',
            ),
            new SettingDefinition(
                key: 'limit',
                type: 'int',
                default: 10,
                label: 'my_extension.my_widget:settings.limit',
                description: 'my_extension.my_widget:settings.description.limit',
            ),
        ];
    }

    public function renderWidget(WidgetContext $context): WidgetResult
    {
        $settings = $context->settings;
        $title = $settings->get('title');
        $limit = $settings->get('limit');

        // Use settings to customize widget output
        return new WidgetResult(
            label: $title,
            content: '<!-- widget content -->',
            refreshable: true
        );
    }
}
Copied!

Editors can configure these widgets directly in the dashboard interface:

  1. Navigate to the dashboard containing the widget.
  2. Click the settings (cog) icon on the widget.
  3. Modify the available settings in the modal dialog.
  4. Click Save to apply the changes.

The widget automatically refreshes with the updated configuration.

Feature: #107047 - FlexForm enhancements: Direct plugin registration and raw TCA support 

See forge#107047

FlexForm direct plugin registration 

The methods \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPlugin() and \TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerPlugin() have been extended to accept a FlexForm definition directly via an additional $flexForm argument.

This new argument allows extensions to provide the FlexForm data structure when registering a plugin. The FlexForm can either be a reference to a FlexForm XML file (for example, FILE:EXT:my_extension/Configuration/FlexForm.xml) or the XML content itself.

This simplifies configuration and avoids the need to define the FlexForm separately in TCA.

Examples 

Direct FlexForm plugin registration

EXT:my_extension/Configuration/TCA/Overrides/tt_content.php
ExtensionUtility::registerPlugin(
    'MyExtension',
    'MyPlugin',
    'My Plugin Title',
    'my-extension-icon',
    'plugins',
    'Plugin description',
    'FILE:EXT:my_extension/Configuration/FlexForm.xml'
);
Copied!

Alternatively, using addPlugin() when not using Extbase:

EXT:my_extension/Configuration/TCA/Overrides/tt_content.php
ExtensionManagementUtility::addPlugin(
    [
        'My Plugin Title',
        'my_plugin',
        'my-extension-icon'
    ],
    'FILE:EXT:my_extension/Configuration/FlexForm.xml'
);
Copied!

Internally, this adds the FlexForm definition to the ds option of the plugin via the columnsOverrides configuration and also adds the pi_flexform field to the showitem list. For more information, see Breaking: #107047 - Remove pointer field functionality of TCA flex, which describes the migration of the ds option from multi-entry to single-entry.

FlexFormTools schema parameter requirement 

The service FlexFormTools has been refactored to remove its dependency on $GLOBALS['TCA'] , which caused architectural issues.

The following methods now support an explicit $schema parameter that accepts either a TcaSchema object or a raw TCA configuration array:

  • getDataStructureIdentifier()
  • parseDataStructureByIdentifier()
  • cleanFlexFormXML()

Previously, these methods had no schema parameter and relied on $GLOBALS['TCA'] internally, which was problematic during schema building.

Calling code must now explicitly provide schema data, either as:

  • A resolved TcaSchema object (for normal usage)
  • A raw TCA configuration array (for schema building contexts)

This architectural improvement eliminates circular dependencies and allows FlexFormTools to be used during schema building processes where TCA Schema objects are not yet available, resolving issues in components such as the RelationMapBuilder .

FlexFormTools with TCA Schema

Example using TCA Schema
use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);

// Using TCA Schema object
$tcaSchema = $tcaSchemaFactory->get('tt_content');
$identifier = $flexFormTools->getDataStructureIdentifier(
    $fieldTca,
    'tt_content',
    'pi_flexform',
    $row,
    $tcaSchema
);
Copied!

FlexFormTools with raw TCA array

Example using raw TCA configuration
use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);

// Using raw TCA configuration array
$rawTca = $fullTca['tt_content'];
$identifier = $flexFormTools->getDataStructureIdentifier(
    $fieldTca,
    'tt_content',
    'pi_flexform',
    $row,
    $rawTca
);
Copied!

Schema building context

Example usage in RelationMapBuilder
use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
use TYPO3\CMS\Core\Utility\GeneralUtility;

// In RelationMapBuilder - previously not possible
$flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);

foreach ($tca as $table => $tableConfig) {
    foreach ($tableConfig['columns'] ?? [] as $fieldName => $fieldConfig) {
        if ($fieldConfig['config']['type'] === 'flex') {
            // Can now use raw TCA during schema building
            $dataStructure = $flexFormTools->parseDataStructureByIdentifier(
                $identifier,
                $tableConfig // Raw TCA array
            );
        }
    }
}
Copied!

Impact 

Direct FlexForm plugin registration

This enhancement simplifies plugin configuration and FlexForm integration, as FlexForms can now be registered directly with the plugin. The call ExtensionManagementUtility::addPiFlexFormValue() is no longer required. This method has been deprecated; see Deprecation: #107047 - ExtensionManagementUtility::addPiFlexFormValue().

FlexFormTools schema support

The service now automatically detects the input type and uses the appropriate resolution strategy for both TCA Schema objects and raw TCA arrays. It no longer relies on $GLOBALS['TCA'] , allowing direct control over the service and making it usable during schema building where no TCA Schema is available.

Technical details

The service uses PHP union types ( array|TcaSchema) and automatically routes to the appropriate internal methods. Both input types produce identical normalized output, ensuring consistent data structures for all FlexFormTools consumers.

Feature: #107056 - Introduce headerData and footerData ViewHelpers 

See forge#107056

Description 

Two new Fluid ViewHelpers have been introduced to allow injecting arbitrary content into the HTML <head> or before the closing </body> tag of a rendered page:

  • <f:page.headerData> - injects content into the <head> section
  • <f:page.footerData> - injects content before the closing </body> tag

The ViewHelpers internally use the PageRenderer API and are useful when existing ViewHelpers such as <f:asset.css> or <f:asset.script> do not support all required attributes or use cases (for example, dns-prefetch, preconnect, tracking scripts, or inline JavaScript).

Example usage for <f:page.headerData>:

<f:page.headerData>
    <link
        rel="preload"
        href="/fonts/myfont.woff2"
        as="font"
        type="font/woff2"
        crossorigin="anonymous"
    >
    <link rel="dns-prefetch" href="//example-cdn.com">
    <link rel="preconnect" href="https://example-cdn.com">
</f:page.headerData>
Copied!

Example usage for <f:page.footerData>:

<f:page.footerData>
    <script>
        var _paq = window._paq = window._paq || [];
        _paq.push(['trackPageView']);
        _paq.push(['enableLinkTracking']);
        (function() {
            var u = "https://your-matomo-domain.example.com/";
            _paq.push(['setTrackerUrl', u + 'matomo.php']);
            _paq.push(['setSiteId', '1']);
            var d = document,
                g = d.createElement('script'),
                s = d.getElementsByTagName('script')[0];
            g.async = true;
            g.src = u + 'matomo.js';
            s.parentNode.insertBefore(g, s);
        })();
    </script>
</f:page.footerData>
Copied!

Both ViewHelpers output the given content as-is. Any user-supplied input passed to these ViewHelpers must be escaped manually to prevent Cross-Site Scripting (XSS) vulnerabilities.

Impact 

Extension authors and integrators can now use the new ViewHelpers to add raw HTML content, such as <link> or <script> tags, directly into the rendered page output.

Feature: #107104 - Introduce UrlFactory JavaScript module 

See forge#107104

Description 

TYPO3 already uses the native URL and URLSearchParams objects when working with URLs. The module @typo3/core/factory/url-factory.js has been introduced to provide a consistent and convenient way to create and manage these objects.

Impact 

URL and URLSearchParams objects can now be created by the factory's createUrl() and createSearchParams() methods.

The method createUrl() creates a full URL object and automatically sets its base. It accepts the following arguments:

  • url - string
  • parameters - mixed

If provided, the parameters value is passed to createSearchParams() (described below).

The method createSearchParams() creates a URLSearchParams object and accepts the following argument:

  • parameters - mixed

Parameters can be passed as plain string values or nested objects. Passing a plain array is not supported.

Examples 

The following examples assume the existence of TYPO3.settings.ajaxUrls.my_dedicated_endpoint, pointing to the route /custom_endpoint, while being on https://localhost for documentation purposes.

Create a URL object
import { UrlFactory } from '@typo3/core/factory/url-factory.js';

const url = UrlFactory.createUrl(
    TYPO3.settings.ajaxUrls.my_dedicated_endpoint
);
console.log(url.toString());
// https://localhost/custom_endpoint
Copied!
Create a URL object containing a query string from a nested object
import { UrlFactory } from '@typo3/core/factory/url-factory.js';

const url = UrlFactory.createUrl(
    TYPO3.settings.ajaxUrls.my_dedicated_endpoint,
    {
        foo: 'bar',
        baz: {
            hello: 'world',
        },
    }
);
console.log(url.toString());
// https://localhost/custom_endpoint?foo=bar&baz[hello]=world
Copied!
Create a URLSearchParams object from a string input
import { UrlFactory } from '@typo3/core/factory/url-factory.js';

const urlSearchParams = UrlFactory.createSearchParams(
    'foo=bar&baz=bencer'
);
console.log(urlSearchParams.toString());
// foo=bar&baz=bencer
Copied!
Create a URLSearchParams object from an object input
import { UrlFactory } from '@typo3/core/factory/url-factory.js';

const urlSearchParams = UrlFactory.createSearchParams({
    foo: 'bar',
    baz: 'bencer',
});
console.log(urlSearchParams.toString());
// foo=bar&baz=bencer
Copied!

Feature: #107105 - Introduce expression for accessing site locale 

See forge#107105

Description 

A new Symfony ExpressionLanguage expression locale() has been introduced.

This expression allows integrators and developers to directly access the current site locale, which is provided as a locale object. All public methods of this object are available for use.

For more information, refer to the API documentation: API

Example 

Using locale() in TypoScript conditions
[locale().getName() == "en-US"]
    page.20.value = bar
[END]
[locale().getCountryCode() == "US"]
    page.30.value = foo
[END]
[locale().isRightToLeftLanguageDirection()]
    page.40.value = bar
[END]
Copied!
Using locale() in a form variant definition
variants:
  - identifier: language-variant-1
    condition: 'locale().getName() == "en-US"'
    label: 'First name'
Copied!

Impact 

Developers can now compare or evaluate the site locale directly in expressions, without using siteLanguage("locale").

Feature: #107151 - Add AsNonSchedulableCommand attribute for CLI commands 

See forge#107151

Description 

With forge#101567 the usage of Symfony's #[AsCommand] attribute has been introduced, which allows configuring a Symfony CLI command with a corresponding name, description and further options.

It however lacked TYPO3's custom implementation of the schedulable option, which allows flagging a CLI command to be not allowed to be scheduled via the Administration > Scheduler backend module.

This previously required tagging such a command with the schedulable: false tag attribute in the Services.yaml or Services.php definition.

For this, the PHP attribute AsNonSchedulableCommand has been introduced. Any Symfony Command can use this empty attribute. The automatic Scheduler registry will ignore any command with this tag.

By default, a Symfony Command remains schedulable using the regular Symfony attribute. To prevent redundancy, the new attribute #[AsNonSchedulableCommand] should be used only on top of that.

Another advantage is that an IDE like PhpStorm is capable of showing all usages of that attribute inside a project.

Impact 

Developers can now fully embrace using the Symfony #[AsCommand] attribute and still be able to declare a non-schedulable execution within the scope of the same class, without any service registration.

This is achieved by using the #[AsNonSchedulableCommand] in addition to the #[AsCommand] attribute.

Example 

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use TYPO3\CMS\Core\Attribute\AsNonSchedulableCommand;

#[AsCommand('myextension:import', 'Import data from external source')]
#[AsNonSchedulableCommand]
final class ImportCommand extends Command
{
    // ...
}
Copied!

Feature: #107180 - PSR-14 events for Backend Users module 

See forge#107180

Description 

Several PSR-14 events have been added to allow customizations of the Backend Users module - in particular, which users, groups, and file mounts can be viewed in the module.

AfterBackendUserListConstraintsAssembledFromDemandEvent 

This event is dispatched when the backend user repository fetches a list of filtered backend users (itself called when displaying the list of users in the backend module). It makes it possible to modify the query constraints based on the currently active filtering.

The event provides the following public properties:

  • $demand: An instance of \TYPO3\CMS\Beuser\Domain\Model\Demand containing the current search criteria
  • $query: The \TYPO3\CMS\Extbase\Persistence\QueryInterface instance being used to assemble the query
  • $constraints: An array of query constraints. New constraints can be added to this array.

Example 

Here is an example event listener:

EXT:my_extension/Classes/EventListener/MyEventListener.php
namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Beuser\Event\AfterBackendUserListConstraintsAssembledFromDemandEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

final readonly class MyEventListener
{
    #[AsEventListener]
    public function __invoke(
        AfterBackendUserListConstraintsAssembledFromDemandEvent $event
    ): void {
        $event->constraints[] = $event->query->eq('admin', 1);
    }
}
Copied!

AfterBackendGroupListConstraintsAssembledFromDemandEvent 

This event is dispatched when the backend group repository fetches a list of filtered backend groups (itself called when displaying the list of groups in the backend module). It makes it possible to modify the query constraints based on the currently active filtering.

The event provides the following public properties:

  • $demand: An instance of \TYPO3\CMS\Beuser\Domain\Dto\BackendUserGroup containing the current search criteria
  • $query: The \TYPO3\CMS\Extbase\Persistence\QueryInterface instance being used to assemble the query
  • $constraints: An array of query constraints. New constraints can be added to this array.

Example 

Here is an example event listener:

EXT:my_extension/Classes/EventListener/MyEventListener.php
namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Beuser\Event\AfterBackendGroupListConstraintsAssembledFromDemandEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

final readonly class MyEventListener
{
    #[AsEventListener]
    public function __invoke(AfterBackendGroupListConstraintsAssembledFromDemandEvent $event): void
    {
        $event->constraints[] = $event->query->eq('workspace_perms', 1);
    }
}
Copied!

AfterBackendGroupFilterListIsAssembledEvent 

A list of user groups can be used to filter the users list in the backend module. This event is dispatched right after this list is assembled and makes it possible to modify it.

The event provides the following public properties:

  • $request: The current Extbase request object
  • $backendGroups: An array of backend groups.

Example 

Here is an example event listener:

EXT:my_extension/Classes/EventListener/MyEventListener.php
namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Beuser\Event\AfterBackendGroupFilterListIsAssembledEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

final readonly class MyEventListener
{
    #[AsEventListener]
    public function __invoke(AfterBackendGroupFilterListIsAssembledEvent $event): void
    {
        array_pop($event->backendGroups);
    }
}
Copied!

AfterFilemountsListIsAssembledEvent 

This event is dispatched when the file mounts list is fetched to display in the backend module. It makes it possible to modify this list.

The event provides the following public properties:

  • $request: The current Extbase request object
  • $filemounts: An array of file mounts.

Example 

Here is an example event listener:

EXT:my_extension/Classes/EventListener/MyEventListener.php
namespace MyVendor\MyExtension\EventListener;;

use TYPO3\CMS\Beuser\Event\AfterFilemountsListIsAssembledEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

final readonly class MyEventListener
{
    #[AsEventListener]
    public function __invoke(AfterFilemountsListIsAssembledEvent $event): void
    {
        array_pop($event->filemounts);
    }
}
Copied!

Impact 

These events can be used to implement custom user or permission management processes in the Backend Users module. Be aware that this area is security sensitive. Ensure that no unauthorized data exposure or privilege escalation occurs when modifying these queries or lists.

Feature: #107201 - Extended RSS Widget with Atom Support 

See forge#107201

Description 

Building upon the configurable dashboard widgets functionality introduced in Feature: #107036 - Configurable dashboard widgets, the existing RSS widget has been extended to support Atom feeds. This provides a unified solution for both RSS and Atom feed formats within the TYPO3 Dashboard system.

The configurable widget architecture makes it possible to create feed-based widgets that users can configure directly through the dashboard interface. This capability now extends seamlessly to Atom feeds, which are commonly used by platforms such as GitHub for release feeds and project updates.

The RSS widget now automatically detects and parses both RSS and Atom feed formats. Atom feeds are parsed according to the Atom Syndication Format (RFC 4287). This provides comprehensive support for modern feed formats used by many development platforms and services, all within a single widget implementation.

Impact 

  • The existing "RSS Widget" now supports both RSS and Atom feeds automatically.
  • Atom feeds (commonly used by GitHub, GitLab, and other platforms) can now be displayed using the same RSS widget.
  • Widget instances are configurable with custom labels, feed URLs, and display limits.
  • Each widget can be configured independently, allowing multiple feeds of different formats on the same dashboard.
  • Automatic caching ensures optimal performance with configurable cache lifetimes.

Currently configurable options 

The RSS widget supports the following configuration options for both RSS and Atom feeds:

Label
  • Custom title for the widget instance.
  • Optional field that defaults to the widget’s default title.
Feed URL
  • RSS or Atom feed URL to display.
  • Format detection is automatic based on feed content.
  • Can be preconfigured in the service definition to create read-only instances.
Limit
  • Number of feed entries to show (default: 5).
  • Configurable integer value to control widget content density.
Lifetime
  • Cache duration in seconds for the feed (default: 43200 = 12 hours).
  • Advanced setting typically configured by integrators.
  • Not configurable in the user interface.

Example for RSS widget with Atom feed 

Service configuration

EXT:my_extension/Configuration/Services.yaml
services:

  # Button provider for external link
  dashboard.buttons.github_releases:
    class: 'TYPO3\CMS\Dashboard\Widgets\Provider\ButtonProvider'
    arguments:
      $title: 'View all releases'
      $link: 'https://github.com/TYPO3/typo3/releases'
      $target: '_blank'

  # RSS widget with Atom feed URL
  dashboard.widget.github_releases:
    class: 'TYPO3\CMS\Dashboard\Widgets\RssWidget'
    arguments:
      $buttonProvider: '@dashboard.buttons.github_releases'
      $options:
        feedUrl: 'https://github.com/TYPO3/typo3/releases.atom'
        lifeTime: 43200
        limit: 10
    tags:
      - name: dashboard.widget
        identifier: 'github_releases'
        groupNames: 'general'
        title: 'my_extension.widgets:github_releases.title'
        description: 'my_extension.widgets:github_releases.description'
        iconIdentifier: 'content-widget-rss'
        height: 'large'
        width: 'medium'
Copied!

Usage in the dashboard

  1. Navigate to the dashboard where you want to add the widget.
  2. Click Add widget and select the RSS widget.
  3. Click the settings (cog) icon to customize the widget.
  4. Configure the feed URL (RSS or Atom), limit, and label as needed.
  5. Save the configuration to apply the changes.

The widget automatically detects the feed format and displays entries with titles, publication dates, content summaries, and author information when available.

Feed format support 

The RSS widget now supports both feed formats:

RSS feeds
Item titles
displayed as clickable links.
Publication dates
used for sorting entries (newest first).
Descriptions
displayed as entry content (HTML tags stripped).
Atom feeds
Entry titles
displayed as clickable links.
Publication dates
used for sorting entries (newest first).
Content/Summary
displayed as entry description (HTML tags stripped).
Author information
name, email, and URL when provided in the feed.

Feature: #107240 - Add warning when pasting password with whitespace 

See forge#107240

Description 

A new warning mechanism has been introduced in the backend login form to help users avoid authentication issues when pasting passwords that contain leading or trailing whitespace.

When a password is pasted into the backend login form, TYPO3 now detects if the pasted text contains leading or trailing whitespace characters (spaces, tabs, newlines, etc.) and displays a warning message to the user.

The warning includes an action button that allows users to automatically remove the surrounding whitespace from the pasted password, ensuring successful login attempts.

This feature helps prevent common login failures caused by accidentally copying whitespace along with passwords from password managers, text editors, or other sources.

The implementation includes:

  • Detection of leading and trailing whitespace in pasted passwords
  • Visual warning message displayed to the user
  • One-click action to remove the surrounding whitespace

Example 

The whitespace detection covers various whitespace characters including:

  • Regular spaces (U+0020)
  • Tabs (U+0009)
  • Line breaks (U+000A, U+000D)
  • Other Unicode whitespace characters

Impact 

Users now receive immediate feedback when pasting passwords that contain surrounding whitespace, reducing login failures and improving the overall user experience when authenticating to the TYPO3 backend.

Feature: #107256 - PSR-14 event to modify options in EmailFinisher 

See forge#107256

Description 

A new PSR-14 event \TYPO3\CMS\Form\Event\BeforeEmailFinisherInitializedEvent has been introduced. This event is dispatched before the \EmailFinisher is initialized and allows listeners to modify the finisher options dynamically.

This enables developers to customize email behavior programmatically, such as:

  • Setting alternative recipients based on frontend user permissions
  • Modifying the email subject or content dynamically
  • Replacing recipients with developer email addresses in test environments
  • Adding or removing CC or BCC recipients conditionally
  • Customizing reply-to addresses

The event provides access to both the finisher context (read-only) and the options array, allowing for flexible manipulation of the email configuration.

To modify the \EmailFinisher options, the following methods are available:

  • getFinisherContext(): Returns the FinisherContext containing form runtime and request information
  • getOptions(): Returns the current finisher options array
  • setOptions(): Allows setting the modified options array

Example 

The corresponding event listener class:

Example event listener class
<?php

namespace MyVendor\MyExtension\Form\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Form\Event\BeforeEmailFinisherInitializedEvent;

final class BeforeEmailFinisherInitializedEventListener
{
    #[AsEventListener('my_extension/form/modify-email-finisher-options')]
    public function __invoke(BeforeEmailFinisherInitializedEvent $event): void
    {
        $options = $event->getOptions();
        $context = $event->getFinisherContext();

        // Overwrite recipients based on FormContext
        if ($context->getFormRuntime()->getFormDefinition()->getIdentifier() === 'my-form-123') {
            $options['recipients'] = ['user@example.org' => 'John Doe'];
        }

        // Modify subject dynamically
        $options['subject'] = 'Custom subject: ' . ($options['subject'] ?? '');

        // Clear CC and BCC recipients
        $options['replyToRecipients'] = [];
        $options['blindCarbonCopyRecipients'] = [];

        $event->setOptions($options);
    }
}
Copied!

Impact 

It is now possible to dynamically modify \EmailFinisher options before email processing begins, using the new PSR-14 event BeforeEmailFinisherInitializedEvent . This provides developers with full control over email configuration without needing to extend or override the \EmailFinisher class.

Feature: #107281 - Type-specific TCAdefaults support 

See forge#107281

Description 

The TCAdefaults configuration has been extended to support type-specific syntax similar to TCEFORM, enabling different default values based on the record type.

This allows configuration like:

Page TSconfig
# Field-level default (applies to all content types)
TCAdefaults.tt_content.header_layout = 1

# Type-specific defaults (applies only to specific content types)
TCAdefaults.tt_content.header_layout.types.textmedia = 3
TCAdefaults.tt_content.frame_class.types.textmedia = ruler-before
Copied!

The same syntax is supported in User TSconfig as well.

Type-specific defaults take precedence over field-level defaults, and Page TSconfig overrides User TSconfig following the established inheritance pattern.

Implementation details 

The feature is implemented in two main areas:

  • Backend forms: The DatabaseRowInitializeNew class now processes type-specific defaults when creating new records in the backend.
  • DataHandler: The DataHandler class supports type-specific defaults when creating records programmatically via the PHP API.

Fallback behavior 

If no type-specific default is found for a given record type, the system falls back to:

  1. Field-level TCAdefaults configuration
  2. TCA 'default' configuration
  3. Database field default value

Automatic field discovery 

This enhancement makes TCAdefaults consistent with TCEFORM patterns and enables automatic field discovery.

Examples 

User TSconfig - Basic type-specific defaults
TCAdefaults.tt_content.header_layout = 1
TCAdefaults.tt_content.header_layout.types.textmedia = 3
TCAdefaults.tt_content.header_layout.types.image = 2
Copied!
Page TSconfig - Multiple fields with type-specific overrides
TCAdefaults.tt_content {
    header_layout = 1
    header_layout.types.textmedia = 3
    header_layout.types.image = 2

    frame_class = default
    frame_class.types.textmedia = ruler-before
    frame_class.types.image = none

    space_before_class = none
}
Copied!
PHP API usage - DataHandler with type-specific defaults
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
$datamap = [
    'tt_content' => [
        'NEW123' => [
            'pid' => 42,
            'CType' => 'textmedia',
            'header' => 'My Content Element',
            // header_layout and frame_class will be set automatically
            // based on type-specific TCAdefaults configuration
        ],
    ],
];
$dataHandler->start($datamap, [], $backendUser);
$dataHandler->process_datamap();
Copied!

Impact 

This feature provides a more flexible and consistent way to configure default values for different record types, reducing repetitive configuration and improving the user experience when creating new records.

The type-specific syntax aligns TCAdefaults with the established TCEFORM pattern, making the configuration more intuitive for TYPO3 developers and integrators.

Feature: #107322 - New PSR-14 AfterRichtextConfigurationPreparedEvent 

See forge#107322

Description 

A new PSR-14 event AfterRichtextConfigurationPreparedEvent has been added.

To modify the configuration, the following methods are available:

  • setConfiguration()
  • getConfiguration()

Example 

The corresponding event listener class:

Example event listener class
<?php

namespace MyVendor\MyExtension\Configuration\EventListener;

use TYPO3\CMS\Core\Configuration\Event\AfterRichtextConfigurationPreparedEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

final class AfterRichtextConfigurationPreparedEventListener
{
    #[AsEventListener('my_extension/configuration/modify-rich-text-configuration')]
    public function __invoke(AfterRichtextConfigurationPreparedEvent $event): void
    {
        $config = $event->getConfiguration();
        $config['editor']['config']['debug'] = true;
        $event->setConfiguration($config);
    }
}
Copied!

Impact 

It is now possible to modify the rich-text configuration after it has been fetched and prepared.

Feature: #107343 - PSR-14 to manipulate form creation process 

See forge#107343

Description 

A new PSR-14 event \TYPO3\CMS\Form\Event\BeforeFormIsCreatedEvent has been introduced. It serves as a direct replacement for the now removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeFormCreate'] .

The new event is dispatched immediately before a new form is created in the backend.

The event provides the following public properties:

  • $form: The form definition array
  • $formPersistenceIdentifier: The form persistence identifier used to store the new form

Example 

An example event listener could look like:

Example event listener class
<?php

namespace MyVendor\MyExtension\Form\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Form\Event\BeforeFormIsCreatedEvent;

final class MyEventListener
{
    #[AsEventListener('my_extension/before-form-is-created')]
    public function __invoke(BeforeFormIsCreatedEvent $event): void
    {
        $event->form['label'] = 'foo';
    }
}
Copied!

Impact 

With the new BeforeFormIsCreatedEvent , it is now possible to modify a form before it is created.

Feature: #107358 - Introduce Fluid page title ViewHelper 

See forge#107358

Description 

A new Fluid ViewHelper <f:page.title> has been introduced to allow setting the page title directly from Fluid templates.

This is especially useful for Extbase plugins that need to set a page title in their detail views without having to implement a custom page title provider.

EXT:my_extension/Resources/Private/Templates/Item/Show.html
<f:page.title>{item.title}</f:page.title>

<h1>{item.title}</h1>
<p>{item.description}</p>
Copied!

The ViewHelper can also be used with static content:

EXT:my_extension/Resources/Private/Templates/Static/About.html
<f:page.title>About Us - Company Information</f:page.title>

<h1>About Us</h1>
<p>Welcome to our company...</p>
Copied!

Impact 

Extension developers can now easily set page titles from their Fluid templates without creating custom page title providers for each extension. This simplifies the implementation of dynamic page titles in Fluid templates or in Extbase plugins for detail views where the title should reflect the displayed record.

The ViewHelper integrates seamlessly with TYPO3's existing page title provider system and respects the configured provider priorities.

Feature: #107359 - Introduce Fluid page meta ViewHelper 

See forge#107359

Description 

A new Fluid ViewHelper <f:page.meta> has been introduced to allow setting meta tags directly from Fluid templates using TYPO3's MetaTagManager API.

This is especially useful for Extbase plugins that need to set meta tags in their detail views without having to implement custom meta tag handling.

In addition, frontend developers do not need to write custom TypoScript anymore but can use Fluid directly.

EXT:my_extension/Resources/Private/Templates/Item/Show.html
<f:page.meta property="description">
    {item.description}
</f:page.meta>
<f:page.meta property="og:title">
    {item.title}
</f:page.meta>
<f:page.meta property="og:type">
    article
</f:page.meta>

<h1>{item.title}</h1>
<p>{item.description}</p>
Copied!

The ViewHelper supports all features of the MetaTagManager API:

OpenGraph and Twitter / X Card meta tags:

<f:page.meta property="og:title">
    My Article Title
</f:page.meta>
<f:page.meta property="og:description">
    Article description
</f:page.meta>
<f:page.meta property="twitter:card">
    summary_large_image
</f:page.meta>
Copied!

Sub-properties for complex meta tags:

<f:page.meta
    property="og:image"
    subProperties="{width: 1200, height: 630, alt: 'Article image'}"
>
    {item.image.url}
</f:page.meta>
Copied!

Custom meta tag types:

<f:page.meta property="author" type="name">
    John Doe
</f:page.meta>
<f:page.meta property="robots" type="name">
    index, follow
</f:page.meta>
Copied!

Replacing existing meta tags:

<f:page.meta property="description" replace="true">
    Override any existing description
</f:page.meta>
Copied!

ViewHelper Arguments 

  • property (required): The meta property name (for example, description, og:title)
  • type (optional): The meta type attribute (name, property, http-equiv). If not set, the appropriate MetaTagManager determines the type automatically.
  • subProperties (optional): Array of sub-properties for complex meta tags.
  • replace (optional): Boolean to replace existing meta tags with the same property (default: false).

Impact 

Extension developers can now easily set meta tags from Fluid templates without needing to use the MetaTagManager API directly in PHP code. This simplifies the implementation of SEO-optimized pages in Extbase plugins, especially for detail views where meta tags should reflect the displayed record.

The ViewHelper integrates seamlessly with TYPO3's existing MetaTagManager system and respects all configured meta tag managers and their priorities.

Feature: #107380 - PSR-14 to manipulate form duplication process 

See forge#107380

Description 

A new PSR-14 event \TYPO3\CMS\Form\Event\BeforeFormIsDuplicatedEvent has been introduced. It serves as a direct replacement for the now removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeFormDuplicate'] .

The new event is dispatched immediately before a form is duplicated in the backend.

The event provides the following public properties:

  • $form: The form definition array
  • $formPersistenceIdentifier: The form persistence identifier used to store the duplicated form

Example 

An example event listener could look like this:

Example event listener class
namespace MyVendor\MyExtension\Form\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Form\Event\BeforeFormIsDuplicatedEvent;

final class BeforeFormIsDuplicatedEventListener
{
    #[AsEventListener('my_extension/before-form-is-duplicated')]
    public function __invoke(BeforeFormIsDuplicatedEvent $event): void
    {
        $event->form['label'] = 'foo';
    }
}
Copied!

Impact 

With the new BeforeFormIsDuplicatedEvent , it is now possible to modify a form before it is duplicated.

Feature: #107382 - PSR-14 before form deletion 

See forge#107382

Description 

A new PSR-14 event BeforeFormIsDeletedEvent has been introduced. It serves as a direct replacement for the now removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeFormDelete'] .

The new event is dispatched immediately before a form is deleted in the backend.

The event provides the following public properties:

  • $formPersistenceIdentifier: The form persistence identifier (read-only)
  • $preventDeletion: A boolean flag that can be set to true to prevent the deletion of the form

The new event is stoppable. As soon as $preventDeletion is set to true, no further listener is called.

Example 

An example event listener could look like this:

Example event listener class
namespace MyVendor\MyExtension\Form\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Form\Event\BeforeFormIsDeletedEvent;

final class BeforeFormIsDeletedEventListener
{
    #[AsEventListener('my_extension/before-form-is-deleted')]
    public function __invoke(BeforeFormIsDeletedEvent $event): void
    {
        $event->preventDeletion = true;
        $persistenceIdentifier = $event->formPersistenceIdentifier;
        // Do something with the persistence identifier
    }
}
Copied!

Impact 

With the new BeforeFormIsDeletedEvent , it is now possible to prevent the deletion of a form and to add custom logic based on the delete action.

Feature: #107388 - PSR-14 to manipulate form before it is saved 

See forge#107388

Description 

A new PSR-14 event \TYPO3\CMS\Form\Event\BeforeFormIsSavedEvent has been introduced. It serves as a direct replacement for the now removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeFormSave'] .

The new event is dispatched immediately before a form is saved in the backend.

The event provides the following public properties:

  • $form: The form definition array
  • $formPersistenceIdentifier: The form persistence identifier used to store the form

Example 

An example event listener could look like this:

Example event listener class
namespace MyVendor\MyExtension\Form\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Form\Event\BeforeFormIsSavedEvent;

final class BeforeFormIsSavedEventListener
{
    #[AsEventListener('my-extension/before-form-is-saved')]
    public function __invoke(BeforeFormIsSavedEvent $event): void
    {
        $event->form['label'] = 'foo';
    }
}
Copied!

Impact 

With the new BeforeFormIsSavedEvent , it is now possible to modify a form definition as well as the form persistence identifier before it is saved.

Feature: #107435 - File replace modal with enhanced file information 

See forge#107435

Description 

The file replacement functionality has been significantly improved to provide a better user experience in the TYPO3 backend. Instead of opening the file replacement interface in a new window, it now opens in a modal dialog, maintaining the user's context and providing a more consistent experience with other TYPO3 backend operations.

The modal displays thumbnails for supported file types, allowing users to visually verify the current file before replacement. Additionally, the following detailed information is shown:

  • File type
  • File size
  • Creation date

Impact 

This enhancement significantly improves the file management experience in TYPO3 by providing a more modern, context-aware interface for file replacement. The combination of modal-based interaction and file information display creates a more efficient and user-friendly workflow that aligns with modern web application patterns.

The changes particularly benefit content editors who frequently work with files, providing them with the visual and metadata context needed to make confident file replacement decisions without interrupting their content creation workflow.

Feature: #107436 - Symfony Translation Component integration 

See forge#107436

Description 

TYPO3 now utilizes the Symfony Translation component for reading localization label files such as XLIFF and PO, instead of its custom localization parsers.

The migration brings several improvements:

  • Standardized file parsing using Symfony's translation loaders
  • Enhanced API for accessing translation catalogues
  • Support for custom translation loaders following Symfony standards

The new system maintains backward compatibility while providing a modern foundation for future improvements with translatable labels.

In addition, all label-related configuration options have been streamlined under the $GLOBALS['TYPO3_CONF_VARS']['LANG'] namespace.

The following new configuration options have been introduced:

$GLOBALS['TYPO3_CONF_VARS']['LANG']['loader']
Configure custom translation loaders.
$GLOBALS['TYPO3_CONF_VARS']['LANG']['requireApprovedLocalizations']
Moved from SYS.lang.
$GLOBALS['TYPO3_CONF_VARS']['LANG']['format']
Moved from SYS.lang.
$GLOBALS['TYPO3_CONF_VARS']['LANG']['availableLocales']
Moved from EXTCONF.lang.
$GLOBALS['TYPO3_CONF_VARS']['LANG']['resourceOverrides']
Moved from SYS.locallangXMLOverride.

Custom translation loaders 

Extension developers can now implement custom translation loaders by implementing Symfony's translation loader interfaces:

Example custom loader
use Symfony\Component\Translation\Loader\LoaderInterface;
use Symfony\Component\Translation\MessageCatalogue;

class CustomLoader implements LoaderInterface
{
    public function load(mixed $resource, string $locale, string $domain = 'messages'): MessageCatalogue
    {
        // Custom loading logic
        $catalogue = new MessageCatalogue($locale);
        // ... populate catalogue
        return $catalogue;
    }
}
Copied!

Register custom loaders via configuration:

Register custom loader in configuration
$GLOBALS['TYPO3_CONF_VARS']['LANG']['loader']['fileEnding']
    = \MyVendor\MyExtension\Translation\CustomLoader::class;
Copied!

Impact 

All previous configuration options have been moved to the new $GLOBALS['TYPO3_CONF_VARS']['LANG'] namespace. These are automatically migrated to the new location when accessing the install tool.

Please note: This functionality only affects the internal handling of translation files ("locallang" files). The public API of the localization system remains unchanged.

Feature: #107441 - Allow more hashing algorithms in FAL 

See forge#107441

Description 

Previously, FAL's LocalDriver only supported md5 and sha1 as hashing algorithms. While this may be sufficient for many use cases, it might be necessary to use different hashing algorithms depending on the specific scenario.

The method LocalDriver->hash() is now able to use any hashing algorithm that is registered in PHP itself by building a HashContext object and updating it by streaming the file content.

Impact 

FAL's LocalDriver can now make use of different hashing algorithms, e.g. crc32, sha256 and many more.

Feature: #107471 - New scheduler task wizard 

See forge#107471

Description 

A wizard to create new scheduler tasks has been introduced in the Administration > Scheduler module to significantly improve the user experience when creating new scheduler tasks. The wizard replaces the previous dropdown-based task selection in FormEngine with a modern, categorized interface similar to the content element wizard.

UX improvements 

  • Categorized task selection: Tasks are now organized by extension or category for better discoverability
  • Search functionality: Users can search and filter available tasks
  • Visual task representation: Each task displays with proper icons, titles, and descriptions

Technical improvements 

  • Prevents broken records: The old system preselected the first available task type when creating new tasks, which caused validation issues when users changed the task type, since required fields for the new type might not be properly initialized
  • Clean task type preselection: The selected task type is directly passed to FormEngine, eliminating the need to change the type in the form
  • Extensible via PSR-14 event: Extensions can modify wizard items through the new ModifyNewSchedulerTaskWizardItemsEvent

PSR-14 event 

A new PSR-14 event ModifyNewSchedulerTaskWizardItemsEvent has been introduced to allow extensions to modify the wizard items.

The event provides the following methods:

  • getWizardItems(): Returns the current wizard items array
  • setWizardItems(): Sets the complete wizard items array
  • addWizardItem(): Adds a single wizard item
  • removeWizardItem(): Removes a wizard item by key
  • getRequest(): Returns the current server request

Example 

A corresponding event listener class:

Example event listener class
namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Scheduler\Event\ModifyNewSchedulerTaskWizardItemsEvent;

final class ModifySchedulerTaskWizardListener
{
    #[AsEventListener('my-extension/scheduler/modify-wizard-items')]
    public function __invoke(ModifyNewSchedulerTaskWizardItemsEvent $event): void
    {
        // Add a custom task to the wizard
        $event->addWizardItem('my_custom_task', [
            'title' => 'My Custom Task',
            'description' => 'A custom task provided by my extension',
            'iconIdentifier' => 'my-custom-icon',
            'taskType' => 'MyVendor\\MyExtension\\Task\\CustomTask',
            'taskClass' => 'MyVendor\\MyExtension\\Task\\CustomTask',
        ]);

        // Remove an existing task
        $event->removeWizardItem('redirects_redirects:checkintegrity');

        // Modify existing wizard items
        $wizardItems = $event->getWizardItems();
        foreach ($wizardItems as $key => $item) {
            if (isset($item['title'])
                && str_contains($item['title'], 'referenceindex:update')) {
                $item['title'] = 'Update reference index';
                $event->addWizardItem($key, $item);
            }
        }
    }
}
Copied!

Impact 

  • The scheduler task creation workflow is significantly improved with better UX
  • The risk of creating broken task records due to task type changes is eliminated
  • Extensions can easily modify the wizard through the PSR-14 event
  • The interface is more consistent with other TYPO3 wizard interfaces
  • Task discovery is improved through categorization and search functionality

Feature: #107488 - Extensible scheduler task timing options 

See forge#107488

Description 

The scheduler task timing configuration has been migrated to the single TCA execution_details field, which is of type json. This field has now been enhanced to allow extensions to customize the corresponding timing-related fields, particularly the frequency field with custom cron expressions. This is achieved through the new overrideFieldTca option, available in the TCA field configuration.

Previously, frequency options were only configurable through the global $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['frequencyOptions'] array, which has been moved to the TCA configuration to provide better extensibility and consistency with TYPO3's configuration patterns, see Breaking: #107488 - Scheduler frequency options moved to TCA.

Enhanced customization options 

Extensions can now override any timing-related field configuration using the overrideFieldTca mechanism in the execution_details field.

Available fields:

  • Frequency field: frequency
  • Running type field: runningType
  • Parallel execution settings: multiple
  • Start or end date fields: start and end

Example usage 

Extensions can now add custom frequency options by creating a TCA override file:

EXT:my_extension/Configuration/TCA/Overrides/tx_scheduler_task.php
$GLOBALS['TCA']['tx_scheduler_task']['columns']['execution_details']['config']
    ['overrideFieldTca']['frequency']['config']['valuePicker']['items'][] = [
        'value' => '0 2 * * *',
        'label' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:daily_2am',
    ];
Copied!

Extensions can also add a description:

EXT:my_extension/Configuration/TCA/Overrides/tx_scheduler_task.php
$GLOBALS['TCA']['tx_scheduler_task']['columns']['execution_details']['config']
    ['overrideFieldTca']['multiple'] = [
        'description' => 'my_extension.messages:multiple.description',
    ];
Copied!

Impact 

This enhancement provides a more flexible and extensible way to configure scheduler task timing options, allowing extensions to seamlessly integrate custom timing configurations while maintaining consistency with TYPO3's configuration patterns.

Feature: #107518 - PSR-14 event to modify form elements after being added 

See forge#107518

Description 

A new PSR-14 event BeforeRenderableIsAddedToFormEvent has been introduced. It serves as an improved replacement for the now removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['initializeFormElement'] .

The new event is dispatched immediately before a renderable has been constructed and added to the corresponding form. This allows full customization of the renderable after it has been initialized.

The event provides the $renderable public property.

Example 

An example event listener could look like this:

Example event listener class
namespace MyVendor\MyExtension\Form\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Form\Event\BeforeRenderableIsAddedToFormEvent;

final class BeforeRenderableIsAddedToFormEventListener
{
    #[AsEventListener('my-extension/before-renderable-is-added-to-form-event')]
    public function __invoke(BeforeRenderableIsAddedToFormEvent $event): void
    {
        $event->renderable->setLabel('foo');
    }
}
Copied!

Impact 

With the new BeforeRenderableIsAddedToFormEvent , it is now possible to modify a renderable after it has been initialized and right before being added to its form.

Feature: #107519 - Add "discard" command to DataHandler 

See forge#107519

Description 

The \TYPO3\CMS\Core\DataHandling\DataHandler PHP API has been extended with a new "discard" command to simplify workspace management.

This new command provides a cleaner, more explicit way to discard workspace records compared to the previous approach using version commands.

The new "discard" command can be used in the $commandArray parameter when calling the DataHandler to remove versioned records from a workspace.

Impact 

The "discard" command offers a more intuitive API for workspace operations:

  • Instead of using complex version commands with actions such as "clearWSID" or "flush", you can now use the straightforward "discard" command.
  • The command name clearly indicates its purpose.
  • The command handles all aspects of discarding workspace records, including any related child records.

Usage 

When using the discard command, it is crucial to use the UID of the versioned record (workspace version), not the UID of the live record.

Discarding a workspace record using DataHandler
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;

// Example: Discard a versioned page record
$versionedPageUid = 123; // This must be the UID of the workspace version!

$commandArray = [
    'pages' => [
        $versionedPageUid => [
            'discard' => true,
        ],
    ],
];

$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
$dataHandler->start([], $commandArray);
$dataHandler->process_cmdmap();
Copied!
Discarding multiple records of different types
$commandArray = [
    'pages' => [
        456 => ['discard' => true], // Versioned page UID
    ],
    'tt_content' => [
        789 => ['discard' => true], // Versioned content element UID
        790 => ['discard' => true], // Another versioned content element UID
    ],
];

$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
$dataHandler->start([], $commandArray);
$dataHandler->process_cmdmap();
Copied!

Migration from legacy commands 

The new discard command replaces the previous version-based approach, which was not widely known:

Legacy approach (still supported but discouraged)
// Old way - will be removed in future versions
$commandArray = [
    'pages' => [
        $versionedUid => [
            'version' => [
                'action' => 'clearWSID',
            ],
        ],
    ],
];
Copied!
New recommended approach
// New way - cleaner and more explicit
$commandArray = [
    'pages' => [
        $versionedUid => [
            'discard' => true,
        ],
    ],
];
Copied!

The previous clearWSID and flush actions are still supported for backward compatibility but are considered deprecated and will be removed in future versions.

Feature: #107526 - Custom TCA types for scheduler tasks 

See forge#107526

Description 

Scheduler tasks can now be created using custom TCA types instead of the legacy :AdditionalFieldProvider approach. This enhancement allows developers to define custom database fields for specific task types, providing full flexibility of FormEngine and DataHandler for editing and persistence.

The new approach replaces three major downsides of the traditional method:

  1. Native database fields are now created automatically via TCA instead of being serialized into a JSON field.
  2. Tasks are registered via TCA record types instead of custom registration methods.
  3. Field handling is now done through standard TCA configuration, eliminating the need for AdditionalFieldProviders.

Benefits 

Using custom TCA types for scheduler tasks provides several advantages:

  • FormEngine handles validation automatically, reducing the risk of XSS or SQL injection vulnerabilities in extension code.
  • Task configuration follows standard TYPO3 form patterns.
  • Developers can use familiar TCA configuration instead of implementing custom field providers.
  • Access to all TCA field types, validation, and rendering options.

Migration 

Existing task types using custom TCA types automatically migrate existing data through the getTaskParameters() and setTaskParameters() methods:

  • During migration, getTaskParameters() is called to extract field values from the serialized task object.
  • For new TCA-based tasks, setTaskParameters() receives the full database record as an array instead of serialized data from the parameters field.
  • The task class name still matches the value of the tasktype field.

Implementation examples 

File storage indexing task 

TCA configuration in Configuration/TCA/Overrides/file_storage_indexing_task.php:

TCA configuration for the file storage indexing task
defined('TYPO3') or die();

use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Scheduler\Task\FileStorageIndexingTask;

ExtensionManagementUtility::addRecordType(
    [
        'label' => 'scheduler.messages:fileStorageIndexing.name',
        'description' => 'scheduler.messages:fileStorageIndexing.description',
        'value' => FileStorageIndexingTask::class,
        'icon' => 'mimetypes-x-tx_scheduler_task_group',
        'iconOverlay' => 'content-clock',
        'group' => 'scheduler',
    ],
    '
        --div--;core.form.tabs:general,
            tasktype,
            task_group,
            description,
            file_storage;scheduler.messages:label.fileStorageIndexing.storage,
        --div--;scheduler.messages:scheduler.form.palettes.timing,
            execution_details,
            nextexecution,
            --palette--;;lastexecution,
        --div--;core.form.tabs:access,
            disable,
        --div--;core.form.tabs:extended,',
    [],
    '',
    'tx_scheduler_task'
);
Copied!

Task class with migration support:

FileStorageIndexingTask class
namespace TYPO3\CMS\Scheduler\Task;

class FileStorageIndexingTask extends AbstractTask
{
    public $storageUid = -1;

    public function execute()
    {
        // Task execution logic
        return true;
    }

    public function getTaskParameters(): array
    {
        return [
            'file_storage' => $this->storageUid,
        ];
    }

    public function setTaskParameters(array $parameters): void
    {
        $this->storageUid = $parameters['storageUid']
            ?? $parameters['file_storage']
            ?? 0;
    }
}
Copied!

Recycler cleaner task with custom fields 

TCA configuration with custom field overrides in Configuration/TCA/Overrides/scheduler_cleaner_task.php:

TCA configuration for the recycler cleaner task
defined('TYPO3') or die();

use TYPO3\CMS\Recycler\Task\CleanerTask;

if (isset($GLOBALS['TCA']['tx_scheduler_task'])) {
    \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addRecordType(
        [
            'label' => 'recycler.tasks:cleanerTaskTitle',
            'description' => 'recycler.tasks:cleanerTaskDescription',
            'value' => CleanerTask::class,
            'icon' => 'mimetypes-x-tx_scheduler_task_group',
            'iconOverlay' => 'content-clock',
            'group' => 'recycler',
        ],
        '
            --div--;core.form.tabs:general,
                tasktype,
                task_group,
                description,
                selected_tables;recycler.tasks:cleanerTaskTCA,
                number_of_days;recycler.tasks:cleanerTaskPeriod,
            --div--;scheduler.messages:scheduler.form.palettes.timing,
                execution_details,
                nextexecution,
                --palette--;;lastexecution,
            --div--;core.form.tabs:access,
                disable,
            --div--;core.form.tabs:extended,',
        [
            'columnsOverrides' => [
                'selected_tables' => [
                    'label' => 'recycler.tasks:cleanerTaskTCA',
                    'config' => [
                        'type' => 'select',
                        'renderType' => 'selectMultipleSideBySide',
                        'size' => 10,
                        'minitems' => 1,
                        'maxitems' => 100,
                        'itemsProcFunc' =>
                            CleanerTask::class. '->getAllTcaTables',
                        'items' => [],
                    ],
                ],
            ],
        ],
        '',
        'tx_scheduler_task'
    );
}
Copied!

Task class with migration support:

CleanerTask class
namespace TYPO3\CMS\Recycler\Task;

use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Scheduler\Task\AbstractTask;

class CleanerTask extends AbstractTask
{
    protected int $period = 0;

    protected array $tcaTables = [];

    public function execute()
    {
        // Task execution logic
        return true;
    }

    public function getAdditionalInformation()
    {
        $message = sprintf(
            $this->getLanguageService()->sL(
                'recycler.tasks:cleanerTaskDescriptionTables'
            ),
            implode(', ', $this->tcaTables)
        );
        $message .= '; ';
        $message .= sprintf(
            $this->getLanguageService()->sL(
                'recycler.tasks:cleanerTaskDescriptionDays'
            ),
            $this->period
        );
        return $message;
    }

    public function getTaskParameters(): array
    {
        return [
            'selected_tables' => implode(',', $this->tcaTables),
            'number_of_days' => $this->period,
        ];
    }

    public function setTaskParameters(array $parameters): void
    {
        $tcaTables = $parameters['RecyclerCleanerTCA']
            ?? $parameters['selected_tables']
            ?? [];
        if (is_string($tcaTables)) {
            $tcaTables = GeneralUtility::trimExplode(',', $tcaTables, true);
        }
        $this->tcaTables = $tcaTables;
        $this->period = (int)(
            $parameters['RecyclerCleanerPeriod']
            ?? $parameters['number_of_days']
            ?? 180
        );
    }
}
Copied!

Creating custom task types 

To create a custom scheduler task with TCA configuration:

  1. Create the task class extending AbstractTask. Implement the core logic in the execute() method.
  2. Implement migration methods getTaskParameters() and setTaskParameters().
  3. Optionally add getAdditionalInformation() for backend listing output.
  4. Add TCA configuration in Configuration/TCA/Overrides/tx_scheduler_task_your_task.php.
  5. Define custom fields in the TCA showitem configuration.
  6. Register the task type using ExtensionManagementUtility::addRecordType().

Example custom task TCA registration in Configuration/TCA/Overrides/tx_scheduler_task_your_task.php:

Example custom task TCA registration
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

ExtensionManagementUtility::addTCAcolumns(
    'tx_scheduler_task',
    [
        'custom_field' => [
            'label' => 'my_extension.tasks:custom_field',
            'config' => [
                'type' => 'input',
            ],
        ],
    ]
);

ExtensionManagementUtility::addRecordType(
    [
        'label' => 'My Custom Task',
        'description' => 'Description of what this task does',
        'value' => \MyVendor\MyExtension\Task\CustomTask::class,
        'icon' => 'my-custom-icon',
        'iconOverlay' => 'my-custom-icon-overlay',
        'group' => 'my_extension',
    ],
    '
        --div--;General,
            tasktype,
            task_group,
            description,
            custom_field,
            number_of_days;my_extension.tasks:myTaskPeriodLabel,
        --div--;Timing,
            execution_details,
            nextexecution,
            --palette--;;lastexecution,
        --div--;Access,
            disable,
    ',
    [
        'columnsOverrides' => [
            'number_of_days' => [
                'config' => [
                    'eval' => 'required',
                ],
            ],
        ],
    ],
    '',
    'tx_scheduler_task'
);
Copied!

Impact 

  • Extension developers can create more maintainable and secure scheduler tasks.
  • Custom task configuration benefits from full FormEngine capabilities.
  • Existing tasks are automatically migrated without data loss.
  • Development follows standard TYPO3 TCA patterns.
  • Improved validation and security through FormEngine and DataHandler.

Feature: #107528 - PSR-14 event before renderable is removed from form 

See forge#107528

Description 

A new PSR-14 event \TYPO3\CMS\Form\Event\BeforeRenderableIsRemovedFromFormEvent has been introduced. It serves as an improved replacement for the now removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeRemoveFromParentRenderable'] .

The new event is dispatched immediately before a renderable is deleted from the form.

The event provides the following public properties:

  • $renderable: The form element (read-only).
  • $preventRemoval: A boolean flag that can be set to true to prevent the removal of the renderable.

The event is stoppable. As soon as $preventRemoval is set to true, no further listeners are executed.

Example 

An example event listener could look like this:

Example event listener class
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Form\Event\BeforeRenderableIsRemovedFromFormEvent;

class MyEventListener {

    #[AsEventListener(
        identifier: 'my-extension/before-renderable-is-removed-from-form-event',
    )]
    public function __invoke(BeforeRenderableIsRemovedFromFormEvent $event): void
    {
        $event->preventRemoval = true;
        $renderable = $event->renderable;
        // Custom logic before the renderable is removed
    }
}
Copied!

Impact 

With the new BeforeRenderableIsRemovedFromFormEvent , it is now possible to prevent the deletion of a renderable and to add custom logic based on the deletion.

Feature: #107537 - System resource API for system file access and public URI generation 

See forge#107537

Description 

TYPO3 allows files to be configured for multiple purposes. For example, a logo to be shown on the login page, CSS and JavaScript files to be added to web pages, or icons to be used for records. Most of the time, these are specified using the EXT syntax to reference a file within an extension. However, it can sometimes also be useful to reference an external URL or a file from a FAL storage.

To achieve this, TYPO3 Core code previously needed to parse the specified configuration, identify the type (extension file, FAL, URL), and then generate a URL from it. While there was some API available for this purpose, it was incomplete and consisted of multiple parts that were not clearly named, making them easy to misuse. As a consequence, code in the TYPO3 Core differed depending on where the configuration was used.

For users, this could lead to issues when a specific resource syntax worked in one place (for example, in TypoScript) but not in another (for example, in a Fluid email template). Moreover, the generated URLs could be inconsistent — sometimes including a cache-busting addition and sometimes not.

Third-party developers struggled with a scattered API that was hard to use and understand, especially regarding which methods should be called, in what order, and how to maintain compatibility between Composer mode and classic mode.

The new System Resource API addresses these shortcomings and enables many additional features to be built on top of it.

The code for resource resolving and URL generation is now encapsulated in a precise and centralized API in one place. This not only makes it easier to maintain and fix bugs, but ensures that such fixes apply automatically to all parts of the system where resources are configured.

Before diving into the details of the API, some terminology is clarified, followed by a top-level overview.

Naming conventions and top-level overview 

A system resource is a file or folder within a TYPO3 project. This can be a:

  • package resource – a file within an extension
  • FAL resource – a file from a FAL storage
  • app resource – a file in the TYPO3 project folder
  • URI resource – a URL

Package resource 

A package resource can now be specified with a new syntax like this:

PKG:my-vendor/package-name:Resources/Public/Icons/Extension.svg

It consists of three parts, separated by a colon (:):

  1. PKG prefix
  2. Composer name of the package (also possible in classic mode for extensions that contain a composer.json)
  3. Relative path to the file within the package

The well-known EXT syntax can still be used for the time being:

EXT:ext_name/Resources/Public/Icons/Extension.svg

This syntax is not yet deprecated, but users are advised to switch to the new syntax for new projects.

App resource 

An app resource is a file or folder within your TYPO3 installation. Such files can also be specified using the PKG syntax, using the virtual name typo3/app as package name:

PKG:typo3/app:public/typo3temp/assets/style.css

By default, only access to a fixed set of folders is allowed: public/_assets, public/typo3temp/assets, and public/uploads. Additional allowed folders can be configured via:

$GLOBALS['TYPO3_CONF_VARS']['FE']['addAllowedPaths']

FAL resource 

While users are encouraged to use package and app resources, it is also possible to specify files from FAL storages using the following syntax:

FAL:1:/identifier/of/file.svg
Copied!

It consists of three parts, separated by a colon (:):

  1. FAL prefix
  2. Storage ID (UID of the FAL storage record)
  3. Identifier of the file within this storage (for hierarchical storages, a slash-separated path)

URI resource 

A fully qualified URL (including HTTP(S) scheme) can be specified:

https://www.example.com/my/image.svg

URIs relative to the current host can be specified by prefixing them with URI: like so:

URI:/path/to/my/image.svg

The string after the URI: prefix must be a valid URI. This means, that TYPO3 will now throw an exception, rather than rendering an invalid URI to HTML, when an invalid URI is provided.

Legacy resource annotations 

The following legacy string representations can still be used, but they are deprecated and will be removed in future TYPO3 versions:

  • FAL resource (relative path to the default FAL storage): fileadmin/identifier/of/file.svg
  • App resource (relative path to the project’s public directory): typo3temp/assets/style.css
  • App resource (relative path to the project’s public directory): _assets/vite/foo.css

All representations mentioned here are resource identifiers. They are strings that uniquely identify a resource within the system.

API description 

The PHP API consists of two parts:

  1. Resource resolving
  2. URI generation

The result of resource resolving is an object (different objects for different resource types), which can then be passed to the API for URI generation.

Example PHP usage 

use TYPO3\CMS\Core\SystemResource\SystemResourceFactory;
use TYPO3\CMS\Core\SystemResource\Publishing\SystemResourcePublisherInterface;
use TYPO3\CMS\Core\SystemResource\Publishing\UriGenerationOptions;
use Psr\Http\Message\ServerRequestInterface;

public function __construct(
    private readonly SystemResourceFactory $systemResourceFactory,
    private readonly SystemResourcePublisherInterface $resourcePublisher,
) {}

public function renderUrl(string $resourceIdentifier, ServerRequestInterface $request): string
{
    $resource = $this->systemResourceFactory->createPublicResource($resourceIdentifier);
    return (string)$this->resourcePublisher->generateUri(
        $resource,
        $request,
        new UriGenerationOptions(absoluteUri: true),
    );
}
Copied!

The SystemResourceFactory and an implementation of a system resource publisher are injected via dependency injection (DI). The resource publisher is referenced using the interface SystemResourcePublisherInterface .

In the renderUrl() method, the resourceFactory is used to obtain a resource object from a given resource identifier using createPublicResource(). This object is then passed to the generateUri() method of the resourcePublisher.

The method has a second required argument for the current request. If the code has no access to the current request, null can be passed instead. Passing null is discouraged and might be deprecated in future TYPO3 versions. For CLI commands, passing null is fine, but absolute URIs (including scheme and hostname) cannot be generated in this case.

Example Fluid usage 

A new <f:resource> ViewHelper converts a resource identifier into an object that can be passed to other ViewHelpers. Currently only <f:uri.resource> accepts such resource objects, but more (for example <f:image>) will support them in the future.

<html>
<head>
    <link href="{f:resource(identifier:'PKG:typo3/cms-backend:Resources/Public/Css/webfonts.css') -> f:uri.resource()}" rel="stylesheet" />
</head>
<body>
    <f:asset.css identifier="main.css" href="PKG:my-vendor/site-package:Resources/Public/Css/main.css"/>
    <img src="{typo3.systemConfiguration.backend.loginLogo -> f:resource() -> f:uri.resource()}" alt="Logo" height="41" width="150" />
</body>
Copied!

All resource identifiers mentioned above can be passed to existing ViewHelpers like <f:asset>.

Example TypoScript usage 

page.includeCSS.extensionResource = EXT:backend/Resources/Public/Css/webfonts.css
page.includeCSS.packageResource = PKG:my-vendor/ext-name:Resources/Public/Css/main.css
page.includeCSS.appResource = PKG:typo3/app:public/_assets/style.css
page.includeCSS.uriResource = https://www.example.com/css/main.css
page.includeCSSLibs.fal = FAL:1:/templates/css/main.css
Copied!

CSS and JavaScript files can be referenced using all resource identifiers mentioned above.

Impact 

All parts of the TYPO3 Core where URLs are generated from specified resources now use the new API. This means all of those places support the new PKG syntax and have cache busting applied automatically.

TYPO3 installations using other syntax than the supported ones need to migrate their resource references. Resource identifiers such as typo3conf/ext/my_ext/Resources/Public/Image.svg no longer work consistently throughout the system and must be replaced with EXT or PKG resource identifiers.

The benefits are:

  • Consistency throughout the system for system resource resolving and URL generation.
  • A more intuitive API that helps avoid mistakes, including security-related ones.
  • Consolidation of all code currently resolving system resources and generating URLs.
  • A foundation for future features such as publishing system resources to a CDN and letting TYPO3 generate CDN URLs directly instead of parsing HTML output.

Outlook 

This change is a major step forward, but further improvements are planned:

  • Implement flexible resource publishing in Composer and classic mode, allowing different publishing strategies and configuration of additional public resource folders or files.
  • Use this API in all areas that consume private resources, most notably Fluid template files.
  • Replace FAL storage for system assets completely by leveraging the app resource to contain templates, CSS files, logos, images, or even complete themes.

Feature: #107566 - PSR-14 event after current page is resolved 

See forge#107566

Description 

A new PSR-14 event \TYPO3\CMS\Form\Event\AfterCurrentPageIsResolvedEvent has been introduced. It serves as an improved replacement for the now removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterInitializeCurrentPage'] .

The new event is dispatched after the current page has been resolved.

The event provides the following public properties:

  • $currentPage: The current page.
  • $formRuntime: The form runtime object (read-only).
  • $lastDisplayedPage: The last displayed page (read-only).
  • $request: The current request (read-only).

Example 

An example event listener could look like this:

Example event listener class
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Form\Event\AfterCurrentPageIsResolvedEvent;

final class AfterCurrentPageIsResolvedEventListener
{
    #[AsEventListener('my-extension/after-current-page-is-resolved-event')]
    public function __invoke(AfterCurrentPageIsResolvedEvent $event): void
    {
        $event->currentPage->setRenderingOption('enabled', false);
    }
}
Copied!

Impact 

With the new AfterCurrentPageIsResolvedEvent , it is now possible to manipulate the current page after it has been resolved.

Feature: #107568 - PSR-14 event before renderable is validated 

See forge#107568

Description 

A new PSR-14 event BeforeRenderableIsValidatedEvent has been introduced. It serves as an improved replacement for the now removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterSubmit'] .

The new event is dispatched right before a renderable is validated.

The event provides the following public properties:

  • $value: The submitted value of the renderable.
  • $formRuntime: The form runtime object (read-only).
  • $renderable: The renderable (read-only).
  • $request: The current request (read-only).

Example 

An example event listener could look like this:

Example event listener class
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Error\Error;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Localization\TranslationService;
use TYPO3\CMS\Form\Event\BeforeRenderableIsValidatedEvent;

final class BeforeRenderableIsValidatedEventListener
{
    #[AsEventListener('my-extension/before-renderable-is-validated')]
    public function __invoke(BeforeRenderableIsValidatedEvent $event): void
    {
        $renderable = $event->renderable;
        if ($renderable->getType() !== 'AdvancedPassword') {
            return;
        }

        $elementValue = $event->value;
        if ($elementValue['password'] !== $elementValue['confirmation']) {
            $processingRule = $renderable
                ->getRootForm()
                ->getProcessingRule($renderable->getIdentifier());

            $processingRule->getProcessingMessages()->addError(
                GeneralUtility::makeInstance(
                    Error::class,
                    GeneralUtility::makeInstance(TranslationService::class)->translate('validation.error.1556283177', null, 'EXT:form/Resources/Private/Language/locallang.xlf'),
                    1556283177
                )
            );
        }

        $event->value = $elementValue['password'];
    }
}
Copied!

Impact 

With the new BeforeRenderableIsValidatedEvent , it is now possible to modify or validate the value of a renderable element before TYPO3 Core performs its built-in validation logic. This allows extensions to inject custom validation rules or preprocessing steps before standard validation runs.

Feature: #107569 - PSR-14 event before renderable is rendered 

See forge#107569

Description 

A new PSR-14 event BeforeRenderableIsRenderedEvent has been introduced. It serves as a replacement for the now removed hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeRendering'] .

The new event is dispatched right before a renderable is rendered.

The event provides the following public properties:

  • $renderable: The form element.
  • $formRuntime: The form runtime.

Example 

An example event listener could look like this:

Example event listener class
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Form\Event\BeforeRenderableIsRenderedEvent;

final class BeforeRenderableIsRenderedEventListener
{
    #[AsEventListener('my-extension/before-renderable-is-rendered')]
    public function __invoke(BeforeRenderableIsRenderedEvent $event): void
    {
        $renderable = $event->renderable;
        if ($renderable->getType() !== 'Date') {
            return;
        }

        $date = $event->formRuntime[$renderable->getIdentifier()];
        if ($date instanceof \DateTime) {
            $event->formRuntime[$renderable->getIdentifier()] =
                $date->format('Y-m-d');
        }
    }
}
Copied!

Impact 

With the new BeforeRenderableIsRenderedEvent , it is now possible to modify a renderable before it is rendered or adjust the form runtime data before rendering.

Feature: #107581 - Improve handling of FinisherException 

See forge#107581

Description 

\FinisherException instances thrown during form processing are now caught within their respective finisher. Instead of resulting in a generic 503 error page, the exception is logged, and a user-friendly error message is displayed to the user, indicating that the form could not be submitted successfully.

The error message can be customized for each finisher using the new errorMessage option. Additionally, the newly introduced Error template can be overridden and customized.

Impact 

Users no longer see a 503 error page if a FinisherException occurs. Instead, they receive a clear, user-friendly message in the form frontend. All FinisherExceptions are logged for further analysis and debugging.

This change improves the user experience and makes error handling in forms more flexible and transparent.

Feature: #107628 - Improved backend module naming and structure 

See forge#107628

Description 

TYPO3 Core's backend module structure has been modernized with clearer, more intuitive naming conventions that better align with industry standards used by enterprise content management systems.

Module names should immediately convey their purpose to editors and administrators. This series of renamings improves clarity, discoverability, and reduces the cognitive load for both new and experienced users.

Clear and consistent naming is fundamental to good user experience. When comparing TYPO3 Core with other enterprise CMS platforms, it became evident that some of TYPO3 Core's module names were either too technical, ambiguous, or did not clearly communicate their purpose.

To summarize, the goals of this restructuring are:

  • Improved clarity: Module names immediately convey their purpose.
  • Industry alignment: Adopt naming conventions familiar to users of other enterprise CMS platforms.
  • Better discoverability: Help new users find functionality without extensive training.
  • Reduced cognitive load: Minimize confusion when navigating the backend.

Module renamings 

The following modules have been renamed.

Top-level modules 

Web => Content 

The top-level Web module has been renamed to Content.

Rationale: TYPO3 is a content management system. The primary workspace where editors create and manage content should be clearly labeled as such. The term "Web" was ambiguous and did not communicate the module's purpose. Users outside the TYPO3 ecosystem often did not understand what "Web" meant in this context.

Migration: Update module parent references from 'web' to 'content':

return [
    'my_module' => [
        'parent' => 'content',  // Previously: 'web'
    ],
];
Copied!

File => Media 

The top-level File module has been renamed to Media.

Rationale: The term "Media" clearly indicates the module's purpose of managing digital media files (images, videos, documents, audio files, etc.) within the CMS. The term "File" was too generic and technical, while "Media" is widely understood and commonly used in content management systems to refer to digital content assets.

Migration: Update module parent references from 'file' to 'media':

return [
    'my_module' => [
        'parent' => 'media',  // Previously: 'file'
    ],
];
Copied!

Site Management => Sites 

The top-level Site Management module has been renamed to Sites.

Rationale: The former label "Site Management" was long and formal, and did not align with TYPO3’s evolving, concise module naming strategy. The simplified name "Sites" improves scanability in the module menu and matches the naming of other top-level modules. It also better reflects the purpose of the module: providing an overview entry point for all configured (web)sites.

Migration: The top-level module identifier site is kept. No migration is necessary.

Admin (tools) <=> System 

The top-level module formerly known as Admin tools is now called Administration.

The purpose of this top-level module has changed. It now contains those modules useful to backend administrators, such as user and permission management, the Scheduler, and Integrations.

Most modules formerly found in Admin tools are now located in System.

Rationale: The top-level module Administration now contains modules that are used by backend administrators in their daily work. Modules that require system maintainer permissions are found in the module named System.

Migration: Modules generally accessible to backend administrators should be moved to the top-level module with the identifier admin. Modules that require system maintainer permissions or are mainly useful to system maintainers and DevOps should be moved to system.

return [
    'my_administration_module' => [
        'parent' => 'admin',  // Previously: 'system'
        'access' => 'admin',
    ],
    'my_maintainer_module' => [
        'parent' => 'system',  // Previously: 'tools'
        'access' => 'systemMaintainer',
    ],
];
Copied!

Second-level modules 

For modules, where the module identifier changed, the upgrade wizard "Migrate module permissions" migrates module level group and user permissions.

Page => Layout 

The second-level Page module has been renamed to Layout to better match its scope.

Rationale: The previous module name "Page" did not clearly convey the module’s purpose or workflow. TYPO3 provides multiple ways to interact with a page (e.g. structure, properties, preview), and the term "Page" alone did not describe which aspect was being managed. The renamed module "Layout" more accurately reflects what editors do inside the module: maintain the page layout, manage content elements, and organize them into the correct columns and grids. This provides clearer expectations, improves usability for new editors, and aligns the module name with modern TYPO3 workflows and terminology.

Migration: Since the module is just renamed, there are no migrations necessary.

List => Records 

The second-level List module has been renamed to Records to better convey its purpose and improve clarity.

Rationale: The term "List" is too generic and does not adequately communicate the module's purpose. While "List" could refer to any kind of enumeration or overview, the module actually provides structured access to database records appearing on a page. The new name "Records" is more specific and immediately communicates that this module is about working with data records—viewing, editing, and managing them at the database level.

The term "Records" also aligns with how the module is already described in its own interface ("List of database records") and better reflects the technical nature of the module's functionality. For users coming from other enterprise CMS platforms or database-driven systems, "Records" is a widely understood term that clearly indicates low-level data management capabilities.

This renaming reduces ambiguity and helps users—especially those new to TYPO3—understand that this module provides direct access to the underlying record structure, distinguishing it from the more content-focused Layout module.

Migration: The module identifier has been renamed from web_list to records. An alias is in place. However, use the new identifier when referecing.

- $this->uriBuilder->buildUriFromRoute('web_list');
+ $this->uriBuilder->buildUriFromRoute('records');
Copied!

View => Preview 

The second-level View module has been renamed to Preview to better match its scope. It has also been moved one position down after Records, as that module is considered more important for daily work.

Rationale: The term "Preview" is more precise, as it triggers a frontend preview and cannot be misunderstood as "viewing" a page in the backend context.

Migration: Since the module is already internally referred to as page_preview, no changes in referencing modules are required.

Workspaces => Publish 

The second-level Workspaces module has been renamed to Publish to better match its current scope.

Rationale: The initially introduced "Workspaces administration" tool has been reworked to move content through a publishing process in past versions. For this reason, it is now renamed to "Publish" and is only visible when inside a workspace.

Migration: The module has internally been renamed to workspaces_publish. A module alias is in place, so references to the old workspaces_admin identifier keep working as before, but it is recommended to adapt usages.

The upgrade wizard "Migrate module permissions" migrates backend user and group-level permissions for this module.

Settings => Setup 

The second level Settings module has been integrated into Setup.

Rationale: Combining the "Setup" and "Settings" gives a more concise view, since managing sites and site settings are often done as one task.

Migration: The module identifier site_settings has been removed, the existing actions edit, save and dump have been renamed to editSettings, saveSettings and dumpSettings as part of the site_configuration module identifier.

System > Backend Users => Administration > Users 

The second-level System > Backend Users module has been renamed to Administration > Users to better match its scope.

It has also been moved to the top of the to Administration top level menu as it is frequently used by administrators.

Rationale: The new name "Users" is shorter and easier to recognize in the module menu. While "Backend Users" was technically precise, the simpler term improves readability and usability, making the module easier to find for administrators performing common user management tasks.

Migration: The identifier backend_user_management is kept unchanged, no migration needed.

System > DB Check => System > Database 

The second-level DB Check module has been renamed to Database to better reflect its purpose.

Rationale: The name "Database" is clearer and avoids the ambiguity of the abbreviation "DB". The module now focuses solely on search and query functionality and no longer performs database integrity checks.

Migration: The module identifier has changed from system_dbint to system_database. An alias ensures backward compatibility. Use the new identifier when registering custom modules.

 return [
     'my_database_tool' => [
-        'parent' => 'system_dbint',
+        'parent' => 'system_database',
     ],
 ];
Copied!

Impact 

All renamed module identifiers maintain their previous names as aliases, ensuring full backward compatibility. Existing code, configurations, and third-party extensions continue to work without modification.

Developers are encouraged to update their code to use the new identifiers for consistency and clarity.

The modernized naming improves the overall user experience by making the backend more intuitive and easier to navigate, particularly for users familiar with other enterprise CMS platforms.

Feature: #107663 - Introduce submodule dependencies 

See forge#107663

Description 

Backend modules can now declare a dependency on their submodules using the new appearance['dependsOnSubmodules'] configuration option. When enabled, a module will automatically hide itself from the module menu if none of its submodules are available to the current user.

This feature enables container modules to adapt dynamically to the user's permissions and installed extensions, ensuring the module menu remains clean and only displays modules that provide actual functionality.

Example configuration 

EXT:my_extension/Configuration/Backend/Modules.php
use TYPO3\CMS\Info\Controller\InfoModuleController;

return [
    'content_status' => [
        'parent' => 'content',
        'access' => 'user',
        'path' => '/module/content/status',
        'iconIdentifier' => 'module-info',
        'labels' => 'backend.modules.status',
        'aliases' => ['web_info'],
        'navigationComponent' => '@typo3/backend/tree/page-tree-element',
        'appearance' => [
            'dependsOnSubmodules' => true,
        ],
        'showSubmoduleOverview' => true,
    ],
    'web_info_overview' => [
        'parent' => 'content_status',
        'access' => 'user',
        // ... configuration
    ],
    'web_info_translations' => [
        'parent' => 'content_status',
        'access' => 'user',
        // ... configuration
    ],
];
Copied!

In this example, the Content > Status module will only be shown in the module menu if at least one of its submodules (Overview, Translations) is available to the current user.

If all submodules are either disabled, removed, or the user lacks access permissions to them, the parent module will automatically be hidden from the module menu.

Impact 

Module menus become more intuitive and user-focused. Container modules equipped with appearance['dependsOnSubmodules'] intelligently adapt to the current context, appearing only when they offer actionable functionality to the user.

The Content > Status module leverages this feature to seamlessly disappear from the module menu when extensions are uninstalled or when users lack permissions to access its submodules, preventing dead-end navigation paths and enhancing the overall backend experience.

Feature: #107668 - Improve scheduler task group handling and display 

See forge#107668

Description 

The scheduler module has been enhanced with improved visual organization and quick editing capabilities for task groups. Task groups can now be assigned custom colors to improve visual distinction, and the group name has been made directly editable from the module view.

A new color field has been added to the tx_scheduler_task_group table, allowing administrators to assign hex color values to each task group.

UI improvements 

The scheduler module now provides several enhancements for task groups:

Color coding
Task groups can be assigned a color that is displayed as a left border on the group panel, similar to the page module label. This makes it easy to visually distinguish between different groups at a glance.
Quick edit link
The group name is now a clickable link that opens the group record for editing, providing direct access to the group name, color, and description fields without navigating through the list module.
Description display
Group descriptions are now displayed in the scheduler module directly below the group name, providing additional context about the purpose of each group.
Bold group names
Group names are now displayed in bold for better visual hierarchy and readability in the scheduler module interface.

Implementation details 

The color field is implemented as a TCA field of type 'color' with the following characteristics:

  • Stores standard hex color values (for example #FF8700).
  • Includes a value picker with 11 predefined colors aligned with the TYPO3 brand palette.

Predefined color options:

  • TYPO3 Orange (#FF8700)
  • White (#ffffff)
  • Gray (#808080)
  • Black (#000000)
  • Blue (#2671d9)
  • Purple (#5e4db2)
  • Teal (#2da8d2)
  • Green (#3cc38c)
  • Magenta (#c6398f)
  • Yellow (#ffbf00)
  • Red (#d13a2e)

These options can be customized by manipulating the $GLOBALS['TCA']['tx_scheduler_task_group']['columns']['color']['config']['valuePicker']['items'] array in your TCA overrides file.

Add a custom color option in a TCA override file
$GLOBALS['TCA']['tx_scheduler_task_group']['columns']['color']['config']['valuePicker']['items'][] = [
    'label' => 'My Color',
    'value' => '#ABCDEF',
];
Copied!

Impact 

These enhancements improve the usability of the scheduler module, particularly for installations with many task groups. The improvements allow administrators to:

  • Quickly identify related task groups through visual color coding.
  • Edit group properties directly from the scheduler module.
  • Organize tasks by category, priority, or purpose.
  • See group descriptions without opening the group record.
  • Improve visual navigation in large scheduler configurations.

The feature is fully backward compatible. Existing task groups without colors continue to work as before, displaying without a colored border.

Feature: #107679 - PSR-14 event for custom record retrieval in LinkBuilder 

See forge#107679

Description 

A new PSR-14 event \TYPO3\CMS\Frontend\Event\BeforeDatabaseRecordLinkResolvedEvent has been introduced to retrieve a record using custom code in the \TYPO3\CMS\Frontend\Typolink\DatabaseRecordLinkBuilder .

The event is dispatched with $record set to null. If an event listener retrieves a record from the database, it can assign the record as an array to $record. Doing so stops event propagation and skips the default record retrieval logic in \TYPO3\CMS\Frontend\Typolink\DatabaseRecordLinkBuilder .

Custom event listeners must handle all aspects normally performed by DatabaseRecordLinkBuilder, such as record visibility, language overlay, or version overlay, if relevant.

The event provides the following public properties (all read-only, except for $record):

  • $linkDetails: Information about the link being processed.
  • $databaseTable: The name of the database table the record belongs to.
  • $typoscriptConfiguration: The full TypoScript link handler configuration.
  • $tsConfig: The full TSconfig link handler configuration.
  • $request: The current request object.
  • $record: The database record as an array (initially null).

Example 

An example event listener could look like:

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Frontend\Event\BeforeDatabaseRecordLinkResolvedEvent;

final class MyEventListener
{
    #[AsEventListener(
        identifier: 'my-extension/before-database-record-link-resolved',
    )]
    public function __invoke(BeforeDatabaseRecordLinkResolvedEvent $event): void
    {
        // Retrieve the record from the database as an array
        $result = /* ... */;

        if ($result !== false) {
            // Setting the record stops event propagation and
            // skips the default record retrieval logic
            $event->record = $result;
        }
    }
}
Copied!

Impact 

This new event allows developers to implement custom record retrieval logic for links created with typolink, for example to apply custom access restrictions or fetch data from alternative sources before rendering a link.

Feature: #107683 - File storage tree items modification event and label support 

See forge#107683

Description 

Similar to the page tree functionality introduced in TYPO3 v12 and v13, the file storage tree now supports modification of tree items through the new PSR-14 event \TYPO3\CMS\Backend\Controller\Event\AfterFileStorageTreeItemsPreparedEvent .

The event is dispatched in the file storage TreeController after the storage tree items have been resolved and prepared. It provides the current PSR-7 request as well as the collection of file storage tree items.

Additionally, labels can now be added to file storage tree nodes via user TSconfig, using the combined identifier of the folder:

EXT:my_extension/Configuration/user.tsconfig
options.folderTree.label.1:/campaigns/ {
    label = Main Storage
    color = #ff8700
}
Copied!

Labels and status information 

Similar to the page tree, labels and status information can be added to file storage tree nodes. These features significantly improve the clarity and accessibility of the file storage tree component:

  • Labels: Each node can support multiple labels, sorted by priority. The highest priority label takes precedence, and only its marker is rendered. All additional labels are added to the title attribute of the node.
  • Status information: Can be added through the event to provide additional visual feedback. Like labels, status information is sorted by priority. Only the highest priority status indicator is displayed, while all status labels are added to the title attribute.

Example event listener 

EXT:my_extension/Classes/Backend/EventListener/ModifyFileStorageTreeItems.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend\EventListener;

use TYPO3\CMS\Backend\Controller\Event\AfterFileStorageTreeItemsPreparedEvent;
use TYPO3\CMS\Backend\Dto\Tree\Label\Label;
use TYPO3\CMS\Backend\Dto\Tree\Status\StatusInformation;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;

#[AsEventListener(
    identifier: 'my-extension/backend/modify-file-storage-tree-items',
)]
final readonly class ModifyFileStorageTreeItems
{
    public function __invoke(AfterFileStorageTreeItemsPreparedEvent $event): void
    {
        $items = $event->getItems();

        foreach ($items as &$item) {
            // Add special label for storage with uid 1
            if ($item['resource']->getCombinedIdentifier() === '1:/campaigns/') {
                $item['labels'][] = new Label(
                    label: 'A label',
                    color: '#abcdef',
                    priority: 10,
                );
                $item['statusInformation'][] = new StatusInformation(
                    label: 'An important information',
                    severity: ContextualFeedbackSeverity::INFO,
                    priority: 10,
                    icon: 'content-info',
                );
            }
        }

        $event->setItems($items);
    }
}
Copied!

Impact 

It is now possible to modify the prepared file storage tree items before they are returned by the TreeController , using the new PSR-14 event AfterFileStorageTreeItemsPreparedEvent . Additionally, labels can be assigned to file storage tree nodes via user TSconfig.

Using these functionalities helps provide visual cues and improved accessibility for editors working with file storages and folders.

Feature: #107697 - Unified file creation in Element Browser 

See forge#107697

Description 

The file creation functionality in the TYPO3 backend has been significantly improved and unified to provide a consistent user experience. Previously, creating new files required navigating to a separate page, which resulted in context loss and an inconsistent interface compared to folder creation.

File creation is now fully integrated into the Element Browser pattern, similar to how folder creation works. This change brings a consistent, modern, and more intuitive workflow to file management.

Key improvements 

Unified interface
File creation now uses the same modal-based Element Browser interface as folder creation, providing a consistent look and feel across all file operations.
Single entry point
The new New File button provides access to all file creation methods: file upload with drag-and-drop, online media (YouTube, Vimeo), and text file creation — all in one unified interface.
Context preservation
The modal interface keeps users in context with the file storage tree visible, eliminating the need to navigate away from the current view.
Advanced upload handling
The new implementation uses the drag-uploader component, which provides sophisticated duplication handling (replace, rename, skip, or use existing), replacing the previous simple "override existing" checkbox.
File extension filtering
File creation forms are automatically filtered based on allowed file types and hidden when no valid file extensions are available for the current context.
Updated button labels
For consistency and clarity, button labels have been updated from Create Folder / Create File to New Folder / New File, better reflecting the nature of the actions.

How to use 

In the File List module, click the New File button in the document header toolbar to open a modal dialog. This modal provides three ways to add files:

  1. Upload files - Click Select & upload files to choose files. The drag-and-drop uploader supports advanced duplication handling, allowing you to choose whether to replace, rename, skip, or use existing files when conflicts occur.
  2. Add online media - Enter a URL from supported online media platforms (YouTube, Vimeo) to add media files directly from the web.
  3. Create text file - Create a new empty text file with supported extensions (based on system configuration).

The modal keeps the file storage tree visible, allowing users to navigate between folders without losing context. All three options are available in a single, unified interface, streamlining the file creation workflow.

The previous Upload file button has been removed.

Impact 

The unified file creation interface provides a significantly improved user experience through better context preservation, advanced duplication handling, and a consistent modal-based workflow.

Editors benefit from having all file creation options in one place, eliminating the need to navigate between different pages for different file operations. The modal-based approach keeps the file storage tree visible, making it easier to work with files across multiple folders within a single, continuous workflow.

Feature: #107710 - Support for XLIFF 2.x translation files 

See forge#107710

Description 

TYPO3 now supports both XLIFF 1.2 and XLIFF 2.x translation file formats. The XLIFF loader automatically detects which version is used and parses the file accordingly, making the transition seamless for integrators and extension authors.

XLIFF (XML Localization Interchange File Format) is an XML-based format for storing translatable content. While TYPO3 has traditionally used XLIFF 1.2, the XLIFF 2.x standard brings improvements in structure and simplification.

Version detection 

The XLIFF loader automatically detects the file version by examining:

  1. The XML namespace (urn:oasis:names:tc:xliff:document:2.0 for XLIFF 2.0)
  2. The version attribute in the root element

No configuration or manual intervention is required - both formats work transparently side by side.

Key differences between XLIFF 1.2 and XLIFF 2.x 

For integrators working with translation files, here are the main structural differences:

XLIFF 1.2 structure:

  • Uses <trans-unit> elements directly within <body>
  • Translation approval via approved attribute (yes/no)
  • Namespace: urn:oasis:names:tc:xliff:document:1.2

Example XLIFF 1.2:

<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
    <file source-language="en" target-language="de">
        <body>
            <trans-unit id="button.submit" approved="yes">
                <source>Submit</source>
                <target>Absenden</target>
            </trans-unit>
        </body>
    </file>
</xliff>
Copied!

XLIFF 2.0 structure:

  • Uses <unit> elements containing <segment> elements
  • Translation state via state attribute on <segment> (initial, translated, reviewed, final)
  • More granular and modern structure
  • Namespace: urn:oasis:names:tc:xliff:document:2.0

Example XLIFF 2.0:

<?xml version="1.0" encoding="UTF-8"?>
<xliff version="2.0" xmlns="urn:oasis:names:tc:xliff:document:2.0"
       srcLang="en" trgLang="de">
    <file id="f1">
        <unit id="button.submit">
            <segment state="final">
                <source>Submit</source>
                <target>Absenden</target>
            </segment>
        </unit>
    </file>
</xliff>
Copied!

Translation approval handling 

TYPO3's requireApprovedLocalizations configuration is respected for both formats:

  • XLIFF 1.2: Translations with approved="no" are skipped when approval is required
  • XLIFF 2.x: Translations with state="initial" or state="translated" are treated as not approved, while state="final" and state="reviewed" are considered approved

Impact 

Extension authors and integrators can now use either XLIFF 1.2 or XLIFF 2.x format for their translation files. Existing XLIFF 1.2 files continue to work without any changes, while new projects can leverage the more modern XLIFF 2.x standard.

This also improves compatibility with modern translation tools and services that have adopted the XLIFF 2.0 standard.

Feature: #107712 - Introduce card-based submodule overview 

See forge#107712

Description 

Backend modules can now display a card-based overview of their submodules instead of automatically redirecting to the first available submodule. This new showSubmoduleOverview configuration option enables a more user-friendly navigation experience, similar to the Install Tool's maintenance card layout.

When enabled, clicking on a second-level module displays an overview page with cards for each accessible submodule. Each card shows the module's icon, title, description, and an "Open module" button, allowing users to make an informed choice about which submodule to access.

Example configuration 

return [
    'web_info' => [
        'parent' => 'content',
        'showSubmoduleOverview' => true,
        // ...
    ],
    'web_info_overview' => [
        'parent' => 'web_info',
        'access' => 'user',
        'path' => '/module/web/info/overview',
        'iconIdentifier' => 'module-info',
        'labels' => 'info.modules.overview',
        // ... routes and other configuration
    ],
    'web_info_translations' => [
        'parent' => 'web_info',
        'access' => 'user',
        'path' => '/module/web/info/translations',
        'iconIdentifier' => 'module-info',
        'labels' => 'info.modules.translations',
        // ... routes and other configuration
    ],
];
Copied!

In this example, the Content > Status module displays a card-based overview showing both Pagetree Overview and Localization Overview submodules. Users can read the description of each module before deciding which one to open.

The feature works seamlessly with the existing module permission system. Only submodules that the current user has access to are displayed in the overview. If no accessible submodules exist, a helpful information message is shown instead of an empty page.

Implementation details 

The showSubmoduleOverview option modifies the behavior in several key areas:

  1. Module routing - When set to true, the module's default route targets SubmoduleOverviewController instead of automatically redirecting to the first available submodule.
  2. Middleware behavior - The BackendModuleValidator middleware skips its automatic submodule redirection logic when this option is enabled, allowing the overview page to be displayed.
  3. Navigation enhancement - The system provides automatic navigation capabilities:

    • The SubmoduleOverviewController displays the submodule jump menu, allowing quick access to all available submodules.
    • When showSubmoduleOverview is activated, the ModuleTemplate automatically adds a "Module Overview" menu item to the submodule dropdown.
    • This allows users to easily navigate back to the overview from any submodule, especially useful when a submodule does not manually provide a "go back" button.

To provide meaningful descriptions on the overview cards, modules should define a description or shortDescription in their labels configuration. These are displayed in the card body to help users understand each submodule's purpose.

Impact 

Backend navigation becomes more intuitive and self-documenting. Container modules using showSubmoduleOverview provide users with a clear overview of available functionality, eliminating the confusion of being automatically redirected to an arbitrary first submodule.

The Content > Status module now uses this feature to present its submodules in an accessible, visually organized manner. Users can quickly understand what each submodule offers before navigating to it, improving discoverability and user experience in the TYPO3 backend.

Feature: #107725 - Support username for authentication in Redis cache backend 

See forge#107725

Description 

Since Redis 6.0, it is possible to authenticate against Redis using both a username and a password. Prior to this version, authentication was only possible with a password. With this change, the Redis cache backend in TYPO3 now supports both authentication mechanisms.

You can configure the Redis cache backend as follows:

config/system/additional.php
use TYPO3\CMS\Core\Cache\Backend\RedisBackend;

$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['pages']['backend'] = RedisBackend::class;
$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['pages']['options']
    = [
        'defaultLifetime' => 86400,
        'database' => 0,
        'hostname' => 'redis',
        'port' => 6379,
        'username' => 'redis',
        'password' => 'redis',
    ];
Copied!

Impact 

The RedisBackend now supports authentication using both a username and a password.

The password configuration option is now typed as array|string. Using an array for this configuration option is deprecated and will be removed in TYPO3 v15.0.

Feature: #107755 - Add creation information for redirects 

See forge#107755

Description 

The redirects component has been extended with additional meta information to improve audit trails and team collaboration. Redirects now automatically capture and display:

  • Creation timestamp
  • Creating backend user

After months or even years, teams frequently wonder why a particular redirect exists and who created it. Displaying the creator (backend user) and creation date directly in the backend module makes auditing and communication significantly easier.

Impact 

All newly created redirects automatically include creation information, regardless of whether they are created:

  • Manually through the redirects backend module
  • Automatically when updating a page slug
  • Programmatically through the DataHandler API

Existing redirects created before this feature will only show the creation date, as the user information was not tracked previously.

Feature: #107759 - TranslateViewHelper supports translation domain syntax 

See forge#107759

Description 

The Fluid <f:translate> ViewHelper now supports the new translation domain syntax, providing a more concise and readable way to reference translation labels.

A new domain attribute has been added that accepts both traditional extension names and the new translation domain names.

The ViewHelper now supports multiple ways to specify translations:

  1. New domain attribute - recommended for new code:

    <f:translate key="form.legend" domain="my_extension" />
    <f:translate key="form.legend" domain="my_extension.messages" />
    Copied!
  2. Inline domain syntax in key - shortest form:

    <f:translate key="my_extension.messages:form.legend" />
    <f:translate key="LLL:my_extension.messages:form.legend" />
    Copied!
  3. Traditional extensionName attribute - still supported:

    <f:translate key="form.legend" extensionName="MyExtension" />
    <f:translate key="form.legend" extensionName="my_extension" />
    Copied!
  4. Full LLL reference - classic syntax, still supported:

    <f:translate key="LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:form.legend" />
    Copied!

Domain attribute priority 

When both domain and extensionName are provided, the domain attribute takes precedence:

<!-- Uses "other_extension.messages" domain, not "MyExtension" -->
<f:translate key="label" domain="other_extension.messages" extensionName="MyExtension" />
Copied!

Automatic domain detection 

If neither domain nor extensionName are specified, the ViewHelper attempts to automatically detect the translation domain from the context:

  1. Extbase context: Uses the controller extension name
  2. Key with domain prefix: Extracts domain from key="domain:id"
  3. LLL reference: Parses the extension key from the file path

Examples 

Using domain attribute with full domain name 

You can specify the exact translation domain including resource names:

<f:translate key="menu.item" domain="backend.toolbar" />
<!-- Resolves to: EXT:backend/Resources/Private/Language/locallang_toolbar.xlf -->
Copied!

Inline domain syntax in key 

The shortest form combines domain and key directly:

<f:translate key="core.form.tabs:general" />
<!-- Resolves to: EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf -->
Copied!

With arguments and default values 

All existing ViewHelper features work with the new syntax:

<f:translate
    key="users"
    domain="backend.messages"
    arguments="{0: count}"
    default="No users found"
/>
Copied!

Using variables for domain 

Dynamic domain selection is supported:

<f:translate key="label.title" domain="{myDomain}" />
Copied!

Migration from extensionName 

Existing code using extensionName continues to work without changes. However, new code should prefer the domain attribute combined with translation domain syntax for better readability.

Before:

<f:translate key="LLL:EXT:my_extension/Resources/Private/Language/locallang_form.xlf:legend" />
<f:translate key="legend" extensionName="MyExtension" />
Copied!

After:

<f:translate key="my_extension.form:legend" />
<f:translate key="legend" domain="my_extension.form" />
Copied!

Impact 

The <f:translate> ViewHelper now provides a more convenient and readable way to reference translations using the new translation domain syntax. This reduces verbosity in Fluid templates and aligns with modern translation system conventions used in Symfony and other frameworks.

All existing syntax forms remain fully supported, ensuring backward compatibility. The new syntax can be adopted incrementally within a project, and both old and new forms can coexist in the same template.

Feature: #107783 - Register metadata extractors via interface 

See forge#107783

Description 

Metadata extractors are service classes that are automatically executed whenever an asset or file is added to the FAL storage, or FAL indexing is executed.

Registration of metadata extractors now happens automatically when the required interface ExtractorInterface is implemented by the class, utilizing autoconfigure tagging provided by the Symfony Dependency Injection framework.

No further manual registration is required.

Additionally, the class ExtractorRegistry now uses strong type declarations, which should not affect public consumers. The interface remains unchanged in its type declarations.

Impact 

Instances of ExtractorInterface are now detected and registered automatically.

Feature: #107784 - Autoconfigure backend layout data providers 

See forge#107784

Description 

Backend layout providers are now autoconfigured once they implement the required DataProviderInterface . Each autoconfigured layout provider is tagged with page_layout.data_provider in the service container and is automatically added to the global DataProviderCollection , if autoconfiguration is enabled in Services.yaml or Services.php.

Since backend layout providers must be identifiable to establish a relation to a configured backend layout, the corresponding interface has been extended. It now requires backend layout providers to implement a new method getIdentifier().

Example 

EXT:my_extension/Classes/View/BackendLayout/MyLayoutDataProvider.php
use TYPO3\CMS\Backend\View\BackendLayout\DataProviderInterface;

final class MyLayoutDataProvider implements DataProviderInterface
{
    // ...

    public function getIdentifier(): string
    {
        return 'my_provider';
    }
}
Copied!

Manual service configuration 

If autoconfiguration is disabled, manually tag the service in Services.yaml:

EXT:my_extension/Configuration/Services.yaml
services:
  MyVendor\MyExtension\View\BackendLayout\MyLayoutDataProvider:
    tags:
      - name: page_layout.data_provider
Copied!

Provider ordering 

If you need to control the order in which providers are processed, use service priorities in your Services.yaml:

EXT:my_extension/Configuration/Services.yaml
services:
  MyVendor\MyExtension\View\BackendLayout\MyLayoutDataProvider:
    tags:
      - name: page_layout.data_provider
        priority: 100
Copied!

Impact 

Backend layout data providers are now automatically registered and can be used without further configuration. This improves developer experience and reduces configuration overhead. The previous registration method via $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['BackendLayoutDataProvider'] can no longer be used. Instead, existing backend layout providers must implement the new method getIdentifier().

Using the new autoconfigure-based approach, developers can still support multiple TYPO3 Core versions by keeping the legacy array-based approach next to the new autoconfigure-based configuration.

Feature: #107791 - Reports module uses native submodule overview 

See forge#107791

Description 

The Administration > Reports module has been refactored to use TYPO3 Core’s native submodule system with the showSubmoduleOverview feature instead of a custom report registration mechanism. This provides a more consistent user experience and aligns the Reports module with other TYPO3 backend modules.

The module now displays a card-based overview of available reports, similar to other modules like the Content > Status module. Each report is registered as a proper backend submodule in Configuration/Backend/Modules.php.

Changes 

Module structure:

  • Administration > Reports is now a container module with showSubmoduleOverview enabled.
  • Individual reports (Status, Record Statistics) are registered as submodules.
  • The native submodule overview automatically provides cards with icons, titles, and descriptions.

Benefits:

  • Automatic "Module Overview" menu item in the submodule dropdown.
  • Consistent navigation experience across all TYPO3 backend modules.
  • Simpler architecture without custom registration infrastructure.
  • Easier to extend - just add submodules to Modules.php.

Example 

Creating a custom report now follows the same mechanism as registering a backend submodule:

EXT:my_extension/Configuration/Backend/Modules.php
return [
    'system_reports_myreport' => [
        'parent' => 'system_reports',
        'access' => 'admin',
        'path' => '/module/system/reports/myreport',
        'iconIdentifier' => 'my-report-icon',
        'labels' => 'my_extension.module',
        'routes' => [
            '_default' => [
                'target' => \MyVendor\MyExtension\Controller\MyReportController::class . '::handleRequest',
            ],
        ],
    ],
];
Copied!

The controller returns a standard PSR-7 response with rendered content:

EXT:my_extension/Classes/Controller/MyReportController.php
use TYPO3\CMS\Core\Attribute\AsController;
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

#[AsController]
final readonly class MyReportController
{
    public function __construct(
        protected ModuleTemplateFactory $moduleTemplateFactory,
    ) {}

    public function handleRequest(ServerRequestInterface $request): ResponseInterface
    {
        $view = $this->moduleTemplateFactory->create($request);
        $view->assign('data', $this->collectReportData());
        return $view->renderResponse('MyReport');
    }
}
Copied!

Impact 

The Administration > Reports module now provides a cleaner and more intuitive interface using standard TYPO3 backend navigation patterns.

Extension developers can create custom reports by registering them as backend submodules, using the same module registration mechanisms already available in TYPO3, instead of implementing a separate custom report interface.

Feature: #107794 - Improved breadcrumb navigation in backend 

See forge#107794

Description 

The TYPO3 backend now provides contextual breadcrumb navigation in the document header of backend modules, helping users understand their current location and navigate back through hierarchies.

Breadcrumbs are automatically displayed when:

  • Editing records (pages, content elements, etc.)
  • Creating new records
  • Browsing file storages and folders
  • Working with multiple records simultaneously

The breadcrumb navigation includes the following features:

Smart context detection
Breadcrumbs automatically adapt based on what you are working with — whether it is a page, content element, file, or folder.
Hierarchical navigation
Click any breadcrumb item to navigate back to that level in the hierarchy. For pages, the complete page tree path is shown.
Module awareness
Breadcrumbs remember which module you are in and keep you in that module when navigating (for example, staying in the Info module instead of switching to the Page module).
Route preservation
When navigating through breadcrumbs, the current module action or sub-route is preserved (for example, remaining in edit view when clicking parent pages).
Responsive design
On smaller screens, breadcrumb items automatically collapse into a dropdown to save space while maintaining full functionality.

Impact 

Backend users benefit from improved navigation and orientation:

  • Always know where you are: The breadcrumb trail shows your current location in the page tree, file system, or record hierarchy.
  • Quick navigation: Jump back to any parent level with a single click instead of using the browser’s back button or tree navigation.
  • Context preservation: Stay within your current module when navigating through parent items.
  • Special states visible: When creating new records or editing multiple items, this is clearly indicated in the breadcrumb trail.

Examples 

Page editing
When editing page Contact in a site structure like Home → Company → Contact, the breadcrumb shows: HomeCompanyContact
Content creation
When creating a new content element on page About, the breadcrumb shows: HomeAboutCreate New Content Element
File management
When browsing fileadmin/images/products/ the breadcrumb shows: fileadminimagesproducts

For extension developers 

Setting basic breadcrumb context 

Custom backend modules can integrate breadcrumb navigation using new convenience methods on DocHeaderComponent :

// For page-based modules
$view->getDocHeaderComponent()->setPageBreadcrumb($pageInfo);

// For record editing
$view->getDocHeaderComponent()->setRecordBreadcrumb('tt_content', 123);

// For file/folder browsing
$view->getDocHeaderComponent()->setResourceBreadcrumb($file);
Copied!

These methods automatically generate appropriate breadcrumb trails including:

  • Page tree hierarchy for page-based modules
  • Parent pages for content records
  • Folder structure for file resources
  • Module hierarchy for third-level modules

Adding suffix nodes for special states 

The addBreadcrumbSuffixNode() method allows appending custom breadcrumb nodes after the main breadcrumb trail. This is useful for indicating special states or actions such as:

  • “Create New” actions when creating records
  • “Edit Multiple” states when editing multiple records
  • Custom contextual information specific to the current view

Example: Adding a "Create New" suffix node

use TYPO3\CMS\Backend\Dto\Breadcrumb\BreadcrumbNode;

$view = $this->moduleTemplateFactory->create($request);
$docHeader = $view->getDocHeaderComponent();

// Set main breadcrumb context (for example current page)
$docHeader->setPageBreadcrumb($pageInfo);

// Add suffix node for "Create New" action
$docHeader->addBreadcrumbSuffixNode(
    new BreadcrumbNode(
        identifier: 'new',
        label: 'Create New Content Element',
        icon: 'actions-add'
    )
);
Copied!

Example: Multiple suffix nodes

use TYPO3\CMS\Backend\Dto\Breadcrumb\BreadcrumbNode;

$docHeader = $view->getDocHeaderComponent();
$docHeader->setRecordBreadcrumb('pages', $pageUid);

// First suffix: editing mode
$docHeader->addBreadcrumbSuffixNode(
    new BreadcrumbNode(
        identifier: 'edit',
        label: 'Edit',
        icon: 'actions-document-open'
    )
);

// Second suffix: specific field
$docHeader->addBreadcrumbSuffixNode(
    new BreadcrumbNode(
        identifier: 'field',
        label: 'Page Properties'
    )
);
Copied!

Example: Clickable suffix nodes

Suffix nodes can also be clickable by providing a URL:

use TYPO3\CMS\Backend\Dto\Breadcrumb\BreadcrumbNode;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
$url = (string)$uriBuilder->buildUriFromRoute(
    'web_layout',
    ['id' => $pageUid, 'mode' => 'preview']
);

$docHeader->addBreadcrumbSuffixNode(
    new BreadcrumbNode(
        identifier: 'preview',
        label: 'Preview Mode',
        icon: 'actions-view',
        url: $url
    )
);
Copied!

Deprecation notice 

The previous setMetaInformation() method has been deprecated in favor of the new breadcrumb API. See Deprecation: #107813 - Deprecate MetaInformation API in DocHeader for migration instructions.

Feature: #107795 - Introduce Integrations module 

See forge#107795

Description 

The new Integrations module has been introduced in the TYPO3 backend under the Administration section. This module serves as the central hub for connecting TYPO3 with external systems and third-party services.

The module uses the card-based submodule overview feature to provide an intuitive and organized interface for managing different types of integrations. At present, it consolidates the following existing modules as third-level submodules:

  • Webhooks - Manage outgoing HTTP webhooks to external systems
  • Reactions - Manage incoming HTTP webhooks from external systems

The Integrations module uses a three-level hierarchy structure:

  • Administration (main module)
  • Integrations (second-level parent module with card-based overview)
  • Webhooks / Reactions (third-level modules)

Module navigation 

The third-level modules (Administration > Integrations > Webhooks and :guilabel:`Administration > Integrations > Reactions) now include:

  • Doc header module menu - Quick navigation dropdown to switch between submodules or return to the Integrations overview
  • Go back button - Direct link to return to the Integrations overview

Backward compatibility 

The existing module identifiers continue to work through aliases:

  • webhooks_management redirects to integrations_webhooks
  • system_reactions redirects to integrations_reactions

Impact 

The new Administration > Integrations module provides a centralized location for managing all types of external system integrations in TYPO3. This improves backend organization and user experience by grouping related functionality together.

The module is designed to be extensible, allowing future integration types, such as translation services, AI platforms, or other external tools, to be added as additional third-level modules within the Integrations hub.

Feature: #107812: Setting to restrict Latest changed Pages Widget to current user's changes 

See forge#107812

Description 

Building upon the configurable dashboard widgets functionality introduced in Feature: #107036 - Configurable dashboard widgets, the existing Latest changed Pages widget can now be configured to display only pages that have been changed by the current TYPO3 user.

Only show my changes

Impact 

  • TYPO3 users can use the Latest changed Pages widget to quickly resume their recent work.
  • Existing setups are unaffected unless this option is explicitly enabled.

Feature: #107823 - ComponentFactory for backend components 

See forge#107823

Description 

A new \TYPO3\CMS\Backend\Template\Components\ComponentFactory class has been introduced as the central location for all backend component creation. It provides factory methods for buttons and menu components, offering both pre-configured buttons for common patterns and basic component creation methods.

The ComponentFactory serves multiple purposes:

  1. Pre-configured common buttons – Ready-to-use buttons like back, close, save, reload, and view with standardized icons, labels, and behavior.
  2. Basic button creation – Factory methods for creating button instances (previously available only on ButtonBar).
  3. Menu component creation – Factory methods for creating Menu and MenuItem instances (previously available only on MenuRegistry and Menu).

The deprecated ButtonBar::make*(), Menu::makeMenuItem(), and MenuRegistry::makeMenu() methods have been replaced by ComponentFactory, providing a cleaner separation of concerns where container classes manage organization and ComponentFactory handles component creation.

Additionally, several "add" methods now support fluent interface patterns to enable method chaining for improved code readability.

Database record list and file list 

The \TYPO3\CMS\Backend\RecordList\DatabaseRecordList and \TYPO3\CMS\Filelist\FileList classes, which are responsible in the backend to show all lists of records and files, now make use of the ComponentFactory. The list of "action buttons" is no longer represented with plain HTML and allows a clear distinction of primary and secondary actions.

For this, the new enum \TYPO3\CMS\Backend\Template\Components\ActionGroup and \TYPO3\CMS\Backend\Template\Components\ComponentGroup are also added to the Button API to allow this grouping.

The existing PSR-14 events ( \TYPO3\CMS\Backend\RecordList\Event\ModifyRecordListRecordActionsEvent , \TYPO3\CMS\Filelist\Event\ProcessFileListActionsEvent ) which are used within these classes have been streamlined to deal with these API changes. Details can be found in the related breaking changes document of forge#107884.

The Button API has also been enhanced to allow passing a new \TYPO3\CMS\Backend\Template\Components\Buttons\ButtonSize enum to differentiate the buttons for certain icon sizes.

Available Factory Methods 

The ComponentFactory provides two categories of methods:

Pre-configured common buttons:

These methods provide ready-to-use buttons with sensible defaults. The returned instances are fully mutable and can be further customized using fluent interface methods (for example, setDataAttributes(), setClasses(), setIcon()).

URL parameters accept both string and UriInterface for convenience.

  • createBackButton(string|UriInterface $returnUrl) – Standard back navigation with "Go back" label.
  • createCloseButton(string|UriInterface $closeUrl) – Close button for modal-like views.
  • createSaveButton(string $formName = '') – Standard save button for forms.
  • createReloadButton(string|UriInterface $requestUri) – Reload current view.
  • createViewButton(array $previewDataAttributes = []) – View/preview page button with data attributes.

Basic button creation:

  • createLinkButton() – Creates a new LinkButton instance.
  • createInputButton() – Creates a new InputButton instance.
  • createGenericButton() – Creates a new GenericButton instance.
  • createSplitButton() – Creates a new SplitButton instance.
  • createDropDownButton() – Creates a new DropDownButton instance.
  • createFullyRenderedButton() – Creates a new FullyRenderedButton instance.
  • createShortcutButton() – Creates a new ShortcutButton instance.
  • createDropDownDivider() – Creates a new DropDownDivider instance.
  • createDropDownItem() – Creates a new DropDownItem instance.

Menu component creation:

  • createMenu() – Creates a new Menu instance.
  • createMenuItem() – Creates a new MenuItem instance.

The Button API has also been enhanced to allow passing a new enum to differentiate the buttons for certain icon sizes.

Improvements to Button API types 

The following button types can use getSize() and setSize() methods in their instance to set the icon size with the \TYPO3\CMS\Backend\Template\Components\Buttons\ButtonSize enum, choosing between a small and medium variant (utilizing CSS classes internally):

  • \TYPO3\CMS\Backend\Template\Components\Buttons\DropDownButton
  • \TYPO3\CMS\Backend\Template\Components\Buttons\GenericButton
  • \TYPO3\CMS\Backend\Template\Components\Buttons\LinkButton

Impact 

Backend module developers should now inject ComponentFactory in their controllers to create buttons. The factory provides:

  1. Pre-configured buttons for common patterns (back, close, save, reload, view).
  2. Basic button creation methods (formerly only available on ButtonBar).

The ButtonBar::make*() methods continue to work but are deprecated and will be removed in TYPO3 v15. This change provides a cleaner architecture where ComponentFactory handles all button creation and ButtonBar focuses solely on managing button positioning and organization.

Example – Using pre-configured buttons (inject ComponentFactory):

use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;

// In controller constructor
public function __construct(
    protected readonly ComponentFactory $componentFactory,
) {}

// In controller action
public function editAction(): ResponseInterface
{
    $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();

    // Use pre-configured back button
    $backButton = $this->componentFactory->createBackButton($returnUrl);
    $buttonBar->addButton($backButton, ButtonBar::BUTTON_POSITION_LEFT, 1);

    // Use pre-configured save button
    $saveButton = $this->componentFactory->createSaveButton('editform');
    $buttonBar->addButton($saveButton, ButtonBar::BUTTON_POSITION_LEFT, 2);

    // ...
}
Copied!

Example – Creating basic buttons via ComponentFactory :

use TYPO3\CMS\Backend\Template\Components\ComponentFactory;

// Inject ComponentFactory in constructor
public function __construct(
    protected readonly ComponentFactory $componentFactory,
) {}

// Create basic buttons via factory
$linkButton = $this->componentFactory->createLinkButton()
    ->setHref($url)
    ->setTitle('Custom')
    ->setIcon($icon);
Copied!

Example - Using the ModuleTemplate convenience method:

public function __construct(
    protected readonly ComponentFactory $componentFactory,
) {}

public function myAction(): ResponseInterface
{
    // Shorthand: use ModuleTemplate::addButtonToButtonBar()
    $this->moduleTemplate->addButtonToButtonBar(
        $this->componentFactory->createReloadButton(
            $this->request->getUri()->getPath()
        ),
        ButtonBar::BUTTON_POSITION_RIGHT
    );

    $this->moduleTemplate->addButtonToButtonBar(
        $this->componentFactory->createBackButton($returnUrl),
        ButtonBar::BUTTON_POSITION_LEFT,
        1
    );

    // ...
}
Copied!

Example - Customizing pre-configured buttons:

public function __construct(
    protected readonly ComponentFactory $componentFactory,
) {}

public function myAction(): ResponseInterface
{
    // Pre-configured buttons return mutable instances that can be further
    // customized.
    $reloadButton = $this->componentFactory
        ->createReloadButton(
            (string)$this->uriBuilder->buildUriFromRoute(
                $currentModule->getIdentifier()
            )
        )
        ->setDataAttributes(['csp-reports-handler' => 'refresh']);

    $this->moduleTemplate->addButtonToButtonBar(
        $reloadButton,
        ButtonBar::BUTTON_POSITION_RIGHT
    );

    // Add custom styling or behavior to a save button
    $saveButton = $this->componentFactory
        ->createSaveButton('myform')
        ->setClasses('btn-primary custom-save')
        ->setDataAttributes(['validate' => 'true']);

    $this->moduleTemplate->addButtonToButtonBar(
        $saveButton,
        ButtonBar::BUTTON_POSITION_LEFT
    );

    // URL parameters accept both string and UriInterface
    $backButton = $this->componentFactory->createBackButton(
        $this->request->getUri()
    ); // UriInterface
    // or
    $backButton = $this->componentFactory->createBackButton(
        '/return/url'
    ); // string

    // ...
}
Copied!

Fluent Interface Improvements 

Several "add" methods now support fluent interface patterns, enabling method chaining:

ButtonBar

$buttonBar
    ->addButton($backButton, ButtonBar::BUTTON_POSITION_LEFT, 1)
    ->addButton($saveButton, ButtonBar::BUTTON_POSITION_LEFT, 2);
Copied!

Menu

$menu
    ->addMenuItem($listItem)
    ->addMenuItem($gridItem)
    ->addMenuItem($tableItem);
Copied!

MenuRegistry

$menuRegistry
    ->addMenu($viewMenu)
    ->addMenu($filterMenu);
Copied!

These changes provide a more fluent API while maintaining backward compatibility, as the return values were previously ignored ( void).

Design Rationale 

Why fluent interface instead of named parameters?

The ComponentFactory intentionally uses a fluent interface approach (chained method calls) rather than accepting parameters in factory methods. This design decision was made for several important reasons:

Consistency with TYPO3 patterns
The fluent interface pattern is well-established throughout TYPO3's codebase and familiar to extension developers. Introducing a different pattern here would be inconsistent with the rest of the framework.
Diverse button types with different properties
Different button types have vastly different configuration requirements. For example, InputButton needs name/value/form attributes, LinkButton needs href attributes, DropDownButton needs items, and SplitButton needs primary and secondary actions. A unified parameter-based approach does not fit this diversity well and would lead to confusing method signatures with many optional parameters.
Pre-configured buttons solve common cases
The factory already provides pre-configured methods like createSaveButton(), createBackButton(), createCloseButton(), createReloadButton(), and createViewButton() that handle the most common use cases with minimal code.
Avoids duplication and maintenance burden
A parameter-based approach would require duplicating all button-specific configuration knowledge in both the button classes and the factory methods. This creates a maintenance burden where changes to a button's properties must be reflected in multiple locations.
Keeps factory simple and maintainable
By keeping factory methods focused on instantiation, the ComponentFactory remains simple, maintainable, and easy to extend. Each button class maintains complete ownership of its own configuration logic.

Fluent interface is already concise

// Common case – use pre-configured button
$saveButton = $this->componentFactory->createSaveButton('myform');

// Custom case - fluent interface is clear and flexible
$customButton = $this->componentFactory->createInputButton()
    ->setName('custom_action')
    ->setValue('1')
    ->setTitle('Custom Action')
    ->setIcon(
        $this->iconFactory->getIcon('actions-heart', IconSize::SMALL)
    );
Copied!

This design ensures the API remains maintainable, consistent with TYPO3 conventions and suitable for the diverse requirements of different button types while still providing convenience methods for common patterns.

Feature: #107871 - Autoconfigure backend avatar providers 

See forge#107871

Description 

Backend avatar providers must either use the PHP attribute #[AsAvatarProvider] or be manually tagged in the service container with backend.avatar_provider.

When autoconfiguration is enabled in Services.yaml or Services.php, applying #[AsAvatarProvider] will automatically add the backend.avatar_provider tag. Otherwise, the tag must be configured manually.

Example 

EXT:my_extension/Classes/Backend/Avatar/MyAvatarProvider.php
use TYPO3\CMS\Backend\Attribute\AsAvatarProvider;
use TYPO3\CMS\Backend\Backend\Avatar\AvatarProviderInterface;

#[AsAvatarProvider(
    'my_provider',
    before: ['provider-1'],
    after: ['provider-2']
)]
final class MyAvatarProvider implements AvatarProviderInterface
{
    // ...
}
Copied!

Impact 

Backend avatar providers are now automatically registered using the PHP attribute #[AsAvatarProvider]. This improves the developer experience and reduces configuration overhead. The previous registration method via $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['backend']['avatarProviders'] can no longer be used.

To support multiple TYPO3 Core versions simultaneously, extensions may still implement the legacy array-based registration alongside the new autoconfiguration-based approach.

Feature: #107873 - New Bookmarks Widget 

See forge#107873

Description 

A new Bookmarks dashboard widget has been added to display the current user's bookmarks directly in the TYPO3 Dashboard. This allows editors and administrators to quickly access frequently used pages, records, or modules without navigating through the backend menu.

Building upon the configurable dashboard widgets functionality introduced in Feature: #107036 - Configurable dashboard widgets, the Bookmarks widget supports the following settings:

Widget Label
Custom title for the widget instance. If left empty and a specific group is selected, the group title is used as widget label automatically
Group
Filter bookmarks by a specific bookmark group, or show all groups.
Limit
Maximum number of bookmarks to display (default: 10).

Impact 

  • Editors can add the Bookmarks widget to their dashboard for quick access to their saved bookmarks.
  • Each widget instance can be configured independently, allowing multiple Bookmarks widgets with different group filters on the same dashboard.
  • Existing bookmarks are displayed automatically without additional setup.

Feature: #107875 - Improved DocHeader layout and unified language selector 

See forge#107875

Description 

The TYPO3 backend document header (DocHeader) has been reorganized to provide a more consistent and intuitive user experience across all backend modules.

This feature includes the following improvements:

Unified language selector 

All backend modules with multi-language support now use a consistent language selector in the top-right area of the DocHeader. This includes:

  • Content > Layout
  • Content > List
  • Content > Preview
  • Record editing (FormEngine)

The language selector displays the currently selected language as the button text (for example "English", "German", and the special All languages) with a descriptive "Language:" prefix for screen readers. This provides immediate visual feedback about the active language while maintaining full accessibility.

The language selector now allows creating new page translations directly from the dropdown (except in Preview mode). The dropdown is organized with existing translations shown first, followed by a divider and a "Create new translation" section header. Languages that do not yet have a translation appear in this separate section.

Selecting a language from the "Create new translation" section will:

  1. Create the page translation via the DataHandler.
  2. Open the FormEngine to edit the newly created translation.
  3. Provide a consistent workflow across all modules.

Unified module actions menu 

Modules with submodules or multiple "actions" now consistently display these options as a dropdown button in the DocHeader button bar. The dropdown button displays the currently active module or action as the button text, providing clear visual context to users.

For accessibility, each dropdown includes a descriptive label:

  • "Display mode" for the Content > Layout module view selector (layout vs language comparison)
  • "Module actions" for general module or submodule navigation
  • Custom labels defined by individual menu configurations

These labels are implemented using visually hidden label elements, ensuring screen readers announce both the dropdown's purpose (for example, "Display mode:") and the current selection (for example, "Layout").

The dropdown is automatically hidden when only a single action is available, reducing visual clutter and showing the dropdown only when navigation choices are actually available.

Examples include Administration > Users with Overview, Users, Groups actions, as well as Content > Status with "Pagetree Overview", "Localization Overview", and more.

This replaces the previous inconsistent mixture of:

  • Back buttons
  • Inline button groups
  • Module menus in different locations

Reorganized DocHeader layout 

The DocHeader now has a more logical structure:

Top row (navigation bar):

  • Left side: Breadcrumb navigation showing the current location
  • Right side: Language selector (when applicable)

Second row (button bar):

  • Left side, group 0: Module actions dropdown (when applicable)
  • Left side, groups 1+: Additional action buttons and context-specific buttons (Save, Close, and so on)
  • Right side: Functional buttons such as "Reload", "Bookmark", or "Clear cache"

Technical details 

Module actions dropdown 

Controllers can use the existing ModuleTemplate::makeDocHeaderModuleMenu() method to automatically create a module actions dropdown based on the module configuration:

$view = $this->moduleTemplateFactory->create($request);
$view->makeDocHeaderModuleMenu();
Copied!

For backward compatibility, the MenuRegistry API is automatically converted to module actions dropdowns.

Language selector integration 

The language selector is integrated via the DocHeaderComponent::setLanguageSelector() method.

First, inject the \TYPO3\CMS\Backend\Template\Components\ComponentFactory into your controller:

use TYPO3\CMS\Backend\Template\Components\ComponentFactory;

public function __construct(
    private readonly ComponentFactory $componentFactory,
) {}
Copied!

Then build the language selector dropdown with language items:

// Get available site languages for the current page
$siteLanguages = $this->site->getAvailableLanguages($pageRecord, $backendUser);
$currentLanguageId = (int)$moduleData->get('language');

// Build dropdown button
$languageDropdown = $this->componentFactory->createDropDownButton()
    ->setLabel($languageService->sL('core.core:labels.language'))
    ->setShowLabelText(true)
    ->setShowActiveLabelText(true);

// Create dropdown items for each language
foreach ($siteLanguages as $siteLanguage) {
    $languageId = $siteLanguage->getLanguageId();
    $isActive = $currentLanguageId === $languageId;

    // Build URL for language selection
    $href = $this->uriBuilder->buildUriFromRoute('web_layout', [
        'id' => $pageId,
        'language' => $languageId,
    ]);

    $item = $this->componentFactory->createDropDownRadio()
        ->setHref($href)
        ->setLabel($siteLanguage->getTitle())
        ->setIcon(
            $this->iconFactory->getIcon($siteLanguage->getFlagIdentifier())
        )
        ->setActive($isActive);

    $languageDropdown->addItem($item);
}

$view->getDocHeaderComponent()->setLanguageSelector($languageDropdown);
Copied!

The language selector component is automatically rendered in the top-right area of the DocHeader navigation bar.

Accessibility implementation 

The setShowActiveLabelText(true) method enables display of the active item's label while maintaining accessibility. When enabled with setShowLabelText(true), the button renders the dropdown's label in a visually hidden span followed by the active item's label:

  • Visual users see: "English" (active language)
  • Screen reader users hear: "Language: English" (descriptive label + active language)

When setShowLabelText(false), the accessibility information is provided through aria-label and title attributes instead.

This pattern applies to all dropdown buttons in the DocHeader:

$dropdown->setLabel('Display mode')  // Descriptive label for context
    ->setShowLabelText(true)         // Show label text
    ->setShowActiveLabelText(true);  // Show active item label
Copied!

Impact 

The changes provide a more consistent and intuitive user experience. Users can now create and switch between page translations directly from the DocHeader across all relevant modules, without needing to navigate to specific page areas. The clear separation between existing translations and languages that can be created improves discoverability and reduces confusion about which actions are available.

The language selector and module actions dropdowns now display the currently active selection as the button text, providing immediate visual feedback. Users can see at a glance which language is selected or which module action is active, without needing to open the dropdown.

Accessibility has been significantly improved through the use of descriptive labels rendered as visually hidden elements or aria-label attributes. Screen reader users now receive proper context about each dropdown's purpose (e.g., "Language:", "Display mode:", "Module actions") combined with the current selection, ensuring equal access to navigation functionality.

The module actions dropdown makes it immediately clear which module or action is currently active, and which alternatives are available. The dropdown is only shown when multiple options exist, reducing visual clutter. Removal of redundant back buttons in favor of consistent breadcrumb navigation further reduces visual noise.

Feature: #107889 - Introduce TCA option "itemsProcessors" 

See forge#107889

Description 

As part of centralizing and improving the processing of items for select, check, and radio type fields, a new TCA option itemsProcessors has been introduced as a replacement for itemsProcFunc. This option is an array, allowing any number of processors to be called instead of just one. Processors are ordered by their array key (numerical) and executed in that order. Processors can receive arbitrary data through the $context->processorParameters property.

All processors must implement the ItemsProcessorInterface interface.

Processor methods receive two parameters: a SelectItemCollection instance containing the items, and an ItemsProcessorContext instance providing access to table, field, row data, and configuration. The processor must return a SelectItemCollection. This means that added items can no longer be untyped arrays, making the entire process cleaner and safer.

A new Page TSconfig option is also available, mirroring the existing one for itemsProcFunc. See the example below for syntax.

Example 

The TCA registration might look like this:

EXT:my_extension/Configuration/TCA/my_table.php
 use MyVendor\MyExtension\Processors\SpecialRelationsProcessor;

'relation' => [
    'label' => 'Relational field',
    'config' => [
        'type' => 'select',
        'renderType' => 'selectSingle',
        'items' => [
            [
                'value' => 0,
                'label' => '',
            ],
        ],
        'foreign_table' => 'some_foreign_table',
        'itemsProcessors' => [
            100 => [
                'class' => SpecialRelationsProcessor::class,
                'parameters' => [
                    'foo' => 'bar',
                ],
            ],
            50 => [
                'class' => SpecialRelationsProcessor2::class,
            ],
        ],
    ],
],
Copied!

In this example, SpecialRelationsProcessor2 will be called before SpecialRelationsProcessor.

Here is an example processor:

EXT:my_extension/Classes/Processors/SpecialRelationsProcessor.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Processors;

use TYPO3\CMS\Core\DataHandling\ItemsProcessorContext;
use TYPO3\CMS\Core\DataHandling\ItemsProcessorInterface;
use TYPO3\CMS\Core\Schema\Struct\SelectItem;
use TYPO3\CMS\Core\Schema\Struct\SelectItemCollection;

final class SpecialRelationsProcessor implements ItemsProcessorInterface
{
    public function processItems(
        SelectItemCollection $items,
        ItemsProcessorContext $context,
    ): SelectItemCollection {
        $items->add(
            new SelectItem(
                type: 'select',
                label: 'Extra item',
                value: 42,
            )
        );

        return $items;
    }
}
Copied!

The $context->processorParameters property contains any parameters defined in the TCA declaration.

Using Page TSconfig to pass custom parameters to the processor would look like this:

TCEFORM.example_table.content.itemsProcessors.100.foo = bar
Copied!

Note that the numerical key of the processor must be reused. With this setup, the class SpecialRelationsProcessor receives the PHP array ['foo' => 'bar'] in the $context->fieldTSconfig property. The class SpecialRelationsProcessor2 receives an empty array [] (since it is registered with the key 50).

Registration of processors is also possible within FlexForms:

EXT:my_package/Configuration/FlexForms/SomeForm.xml
<some_selector>
    <label>Choice</label>
    <config>
        <type>select</type>
        <renderType>selectSingle</renderType>
        <itemsProcessors>
            <numIndex index="100">
                <class>MyVendor\MyPackage\Processors\SpecialRelationsProcessor</class>
            </numIndex>
        </itemsProcessors>
    </config>
</some_selector>
Copied!

Impact 

It is still possible to use itemsProcFunc, but switching to itemsProcessors is recommended because it offers two main advantages:

  • Being an array, it allows extensions to add processors on top of existing ones and to define their execution order.
  • The processing chain is strictly typed, ensuring safer and more reliable code.

If both itemsProcFunc and itemsProcessors are defined, both are executed, with itemsProcFunc executed first.

Feature: #107953 - Add "Current logged-in users" filter to the "Backend Users" module 

See forge#107953

Description 

A new filter option Current logged-in users has been added to the Administration > Users module. This feature allows administrators to quickly list all backend accounts that are currently active.

The detection mechanism is based on the lastlogin timestamp in combination with the global configuration value $GLOBALS['TYPO3_CONF_VARS']['BE']['sessionTimeout'] , which defines the maximum lifetime of backend user sessions.

Impact 

Administrators can now filter backend users by those who are currently logged in, providing an immediate overview of active sessions directly within the Administration > Users module.

Feature: #108002 - Introduce built-in symmetric encryption/decryption cipher service 

See forge#108002

Description 

TYPO3 now provides a built-in symmetric encryption and decryption service using the modern XChaCha20-Poly1305 AEAD (Authenticated Encryption with Associated Data) cipher. This service allows extensions and core code to securely encrypt sensitive data such as API tokens, passwords, or personal information without implementing custom encryption solutions or relying on third-party libraries.

Benefits 

The new \TYPO3\CMS\Core\Crypto\Cipher\CipherService provides several advantages:

  • Secure by default: Uses modern, cryptographically secure algorithms provided by the libsodium library (ext-sodium), which is available by default in all PHP versions currently supported by TYPO3.
  • Seamless integration: Secret keys can be derived from TYPO3's existing $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] , eliminating the need to manage additional encryption keys.
  • Authenticated encryption: The AEAD cipher not only encrypts data but also ensures its integrity, preventing tampering.
  • Application-specific keys: Different components can derive isolated encryption keys using seed values, enhancing security through key separation.

Key Derivation 

Secret keys are derived from the existing TYPO3 encryption key using the \TYPO3\CMS\Core\Crypto\Cipher\KeyFactory . This approach leverages the master encryption key that is already configured in your TYPO3 installation, ensuring that different applications or contexts can have their own isolated keys while using the same master key.

Please note that to decrypt data in the future, the encryptionKey must not be changed and must remain available.

Example for deriving a secret key:

use TYPO3\CMS\Core\Crypto\Cipher\KeyFactory;

// Get the KeyFactory via dependency injection
public function __construct(
    private readonly KeyFactory $keyFactory
) {}

// Derive an application-specific secret key from the TYPO3 encryption key
// The seed 'my-extension-tokens' identifies this specific use case
$secretKey = $this->keyFactory->deriveSharedKeyFromEncryptionKey('my-extension-tokens');

// You can also derive multiple sub-keys from the same seed
$secretKey1 = $this->keyFactory->deriveSharedKeyFromEncryptionKey('my-extension-tokens', 1);
$secretKey2 = $this->keyFactory->deriveSharedKeyFromEncryptionKey('my-extension-tokens', 2);
Copied!

Each unique seed produces a different encryption key, allowing for key separation between different features or data types within your extension.

Key Usage/Generation 

Instead of deriving a key, it is also possible to generate a new key from a random value or to use a provided key (e.g., via environment variables).

  • $this->keyFactory->createSharedKeyFromString(getenv('MY_APP_KEY'))
  • $this->keyFactory->generateSharedKey()

Encryption Example 

To encrypt sensitive data, use the CipherService::encrypt() method:

use TYPO3\CMS\Core\Crypto\Cipher\CipherService;
use TYPO3\CMS\Core\Crypto\Cipher\KeyFactory;

// Get services via dependency injection
public function __construct(
    private readonly CipherService $cipherService,
    private readonly KeyFactory $keyFactory
) {}

public function encryptApiToken(string $apiToken): string
{
    // Derive a secret key for API token encryption
    $secretKey = $this->keyFactory->deriveSharedKeyFromEncryptionKey('api-tokens');

    // Encrypt the API token
    $cipherValue = $this->cipherService->encrypt($apiToken, $secretKey);

    // Convert to string for storage in database
    // The result has the format: {base64url-encoded-json}
    // Example: "eyJub25jZSI6IlxcXHUwMDEy4oCmIiwiY2lwaGVyIjoi4oCmIn0"
    // This string contains both the nonce and ciphertext in a JSON structure
    $encryptedString = (string)$cipherValue;

    return $encryptedString;
}
Copied!

Each encryption generates a unique result due to the random nonce, even when encrypting the same plaintext multiple times.

Decryption Example 

To decrypt data, use the CipherService::decrypt() method with the same secret key that was used for encryption:

use TYPO3\CMS\Core\Crypto\Cipher\CipherService;
use TYPO3\CMS\Core\Crypto\Cipher\CipherValue;
use TYPO3\CMS\Core\Crypto\Cipher\KeyFactory;
use TYPO3\CMS\Core\Crypto\Cipher\CipherDecryptionFailedException;

// Get services via dependency injection
public function __construct(
    private readonly CipherService $cipherService,
    private readonly KeyFactory $keyFactory
) {}

public function decryptApiToken(string $encryptedToken): string
{
    // Derive the same secret key used during encryption
    $secretKey = $this->keyFactory->deriveSharedKeyFromEncryptionKey('api-tokens');

    // Parse the cipher text (format: {base64url-encoded-json})
    $cipherValue = CipherValue::fromSerialized($encryptedToken);

    try {
        // Decrypt the token
        $apiToken = $this->cipherService->decrypt($cipherValue, $secretKey);
        return $apiToken;
    } catch (CipherDecryptionFailedException $e) {
        // Decryption failed - wrong key, tampered data, or invalid format
        throw new \RuntimeException(
            'Failed to decrypt API token: ' . $e->getMessage(),
            1762465682,
            $e
        );
    }
}
Copied!

Decryption will fail if the wrong key is used, the data has been tampered with, or the ciphertext format is invalid.

Impact 

The CipherService and KeyFactory are now available as autoconfigured services throughout TYPO3 core and extensions. This provides a standardized, secure way to encrypt and decrypt sensitive data without requiring additional dependencies beyond ext-sodium, which is already a requirement of TYPO3 core.

Extensions can now securely store encrypted data in the database or configuration files using this built-in service, ensuring consistent security practices across the TYPO3 ecosystem.

Feature: #108008 - Automatic reload and shortcut buttons in backend modules 

See forge#108008

Description 

Backend modules now automatically get reload and shortcut buttons added to their document header, ensuring consistent display across all backend modules.

Previously, controllers manually added these buttons with varying group numbers, leading to inconsistent positioning. Now, buttons always appear on the right side, ensuring they are always the last two buttons regardless of what other buttons are added.

Controllers provide shortcut information using the new DocHeaderComponent::setShortcutContext() method:

use TYPO3\CMS\Backend\Template\Components\DocHeaderComponent;

$view->getDocHeaderComponent()->setShortcutContext(
    routeIdentifier: 'site_configuration.edit',
    displayName: sprintf('Edit site: %s', $siteIdentifier),
    arguments: ['site' => $siteIdentifier]
);
Copied!

Buttons are automatically added during rendering before the PSR-14 ModifyButtonBarEvent is dispatched, allowing event listeners to modify or remove them if needed.

Controllers can disable automatic buttons if custom behavior is required:

use TYPO3\CMS\Backend\Template\Components\DocHeaderComponent;

$view->getDocHeaderComponent()->disableAutomaticReloadButton();
$view->getDocHeaderComponent()->disableAutomaticShortcutButton();
Copied!

Impact 

Reload and shortcut buttons now appear consistently at the same position across all backend modules, providing a predictable user experience.

Controllers no longer need to manually create these buttons, reducing boilerplate code. Use DocHeaderComponent::setShortcutContext() to provide shortcut information and remove manual button creation, see Deprecation: #108008 - Manual shortcut button creation.

Before:

use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
use TYPO3\CMS\Backend\Template\Components\ButtonBar;

$reloadButton = $this->componentFactory->createReloadButton($uri);
$view->addButtonToButtonBar(
    $reloadButton,
    ButtonBar::BUTTON_POSITION_RIGHT,
    3
);

$shortcutButton = $this->componentFactory->createShortcutButton()
    ->setRouteIdentifier('my_module')
    ->setDisplayName('My Module')
    ->setArguments(['id' => $pageId]);
$view->addButtonToButtonBar($shortcutButton);
Copied!

After:

use TYPO3\CMS\Backend\Template\Components\DocHeaderComponent;

// Set shortcut context only
$view->getDocHeaderComponent()->setShortcutContext(
    routeIdentifier: 'my_module',
    displayName: 'My Module',
    arguments: ['id' => $pageId]
);
Copied!

Feature: #108016 - Multi-language selection in page and list modules 

See forge#108016

Description 

The Content > Layout and Content > List modules now support selecting multiple languages simultaneously for improved comparison of translated records.

The language selection state is synchronized between both modules, providing a consistent experience when switching between page and list views.

A fallback mechanism is in place for switching between view modes (Layout and Language Comparison) as well as navigating to a page not available in the current language selection.

Impact 

Users can now:

  • Select multiple specific languages to compare
  • Toggle individual languages on / off
  • Quickly select or deselect all available languages
  • See visual feedback with appropriate icons and counts

The language selection is persisted in module data and shared between the page module and list module for the same page.

Feature: #108027 - Type-specific title in TCA types section 

See forge#108027

Description 

It is now possible to define a type-specific title in the TCA types section. This value overrides the global table title defined in ctrl['title'] for the respective record type.

This allows different record types of the same table to display different titles in the TYPO3 backend user interface, making it clearer which kind of record is being created or displayed.

Implementation 

This feature has been implemented in two areas to ensure consistent behavior:

  1. Schema API The TcaSchemaFactory merges type-specific configuration into sub-schemas using array_replace_recursive(). This makes type-specific titles available through $schema->getTitle().
  2. FormEngine The data provider TcaTypesCtrlOverrides merges the type-specific title and previewRenderer into processedTca['ctrl'] during form rendering. This ensures that both FormEngine and any legacy code accessing ctrl directly will see the correct type-specific values.

Example 

EXT:my_extension/Configuration/TCA/tx_my_table.php
return [
    'ctrl' => [
        'title' => 'my_extension.db:tx_my_table',
        'type' => 'record_type',
        // ... other ctrl configuration
    ],
    'types' => [
        'article' => [
            'title' => 'my_extension.db:tx_my_table.type.article',
            'showitem' => 'title, content, author',
        ],
        'news' => [
            'title' => 'my_extension.db:tx_my_table.type.news',
            'showitem' => 'title, content, publish_date',
        ],
        'event' => [
            // No type-specific title - will use the global ctrl['title']
            'showitem' => 'title, content, event_date',
        ],
    ],
    // ... columns configuration
];
Copied!

In this example:

  • The article type displays the title defined in the language file key tx_my_table.type.article.
  • The news type displays the title defined in tx_my_table.type.news.
  • The event type falls back to the global table title from ctrl['title'].

Impact 

Tables with multiple record types can now define more specific and descriptive titles for each type in the backend user interface. This improves usability and clarity for editors by making it immediately obvious which type of record is being created or edited.

This feature is especially useful for:

  • Content element tables such as tt_content with different CType values
  • Tables with distinct record types serving different purposes
  • Plugin records with varying functionality per type
  • Any table where the record type changes the record's purpose or meaning

Feature: #108049 - Modernized translation workflow 

See forge#108049

Description 

A modernized and extensible translation workflow has been introduced in the TYPO3 backend. The new architecture replaces the previous monolithic localization implementation with a step-based wizard system that guides editors through the translation process. Previously available only in the Content > Layout module, the translation wizard is now the default interface for all translation operations across backend modules.

The backend now consistently opens the new translation wizard whenever users initiate a translation operation, providing a unified experience across all modules. The wizard guides users through the translation process in multiple steps, automatically advancing when no user input is required. This streamlined approach ensures that users only interact with steps requiring configuration or confirmation.

Users can choose between two translation modes: Translate (creates a connected translation) and Copy (creates an independent copy in free mode). To maintain consistency, the wizard only offers the translation mode that matches existing translations for a record, preventing mixed modes.

API for extension developers 

The new LocalizationHandlerRegistry provides a flexible architecture for registering and managing different translation strategies. Handlers implement the LocalizationHandlerInterface and are automatically registered via autoconfiguration with the backend.localization.handler tag. This makes the localization system extensible, allowing custom handlers to be added for specialized translation workflows, such as integration with translation services, AI-powered translation, or custom business logic.

The LocalizationFinisherInterface defines how the wizard completes after a successful translation operation. Finishers can be customized by localization handlers to provide the most appropriate completion behavior for their workflow, such as redirecting to a specific page or reloading the current view.

Custom localization handlers 

Extensions can provide custom localization handlers by implementing the LocalizationHandlerInterface :

EXT:my_extension/Classes/Localization/MyCustomHandler.php
namespace MyVendor\MyExtension\Localization;

use TYPO3\CMS\Backend\Localization\LocalizationHandlerInterface;
use TYPO3\CMS\Backend\Localization\LocalizationInstructions;
use TYPO3\CMS\Backend\Localization\LocalizationMode;
use TYPO3\CMS\Backend\Localization\LocalizationResult;

final class MyCustomHandler implements LocalizationHandlerInterface
{
    public function getIdentifier(): string
    {
        return 'my-custom-handler';
    }

    public function getLabel(): string
    {
        return 'my_extension.messages:handler.label';
    }

    public function getDescription(): string
    {
        return 'my_extension.messages:handler.description';
    }

    public function getIconIdentifier(): string
    {
        return 'my-extension-icon';
    }

    public function isAvailable(LocalizationInstructions $instructions): bool
    {
        // Return true if this handler should be available for the given context
        return $instructions->mainRecordType === 'my_table';
    }

    public function processLocalization(
        LocalizationInstructions $instructions
    ): LocalizationResult {
        // Implement custom localization logic
        // Return a LocalizationResult with success status and finisher
    }
}
Copied!

The handler will be automatically registered via autoconfiguration when it implements LocalizationHandlerInterface . No manual service configuration is required.

Custom finishers 

Extensions can create custom finishers by implementing the LocalizationFinisherInterface and providing a corresponding JavaScript module to handle the frontend logic:

EXT:my_extension/Classes/Localization/Finisher/MyCustomFinisher.php
namespace MyVendor\MyExtension\Localization\Finisher;

use TYPO3\CMS\Backend\Localization\Finisher\LocalizationFinisherInterface;

final class MyCustomFinisher implements LocalizationFinisherInterface
{
    public function getType(): string
    {
        return 'my-custom-finisher';
    }

    public function getModule(): string
    {
        return '@vendor/my-extension/localization/finisher/my-custom-finisher.js';
    }

    public function getData(): array
    {
        return [
            'customData' => 'value',
        ];
    }

    public function getLabels(): array
    {
        return [
            'successMessage' => 'my_extension.messages:finisher.success',
        ];
    }

    public function jsonSerialize(): array
    {
        return [
            'type' => $this->getType(),
            'module' => $this->getModule(),
            'data' => $this->getData(),
            'labels' => $this->getLabels(),
        ];
    }
}
Copied!

The corresponding JavaScript module implements the finisher logic. The finisher is executed in the final step of the wizard and can define custom rendering and behavior. Users can skip the finisher step if no interaction is required.

EXT:my_extension/Resources/Public/JavaScript/localization/finisher/my-custom-finisher.js
import LocalizationFinisher from '@typo3/backend/localization/localization-finisher';
import { html, type TemplateResult } from 'lit';

class MyCustomFinisher extends LocalizationFinisher {
  public render(): TemplateResult {
    // Define what is rendered in the finisher step
    // Can return custom UI elements, messages, actions, etc.
    return html`
      <p>${this.finisher.labels.successMessage}</p>
      <button @click=${() => this.execute()}>
        Perform Action
      </button>
    `;
  }

  public async execute(): Promise<void> {
    // Execute finisher logic (e.g., redirect, reload, show notification)
    // This is called when the wizard completes or when the user clicks
    // the custom action button above
    const customData = this.finisher.data.customData;
    console.log('Finisher executing with data:', customData);

    // Perform your custom finisher action here
    // For example: redirect, reload, show notification, etc.
  }
}

export default MyCustomFinisher;
Copied!

Impact 

The new translation workflow provides a consistent and intuitive user experience across all backend modules. Editors benefit from clear step-by-step guidance through the translation process, with the wizard automatically adapting to the context and showing only relevant options.

Extension developers can register custom localization handlers to integrate specialized translation workflows, such as connections to external translation services or AI-powered translation tools.

Feature: #108148 - Alternative Fluid syntax for CDATA sections 

See forge#108148

Description 

A long-standing issue in Fluid templates has been that the Fluid variable and inline ViewHelper syntax collides with inlined CSS or JavaScript code. This issue has now been addressed with Fluid 5: A new alternative syntax has been introduced that makes collisions between CSS/JavaScript and Fluid far less likely.

The normal inline and variable syntax uses single curly braces { } as tokens in Fluid templates. In <![CDATA[ ]]> sections, this syntax is now ignored. Instead, three curly braces {{{ }}} can be used to call Fluid ViewHelpers or to access variables. The tag-based syntax is disabled altogether in CDATA sections.

Example:

<f:variable name="color" value="red" />
<style>
<![CDATA[
    @media (min-width: 1000px) {
        p {
            background-color: {{{color}}};
        }
    }
]]>
</style>
Copied!

The Fluid Explained documentation contains several practical examples of how this new feature can be used: Avoiding syntax collision with JS and CSS.

Note that it's still considered bad practice to put inline CSS or JavaScript code in Fluid templates. Consider using a dedicated API endpoint, data-* attributes or CSS custom properties (also known as CSS variables) to pass dynamic values to JavaScript and CSS.

Impact 

Inline CSS and JavaScript can now be wrapped in CDATA in Fluid templates, which prevents syntax collision with Fluid's inline syntax.

CDATA sections are thus no longer removed from Fluid templates before rendering, see Breaking: #108148 - CDATA Sections No Longer Removed.

Feature: #108148 - Union types for ViewHelpers 

See forge#108148

Description 

Fluid 5 brings support for union types in ViewHelper argument definitions. Previously, it was necessary to specify an argument as mixed if more than one type should be possible. Now it is possible to specify multiple types separated by a pipe character (|).

The following built-in PHP types can also be used:

  • iterable
  • countable
  • callable

Example:

use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\FileReference;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;

class MyViewHelper extends AbstractViewHelper
{
    public function initializeArguments(): void
    {
        $this->registerArgument(
            'file',
            File::class . '|' . FileReference::class,
            'a file object'
        );
        $this->registerArgument(
            'items',
            'iterable',
            'a list of items'
        );
    }
}
Copied!

This feature also applies to the Argument ViewHelper <f:argument>:

<f:argument name="file" type="TYPO3\CMS\Core\Resource\File|TYPO3\CMS\Core\Resource\FileReference" />
Copied!

Note that union types disable automatic type conversion by Fluid, so it might be necessary to specify more types to keep ViewHelpers flexible. Example:

$this->registerArgument(
    'ids',
    'array|string|int',
    'a list of ids, either comma-separated or as array'
);
Copied!

Impact 

Custom ViewHelper implementations have more options to specify an API with strict type requirements and can avoid manual type checks.

Feature: #108166 - Fluid file extension and template resolving 

See forge#108166

Description 

Fluid 5 introduces a specific file extension for template files. The extension is used in combination with a generic extension, which means that existing syntax highlighting in code editors still works.

Before After
MyTemplate.html MyTemplate.fluid.html
MyTemplate.xml MyTemplate.fluid.xml
... ...

As stated in the Fluid release notes, this new file extension is entirely optional. Existing template files will continue to work. A fallback mechanism is in place, which checks for template files in the following order:

templateRootPath: templates/
template: myTemplate
format: html

1. templates/myTemplate.fluid.html
2. templates/myTemplate.html
3. templates/myTemplate
4. templates/MyTemplate.fluid.html
5. templates/MyTemplate.html
6. templates/MyTemplate
Copied!

This means that *.fluid.* files are preferred over files without the new extension if both files exist in the same folder.

Another noteworthy change in Fluid is that template files no longer need to start with an uppercase character: The user-provided spelling of the template name will be tried first, the uppercase variant will be used as a fallback.

Consequences for template overloading 

If multiple template paths are provided (for example if an extension overloads templates or partials of another extension), this new fallback chain for template file names will be executed per template path. In practice this means the following:

  • A TYPO3 community extension can ship *.html files (without the new file extension), which can be overloaded by a sitepackage extension that already uses *.fluid.html files.
  • A TYPO3 community extension can ship *.fluid.html files, which can still be overloaded by a sitepackage extension that uses *.html files.
  • MyTemplate.html will still overload MyTemplate.html like before, and the same applies to *.fluid.html files.

However, since older Fluid versions do not consider *.fluid.* files, it is not supported to use the new file extension in a TYPO3 community extension that still supports TYPO3 versions below 14.

For the TYPO3 Core this means that all template files can safely be renamed to the new file extension because Fluid 5 is always present. To overload templates from the TYPO3 Core, both the new Fluid file extension and the old file extension can be used, which allows TYPO3 community extensions to remain compatible with multiple TYPO3 Core versions.

Edge case: Templates with file extension specified 

Note that the described file extension fallback chain only works if the file extension is not specified explicitly, but rather derived from the template's format. If the file extension is part of the requested template name, Fluid can't reliably add the *.fluid.* file extension automatically and the template needs to be adjusted.

One use case of this would be a template in format json that calls a partial in format html:

MyTemplate.fluid.json
<!-- Won't work if template file is called MyTemplate.fluid.html: -->
<f:render partial="MyTemplate.html" />

<!-- Needs to be adjusted like this: -->
<f:render partial="MyTemplate.fluid.html" />
Copied!

Impact 

While the TYPO3 Core can already switch to the new *.fluid.* file extension, TYPO3 community extensions will probably continue to use *.html for an extended period. However, on a v14 project, the new file extension can already be used, both for individual development and for the integration of TYPO3 community extensions.

Projects and extension authors willing to switch to the new file extension can use the fluid-rename utility extension, which has already been used for the TYPO3 Core.

This new file extension opens new possibilities because it is now easily recognizable which files will be interpreted by Fluid. This will enable better IDE integration and tooling support in the future.

Feature: #108227 - Allow #[IgnoreValidation] and #[Validate] attributes for method parameters 

See forge#108227

Description 

The Extbase attributes #[IgnoreValidation] and #[Validate] can now be used for controller action method parameters.

This extends the current validation behavior of these attributes, where either (1) the complete method or (2) a single parameter is taken into account for validation. While this works fine for (1), using these attributes in context of (2) raises some concerns regarding

  1. duplication of parameter names,
  2. validation of the existence of a configured parameter, and
  3. unnecessary complexity regarding reflection-based handling of these parameters.

History 

When the attributes were originally implemented as Doctrine annotations, the only possible way to implement behavior (2) was to add the appropriate annotation to the method-related annotation.

Since forge#107229, annotations are no longer used in Extbase, and PHP attributes are the only remaining successor in this area. Since PHP attributes support placement at specific method parameters, the existing attributes can safely rewritten to be placed (1) at a specific method or (2) at a specific method parameter.

Usage with method parameters 

The capabilities of the existing attributes are expanded to allow placement at method parameters as well. The previous behavior (defining validation-related behavior at method level using the existing attribute properties) is now deprecated and will be removed with TYPO3 v15 (see deprecation notice).

Impact 

By allowing the usage of both #[IgnoreValidation] and #[Validate] attributes at method parameter level, the previous error-prone behavior is now hardened. In addition, this change improves developer experience and pushes the Extbase ecosystem towards a modernized architecture.

All installations using these attributes on method level need to migrate these as described in the related section of Deprecation #108227 - Migration.

Feature: #108229 - Fluid cache warmup 

See forge#108229

Description 

TYPO3 v14 leverages the revamped Fluid v5 warmup feature and integrates a Fluid template warmup directly into the CLI command typo3 cache:warmup.

The command finds and compiles all *.fluid.* (for example Index.fluid.html) files found in extensions.

Fluid warmup can also be called directly using typo3 fluid:cache:warmup, which will additionally output compile time deprecations found within Fluid template files.

Verbose output allows to get feedback of warmed up templates and the number of errors/deprecations (or success).

Impact 

The warmup command can be useful to reduce ramp up time after deployments.

Feature: #108240 - Introduce Fresh theme 

See forge#108240

Description 

The new Fresh theme has been introduced for the TYPO3 backend, marking the beginning of broader backend customization capabilities. This theme offers users a modern alternative appearance option with a friendlier purple accent color palette.

Built on the modern CSS architecture, the "Fresh" theme is now the default for new users. It complements the existing Modern and Classic themes. All three themes will be maintained and enhanced equally going forward.

The extensive CSS framework improvements made during the 14.0 cycle now enable easier theme adaptation with minimal code changes, setting the foundation for the expanding theme system.

Theme selection 

Users can select their preferred theme in the User Settings module under the Backend appearance section. The "Fresh" theme provides a more contemporary look and feel while maintaining the familiar TYPO3 backend structure and usability.

All existing themes remain fully supported:

  • Fresh (default for new users) - Modern purple accent colors
  • Modern - Contemporary appearance with traditional accent colors
  • Classic - Traditional TYPO3 backend appearance

Future development 

Additional customization options and modern user interface elements will be incrementally added to the theme system leading up to the long term support release. The modular CSS architecture ensures that all themes benefit from these enhancements equally.

Impact 

The introduction of the "Fresh" theme demonstrates TYPO3's commitment to providing a modern, customizable backend experience. The modular CSS architecture allows for continuous improvement and evolution of the backend design system.

New TYPO3 installations will automatically use the "Fresh" theme, while existing users can opt in through their User Settings. The choice of theme is stored per user, allowing teams with different preferences to work comfortably within the same TYPO3 installation.

Deprecation: #93981 - GraphicalFunctions->gif_or_jpg 

See forge#93981

Description 

Default image formats can now be configured thanks to forge#93981 (see Feature: #93981 - Specify default image conversion processing).

As a result, the method gif_or_jpg() of \TYPO3\CMS\Core\Imaging\GraphicalFunctions is no longer needed.

Fallback behavior for image preview generation now follows the configured file extensions rather than a hardcoded GIF/JPEG switch.

A new method, GraphicalFunctions->determineDefaultProcessingFileExtension(), has been introduced.

It accepts a file extension such as 'pdf' as an argument and returns the corresponding default output extension for preview generation. This method is currently marked as @internal, as it may later be moved to a dedicated service class.

In general, third-party extensions should not determine the output format manually but rely on TYPO3’s built-in image generation APIs.

Deprecated method:

  • \TYPO3\CMS\Core\Imaging\GraphicalFunctions->gif_or_jpg()

Impact 

Calling this method will trigger a deprecation-level log entry and will stop working in TYPO3 v15.0.

Affected installations 

Instances that directly use the deprecated method.

Migration 

use TYPO3\CMS\Core\Imaging\GraphicalFunctions;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$graphicalFunctions = GeneralUtility::makeInstance(GraphicalFunctions::class);

// Before
$filetype = $graphicalFunctions->gif_or_jpg('pdf', 800, 600);
// Returned: 'jpg'

// After
$filetype = $graphicalFunctions->determineDefaultProcessingFileExtension('pdf');
// Returns: 'jpg' (for example, now depends on configuration!)
Copied!

This is a temporary migration using an @internal method, which is subject to change. Code like the above should generally be avoided in third-party extensions.

Instead, use GraphicalFunctions->resize() and specify the argument $targetFileExtension = 'web' so that actual operations use the configured target formats.

Deprecation: #97559 - Deprecate passing an array of configuration values to Extbase attributes 

See forge#97559

Description 

Passing an array of configuration values to Extbase attributes has been deprecated. All configuration values should now be passed as single properties using constructor property promotion. When an array of configuration values is passed for the first available property in an attribute, a deprecation notice will be triggered. The possibility to pass such an array will be removed with TYPO3 v15.

Impact 

The usage of constructor property promotion as an alternative to an array of configuration values enables type safety and value hardening and moves Extbase attributes toward a modern configuration element for models, Data Transfer Objects, and controller actions.

Affected installations 

All installations that make use of Extbase attribute configuration are affected, since this was previously only possible by passing an array of configuration values.

Migration 

Use the available attribute properties instead of an array.

Before: 

use TYPO3\CMS\Extbase\Attribute\FileUpload;
use TYPO3\CMS\Extbase\Attribute\Validate;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Domain\Model\FileReference;

class MyModel extends AbstractEntity
{
    #[Validate(['validator' => 'NotEmpty'])]
    protected string $foo = '';

    #[FileUpload([
        'validation' => [
            'required' => true,
            'maxFiles' => 1,
            'fileSize' => ['minimum' => '0K', 'maximum' => '2M'],
            'allowedMimeTypes' => ['image/jpeg', 'image/png'],
        ],
        'uploadFolder' => '1:/user_upload/files/',
    ])]
    protected ?FileReference $bar = null;
}
Copied!

After: 

use TYPO3\CMS\Extbase\Attribute\FileUpload;
use TYPO3\CMS\Extbase\Attribute\Validate;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Domain\Model\FileReference;

class MyModel extends AbstractEntity
{
    #[Validate(validator: 'NotEmpty')]
    protected string $foo = '';

    #[FileUpload(
        validation: [
            'required' => true,
            'maxFiles' => 1,
            'fileSize' => ['minimum' => '0K', 'maximum' => '2M'],
            'allowedMimeTypes' => ['image/jpeg', 'image/png'],
        ],
        uploadFolder: '1:/user_upload/files/',
    )]
    protected ?FileReference $bar = null;
}
Copied!

Combined diff: 

 class MyModel extends AbstractEntity
 {
-    #[Validate(['validator' => 'NotEmpty'])]
+    #[Validate(validator: 'NotEmpty')]
     protected string $foo = '';

-    #[FileUpload([
-        'validation' => [
+    #[FileUpload(
+        validation: [
             'required' => true,
             'maxFiles' => 1,
             'fileSize' => ['minimum' => '0K', 'maximum' => '2M'],
             'allowedMimeTypes' => ['image/jpeg', 'image/png'],
         ],
-        'uploadFolder' => '1:/user_upload/files/',
-    ])]
+        uploadFolder: '1:/user_upload/files/',
+    )]
     protected ?FileReference $bar = null;
 }
Copied!

Deprecation: #97857 - Deprecate __inheritances operator in form configuration 

See forge#97857

Description 

The custom __inheritances operator, which was available only in YAML configuration files of EXT:form, has been deprecated.

Previously, this operator was used within form definition files to inherit and reuse configuration parts between form element definitions. With native YAML functionality now providing equivalent and more flexible features, this TYPO3-specific operator is no longer necessary.

Developers are encouraged to migrate to standard YAML features such as anchors, aliases, and overrides to avoid code duplication and to simplify form configuration maintenance.

Impact 

Using the __inheritances operator inside a custom YAML form configuration in EXT:form will trigger a PHP E_USER_DEPRECATED error.

Affected installations 

All installations with custom form definitions or form element configurations that use the __inheritances operator in their EXT:form YAML files are affected and need to update those files accordingly.

Migration 

The custom TYPO3 implementation using __inheritances can be replaced with standard YAML syntax.

Developers can achieve the same result by using anchors ( &), aliases ( *), and overrides ( <<:).

Before:

mixins:
  formElementMixins:
    BaseFormElementMixin:
      1761226183:
        identifier: custom
        templateName: Inspector-TextEditor
        label: Custom editor
        propertyPath: custom
    OtherBaseFormElementMixin:
      1761226184:
        identifier: otherCustom
        templateName: Inspector-TextEditor
        label: Other custom editor
        propertyPath: otherCustom

prototypes:
  standard:
    formElementsDefinition:
      Text:
        formEditor:
          editors:
            __inheritances:
              10: 'mixins.formElementMixins.BaseFormElementMixin'
              20: 'mixins.formElementMixins.OtherBaseFormElementMixin'
Copied!

After:

customEditor: &customEditor
  1761226183:
    identifier: custom
    templateName: Inspector-TextEditor
    label: Custom editor
    propertyPath: custom

otherCustomEditor: &otherCustomEditor
  identifier: otherCustom
  templateName: Inspector-TextEditor
  label: Other custom editor
  propertyPath: otherCustom

prototypes:
  standard:
    formElementsDefinition:
      Text:
        formEditor:
          editors:
            <<: *customEditor
            1761226184: *otherCustomEditor
Copied!

Deprecation: #98453 - Scheduler task registration via SC_OPTIONS 

See forge#98453

Description 

The registration of scheduler tasks via $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'] has been deprecated in favor of the new native scheduler task feature using TCA.

Previously, scheduler tasks were registered in ext_localconf.php using the following syntax:

$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][MyTask::class] = [
    'extension' => 'my_extension',
    'title' => 'my_extension.messages:myTask.title',
    'description' => 'my_extension.messages:myTask.description',
    'additionalFields' => MyTaskAdditionalFieldProvider::class,
];
Copied!

This approach required a separate AdditionalFieldProviderInterface implementation to handle custom task fields. The AdditionalFieldProvider was responsible for:

  • Rendering form fields in the scheduler module.
  • Validating field input.
  • Saving and loading field values.

The new approach replaces this with native TCA configuration, providing:

  • Better integration with TYPO3's FormEngine.
  • Automatic validation through TCA field configuration.
  • Enhanced security through FormEngine's XSS protection.
  • Consistency with other TYPO3 backend forms.
  • Access to all TCA field types and rendering options.

In addition, the class AbstractAdditionalFieldProvider and the interface AdditionalFieldProviderInterface have been deprecated.

Impact 

Using $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'] for registering scheduler tasks will stop working in TYPO3 v15.0.

Custom task classes implementing AdditionalFieldProviderInterface should remove this interface implementation. The interface methods ( getAdditionalFields(), validateAdditionalFields(), saveAdditionalFields()) are no longer needed with the new TCA-based approach.

Affected installations 

All installations with custom scheduler tasks registered via $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'] and using AdditionalFieldProviderInterface .

Migration 

Scheduler tasks should now be registered as native task types using TCA. This provides a more integrated and maintainable approach to task configuration.

Migration steps:

  1. Remove the registration from ext_localconf.php.
  2. Create a TCA override file in Configuration/TCA/Overrides/scheduler_my_task_type.php.
  3. Update your task class to implement the new parameter methods.
  4. Remove the \AdditionalFieldProvider class if it exists.

Example migration 

Before:

ext_localconf.php
use MyVendor\MyExtension\Task\MyTask;
use MyVendor\MyExtension\Task\MyTaskAdditionalFieldProvider;

$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][MyTask::class] = [
    'extension' => 'my_extension',
    'title' => 'my_extension.messages:myTask.title',
    'description' => 'my_extension.messages:myTask.description',
    'additionalFields' => MyTaskAdditionalFieldProvider::class,
];
Copied!

After:

Configuration/TCA/Overrides/scheduler_my_task_type.php
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

defined('TYPO3') or die();

if (isset($GLOBALS['TCA']['tx_scheduler_task'])) {
    // Add custom fields to the tx_scheduler_task table
    ExtensionManagementUtility::addTCAcolumns(
        'tx_scheduler_task',
        [
            'my_extension_field' => [
                'label' => 'my_extension.messages:field.label',
                'config' => [
                    'type' => 'input',
                    'size' => 30,
                    'required' => true,
                    'eval' => 'trim', // FormEngine validation replaces custom validation
                    'placeholder' => 'Enter value here...',
                ],
            ],
            'my_extension_email_list' => [
                'label' => 'my_extension.messages:emailList.label',
                'config' => [
                    'type' => 'text',
                    'rows' => 3,
                    'required' => true, // 'required' validation handled by FormEngine
                    'placeholder' => 'admin@example.com',
                ],
            ],
        ]
    );

    // Register the task type
    ExtensionManagementUtility::addRecordType(
        [
            'label' => 'my_extension.messages:my_task.title',
            'description' => 'my_extension.messages:my_task.description',
            'value' => MyTask::class,
            'icon' => 'mimetypes-x-tx_scheduler_task_group',
            'group' => 'my_extension',
        ],
        '
        --div--;core.tabs:general,
            tasktype,
            task_group,
            description,
            my_extension_field,
            my_extension_email_list,
        --div--;scheduler.messages:scheduler.form.palettes.timing,
            execution_details,
            nextexecution,
            --palette--;;lastexecution,
        --div--;core.tabs:access,
            disable,
        --div--;core.tabs:extended,',
        [],
        '',
        'tx_scheduler_task'
    );
}
Copied!

Update your (existing) task class to implement the new methods:

EXT:my_extension/Classes/Task/MyTask.php
namespace MyVendor\MyExtension\Task;

use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Scheduler\Task\AbstractTask;

class MyTask extends AbstractTask
{
    protected string $myField = '';
    protected string $emailList = '';

    public function execute(): bool
    {
        // Your task logic here using $this->myField and $this->emailList
        return true;
    }

    /**
     * Return current field values as an associative array.
     * This method is called during migration from old serialized tasks
     * and when displaying task information.
     */
    public function getTaskParameters(): array
    {
        return [
            'my_extension_field' => $this->myField,
            'my_extension_email_list' => $this->emailList,
        ];
    }

    /**
     * Set field values from an associative array.
     * This method handles both old and new parameter formats for migration.
     *
     * @param array $parameters Values from either old AdditionalFieldProvider or new TCA fields.
     */
    public function setTaskParameters(array $parameters): void
    {
        // Handle migration: check old parameter names first, then new TCA field names
        $this->myField = $parameters['myField'] ?? $parameters['my_extension_field'] ?? '';
        $this->emailList = $parameters['emailList'] ?? $parameters['my_extension_email_list'] ?? '';
    }

    /**
     * Validate task parameters.
     * Only implement this method for validation that cannot be handled by FormEngine.
     * Basic validation like 'required' should be done via TCA 'eval' configuration.
     */
    public function validateTaskParameters(array $parameters): bool
    {
        $isValid = true;

        // Example: Custom email validation (beyond basic 'required' check)
        $emailList = $parameters['my_extension_email_list'] ?? '';
        if (!empty($emailList)) {
            $emails = GeneralUtility::trimExplode(',', $emailList, true);
            foreach ($emails as $email) {
                if (!GeneralUtility::validEmail($email)) {
                    GeneralUtility::makeInstance(FlashMessageService::class)
                        ->getMessageQueueByIdentifier()
                        ->addMessage(
                            GeneralUtility::makeInstance(
                                FlashMessage::class,
                                'Invalid email address: ' . $email,
                                '',
                                ContextualFeedbackSeverity::ERROR
                            )
                        );
                    $isValid = false;
                }
            }
        }

        return $isValid;
    }

    public function getAdditionalInformation(): string
    {
        $info = [];
        if ($this->myField !== '') {
            $info[] = 'Field: ' . $this->myField;
        }
        if ($this->emailList !== '') {
            $info[] = 'Emails: ' . $this->emailList;
        }
        return implode(', ', $info);
    }
}
Copied!

Key methods explained 

The new TCA-based approach uses three key methods for parameter handling:

getTaskParameters(): array

This method is already implemented in AbstractTask to handle task class properties automatically, but it can be overridden in task classes for custom behavior.

The method is primarily used:

  • For migration from the old serialized task format to the new TCA structure.
  • For non-native (deprecated) task types to store their values in the legacy parameters field.

For native TCA tasks, this method is typically no longer needed in custom tasks after the migration has been done, since field values are then stored directly in database columns.

setTaskParameters(array $parameters): void

Sets field values from an associative array. This method handles:

  • Migration from old \AdditionalFieldProvider field names to new TCA field names.
  • Loading saved task configurations when editing or executing tasks.
  • Parameter mapping during task creation and updates.
  • The method should always be implemented, especially for native tasks.

The migration pattern is: $this->myField = $parameters['oldName'] ?? $parameters['new_tca_field_name'] ?? '';

validateTaskParameters(array $parameters): bool

Optional method. Only implement this for validation that cannot be handled by FormEngine.

  • Basic validation (required, trim, etc.) should be done via TCA configuration (required property and eval options).
  • Use this method for complex business logic validation (e.g., email format validation, external API checks).
  • Return false and add a FlashMessage for validation errors.
  • FormEngine automatically handles standard TCA validation rules.

For a complete working example, see SystemStatusUpdateTask and its corresponding TCA configuration in EXT:reports/Configuration/TCA/Overrides/scheduler_system_status_update_task.php.

Deprecation: #106393 - Various methods in BackendUtility 

See forge#106393

Description 

Due to the introduction of the Schema API, several methods of \TYPO3\CMS\Backend\Utility\BackendUtility that retrieve information from $GLOBALS['TCA'] have been deprecated:

  • BackendUtility::getCommonSelectFields()
  • BackendUtility::getItemLabel()
  • BackendUtility::isTableLocalizable()
  • BackendUtility::isTableWorkspaceEnabled()
  • BackendUtility::isRootLevelRestrictionIgnored()
  • BackendUtility::isWebMountRestrictionIgnored()
  • BackendUtility::resolveFileReferences()

Impact 

Calling any of the mentioned methods now triggers a deprecation-level log entry and will stop working in TYPO3 v15.0.

The extension scanner reports usages as a strong match.

Affected installations 

Instances or extensions that directly call these methods are affected.

Migration 

The migration strategy is the same for all cases: use the corresponding Schema API methods directly in your code. In most cases, you'll need to inject TcaSchemaFactory via dependency injection.

getCommonSelectFields 

No substitution is available. The method was marked as @internal already. If your code depends on this functionality, copy the method into your own extension.

getItemLabel 

use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Schema\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;

// Before
return BackendUtility::getItemLabel('pages', 'title');

// After (retrieve an instance of TcaSchemaFactory via dependency
// injection of TYPO3\CMS\Core\Schema\TcaSchemaFactory)
$schema = $this->schemaFactory->has('pages')
    ? $this->schemaFactory->get('pages')
    : null;
return $schema !== null && $schema->hasField('title')
    ? $schema->getField('title')->getLabel()
    : null;
Copied!

isTableLocalizable 

use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Schema\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;

// Before
return BackendUtility::isTableLocalizable('pages');

// After (retrieve an instance of TcaSchemaFactory via dependency
// injection of TYPO3\CMS\Core\Schema\TcaSchemaFactory)
return $this->schemaFactory->has('pages')
    && $this->schemaFactory->get('pages')
        ->hasCapability(TcaSchemaCapability::Language);
Copied!

isTableWorkspaceEnabled 

use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Schema\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;

// Before
return BackendUtility::isTableWorkspaceEnabled('pages');

// After (retrieve an instance of TcaSchemaFactory via dependency
// injection of TYPO3\CMS\Core\Schema\TcaSchemaFactory)
return $this->schemaFactory->has('pages')
    && $this->schemaFactory->get('pages')
        ->hasCapability(TcaSchemaCapability::Workspace);
Copied!

isRootLevelRestrictionIgnored 

use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Schema\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;

// Before
return BackendUtility::isRootLevelRestrictionIgnored('pages');

// After (retrieve an instance of TcaSchemaFactory via dependency
// injection of TYPO3\CMS\Core\Schema\TcaSchemaFactory)
return $this->schemaFactory->has('pages')
    && $this->schemaFactory->get('pages')
        ->getCapability(
            TcaSchemaCapability::RestrictionRootLevel
        )->shallIgnoreRootLevelRestriction();
Copied!

isWebMountRestrictionIgnored 

use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Schema\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;

// Before
return BackendUtility::isWebMountRestrictionIgnored('pages');

// After (retrieve an instance of TcaSchemaFactory via dependency
// injection of TYPO3\CMS\Core\Schema\TcaSchemaFactory)
return $this->schemaFactory->has('pages')
    && $this->schemaFactory->get('pages')
        ->hasCapability(TcaSchemaCapability::RestrictionWebMount);
Copied!

resolveFileReferences 

No substitution is available. Copy the method into your own codebase and adapt it as needed.

Deprecation: #106405 - AbstractTypolinkBuilder->build 

See forge#106405

Description 

The build() method in \TYPO3\CMS\Frontend\Typolink\AbstractTypolinkBuilder has been deprecated in favor of the new \TYPO3\CMS\Frontend\Typolink\TypolinkBuilderInterface .

When creating custom TypolinkBuilder classes, the traditional approach was to:

  1. Extend \TYPO3\CMS\Frontend\Typolink\AbstractTypolinkBuilder
  2. Implement the build() method
  3. Receive dependencies via the constructor

This approach is now deprecated. The new, recommended pattern is to implement TypolinkBuilderInterface directly.

The ContentObjectRenderer property in AbstractTypolinkBuilder has also been deprecated and will be removed in TYPO3 v15.0. It should now be accessed via the \Psr\Http\Message\ServerRequestInterface object instead.

Impact 

Extension authors who have created custom TypolinkBuilder classes that extend AbstractTypolinkBuilder will see deprecation warnings when their link builders are used.

Deprecation warnings occur when:

  • A custom TypolinkBuilder class still implements the build() method
  • Code accesses the deprecated $contentObjectRenderer property
  • Dependencies are passed through the constructor instead of DI

Affected installations 

TYPO3 installations with extensions that:

  • Create custom TypolinkBuilder classes extending AbstractTypolinkBuilder
  • Override or extend the build() method
  • Access the $contentObjectRenderer property directly

Migration 

For end users, link generation continues to work as before using either:

  • ContentObjectRenderer , method typolink()
  • LinkFactory

These public APIs are not affected and do not trigger deprecations.

For extension developers, custom TypolinkBuilder implementations should now use the new \TYPO3\CMS\Frontend\Typolink\TypolinkBuilderInterface .

  1. Implement the new interface:
Recommended migration approach
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Frontend\Typolink\AbstractTypolinkBuilder;
use TYPO3\CMS\Frontend\Typolink\LinkResultInterface;
use TYPO3\CMS\Frontend\Typolink\TypolinkBuilderInterface;

// Before (deprecated)
class MyCustomLinkBuilder extends AbstractTypolinkBuilder
{
    public function build(
        array &$linkDetails,
        string $linkText,
        string $target,
        array $conf
    ): LinkResultInterface {
        // Custom logic using $this->contentObjectRenderer
    }
}

// After (recommended)
class MyCustomLinkBuilder implements TypolinkBuilderInterface
{
    public function buildLink(
        array $linkDetails,
        array $configuration,
        ServerRequestInterface $request,
        string $linkText = ''
    ): LinkResultInterface {
        $contentObjectRenderer = $request->getAttribute(
            'currentContentObject'
        );
        // Custom logic using $contentObjectRenderer
    }
}
Copied!
  1. Use dependency injection for required services instead of accessing them through global state or constructor arguments.
  2. Retrieve ContentObjectRenderer from the request object rather than the deprecated property.

All implementations of TypolinkBuilderInterface are automatically registered as public services in the Dependency Injection container, so no manual service configuration is necessary.

Extensions can maintain backward compatibility during the transition by implementing both the old build() and the new buildLink() methods.

Deprecation: #106527 - markFieldAsChanged() moved to FormEngine main module 

See forge#106527

Description 

The static method markFieldAsChanged() in the module @typo3/backend/form-engine-validation is used to modify the markup in the DOM to mark a field as changed. Technically, this is unrelated to validation itself, therefore the method has been moved to @typo3/backend/form-engine.

Impact 

Calling markFieldAsChanged() from @typo3/backend/form-engine-validation will trigger a deprecation notice in the browser console.

Affected installations 

All extensions using the deprecated code are affected.

Migration 

If not already done, import the main FormEngine module and call markFieldAsChanged() from there.

Example:

- import FormEngineValidation from '@typo3/backend/form-engine-validation.js';
+ import FormEngine from '@typo3/backend/form-engine.js';

- FormEngineValidation.markFieldAsChanged(fieldReference);
+ FormEngine.markFieldAsChanged(fieldReference);
Copied!

Example compatibility layer:

import FormEngine from '@typo3/backend/form-engine.js';
import FormEngineValidation from '@typo3/backend/form-engine-validation.js';

if (typeof FormEngine.markFieldAsChanged === 'function') {
  FormEngine.markFieldAsChanged(fieldReference);
} else {
  FormEngineValidation.markFieldAsChanged(fieldReference);
}
Copied!

Deprecation: #106618 - GeneralUtility::resolveBackPath 

See forge#106618

Description 

The method \TYPO3\CMS\Core\Utility\GeneralUtility::resolveBackPath has been marked as deprecated and will be removed in TYPO3 v15.0.

It served as a mechanism to remove relative path segments such as ".." when referencing files or directories. This was particularly important before TYPO3 v7, when every TYPO3 backend module had its own route and entry point PHP file. Nowadays, it is a relic from the past.

Since TYPO3 v13, this method has become even less relevant, as both the TYPO3 backend and frontend now share the same main entry point file (index.php).

Impact 

TYPO3 no longer resolves the back path of resource references or normalizes paths when rendering or referencing resources in the HTML output - neither in the frontend nor in the backend.

However, existing references will continue to work.

Affected installations 

TYPO3 installations with custom TypoScript inclusions or backend modules that reference files using relative paths may be affected. Such usage is uncommon in modern TYPO3 installations.

Migration 

References to resources should now use the EXT: prefix or be written relative to the public web path of the TYPO3 installation.

References to JavaScript modules (ES6 modules) should be managed through import maps using module names instead of relative paths.

Deprecation: #106821 - Workspace aware inline child tables are enforced 

See forge#106821

Description 

TCA tables that are used as inline child tables in a standard foreign_table relationship must be declared as workspace aware if their parent table is workspace aware.

Impact 

The TYPO3 Core may automatically add versioningWS = true to the ctrl section of inline child tables. In this case, a deprecation-level log entry will be issued, indicating that the TCA definition should be updated accordingly.

This TCA definition should be added even if the workspace system extension is not loaded. The database schema analyzer will then automatically add the required workspace-related database columns.

Affected installations 

TYPO3 instances with extensions that define inline relations where the parent table is workspace aware but the child table is not are affected.

A typical scenario involves inline child tables attached to the tt_content table.

Migration 

An automatic TCA migration scans TCA configurations for this scenario and adds versioningWS = true to affected child tables. Developers should add this declaration manually to their TCA to satisfy the migration and suppress deprecation log messages.

'ctrl' => [
    'versioningWS' => true,
],
Copied!

Note that the automatic migration does not detect children attached to inline fields within type => 'flex' (flex form) fields. Developers and integrators should still explicitly declare such child tables as workspace aware.

In general, the combination of "parent is workspace aware" and "child is not workspace aware" is not supported by the TYPO3 Core in inline foreign_table setups—regardless of whether the parent field is a database column or part of a flex form.

Deprecation: #106969 - Deprecate User TSConfig auth.BE.redirectToURL 

See forge#106969

Description 

The User TSConfig option auth.BE.redirectToURL has been marked as deprecated and will be removed in TYPO3 v15.0.

Impact 

Using the deprecated User TSConfig option triggers a deprecation-level log entry and will stop working in TYPO3 v15.0.

Affected installations 

TYPO3 installations using the User TSConfig option auth.BE.redirectToURL are affected.

Migration 

If a redirect after a successful backend login is required, create a custom PSR-15 middleware to handle the redirection.

Deprecation: #107047 - ExtensionManagementUtility::addPiFlexFormValue() 

See forge#107047

Description 

The method \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPiFlexFormValue() has been deprecated.

This method was historically used to assign FlexForm definitions to plugins registered as subtypes in the list_type field of tt_content. With the removal of subtypes (see Breaking: #105377 - Deprecated functionality removed) and the shift toward registering plugins as dedicated record types via CType, as well as the removal of the ds_pointerField and multi-entry ds array format (see Breaking: #107047 - Remove pointer field functionality of TCA flex), this separate method call is no longer necessary.

Impact 

Calling this method will trigger a deprecation warning. The extension scanner will also report any usages. The method will be removed in TYPO3 v15.0.

Affected installations 

Extensions that call \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPiFlexFormValue() to assign FlexForm definitions to plugins or content elements are affected.

Migration 

Instead of using this method, define the FlexForm configuration using one of the following approaches:

Option 1: Register via registerPlugin() 

Provide the FlexForm definition directly when registering the plugin. This has been possible since Feature: #107047 - FlexForm enhancements: Direct plugin registration and raw TCA support.

EXT:my_extension/ext_localconf.php
use TYPO3\CMS\Extbase\Utility\ExtensionUtility;

ExtensionUtility::registerPlugin(
    'MyExtension',
    'MyPlugin',
    'My Plugin Title',
    'my-extension-icon',
    'plugins',
    'Plugin description',
    'FILE:EXT:my_extension/Configuration/FlexForm.xml'
);
Copied!

Option 2: Register via addPlugin() for non-Extbase plugins 

This is also supported since Feature: #107047 - FlexForm enhancements: Direct plugin registration and raw TCA support.

EXT:my_extension/ext_localconf.php
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconRegistry;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\SelectItemUtility\SelectItem;

ExtensionManagementUtility::addPlugin(
    new SelectItem(
        'select',
        'My Plugin Title',
        'myextension_myplugin',
        'my-extension-icon',
        'plugins',
        'Plugin description'
    ),
    'FILE:EXT:my_extension/Configuration/FlexForm.xml'
);
Copied!

Option 3: Define FlexForm via TCA columnsOverrides 

You can also directly define the FlexForm in TCA:

EXT:my_extension/Configuration/TCA/Overrides/tt_content.php
$GLOBALS['TCA']['tt_content']['types']['my_plugin']['columnsOverrides']
    ['pi_flexform']['config']['ds'] =
    'FILE:EXT:my_extension/Configuration/FlexForm.xml';
Copied!

Deprecation: #107057 - Deprecate auto-render assets sections 

See forge#107057

Description 

The auto-rendering of template sections HeaderAssets and FooterAssets available in Fluid templates has been marked as deprecated in TYPO3 v14.0 and will be removed in TYPO3 v15.0.

Impact 

Using the deprecated sections will raise a deprecation-level log error and will stop working in TYPO3 v15.0.

Affected installations 

TYPO3 installations using the sections HeaderAssets and FooterAssets in Fluid templates.

Migration 

It is recommended to use the f:asset.script or f:asset.css ViewHelpers from the TYPO3 Asset Collector API to render required assets.

In scenarios where the f:asset.script or f:asset.css ViewHelpers are not suitable, users can use the f:page.headerData or f:page.footerData ViewHelpers to render custom HTML header or footer markup.

Deprecation: #107225 - Boolean sort direction in FileList->start() 

See forge#107225

Description 

The fourth parameter of the method \TYPO3\CMS\Filelist\FileList::start() has been renamed from $sortRev to $sortDirection and now accepts both boolean values (for backward compatibility) and SortDirection enum values.

Passing a boolean value for the sort direction (fourth parameter of FileList::start()) has been deprecated in favor of the new \TYPO3\CMS\Filelist\Type\SortDirection enum, providing better type safety and clarity. The parameter name has also changed from $sortRev to $sortDirection to more accurately describe its purpose.

Impact 

Calling FileList::start() with a boolean value as the fourth parameter triggers a deprecation warning. The functionality will continue to work for now but will be removed in TYPO3 v15.0.

Affected installations 

All installations using FileList::start() directly with a boolean value for the sort direction parameter are affected. This mainly applies to custom file browser implementations or extensions that instantiate and configure the FileList class directly, even though it is marked as @internal.

Migration 

Replace boolean values with the corresponding SortDirection enum values:

Example migration
use TYPO3\CMS\Filelist\Type\SortDirection;

// Before (deprecated)
$fileList->start($folder, $currentPage, $sortField, false, $mode);
$fileList->start($folder, $currentPage, $sortField, true, $mode);

// After
$fileList->start(
    $folder,
    $currentPage,
    $sortField,
    SortDirection::ASCENDING,
    $mode
);

$fileList->start(
    $folder,
    $currentPage,
    $sortField,
    SortDirection::DESCENDING,
    $mode
);
Copied!

The migration maintains the same functionality:

  • true (descending) → SortDirection::DESCENDING
  • false (ascending) → SortDirection::ASCENDING

Deprecation: #107229 - Deprecate Annotation namespace of Extbase attributes 

See forge#107229

Description 

With the removed support for PHP annotations in Extbase, the class namespace \TYPO3\CMS\Extbase\Annotation no longer reflects classes using PHP annotations. It now contains PHP attributes only.

The class namespace of all PHP attributes used with models, DTOs, and controller actions in the Extbase context has been moved to \TYPO3\CMS\Extbase\Attribute.

Developers and integrators are advised to migrate existing class usages to the new namespace. The deprecated namespace will be removed in TYPO3 v15.0.

Impact 

Usage of the deprecated class namespace is no longer recommended but will continue to work until TYPO3 v15.0.

Affected installations 

Instances using the deprecated class namespace for Extbase attributes in models, DTOs, or controller actions are affected.

Migration 

Aside from migrating PHP annotations to PHP attributes, existing usages of the class namespace \TYPO3\CMS\Extbase\Annotation should be updated to \TYPO3\CMS\Extbase\Attribute:

-TYPO3\CMS\Extbase\Annotation
+TYPO3\CMS\Extbase\Attribute
Copied!

Before: 

use TYPO3\CMS\Extbase\Annotation as Extbase;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class MyModel extends AbstractEntity
{
    #[Extbase\Validate(['validator' => 'NotEmpty'])]
    protected string $foo = '';
}
Copied!

After: 

use TYPO3\CMS\Extbase\Attribute as Extbase;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class MyModel extends AbstractEntity
{
    #[Extbase\Validate(['validator' => 'NotEmpty'])]
    protected string $foo = '';
}
Copied!

Deprecation: #107287 - FileCollectionRegistry->addTypeToTCA() method 

See forge#107287

Description 

The method \TYPO3\CMS\Core\Resource\Collection\FileCollectionRegistry->addTypeToTCA() has been deprecated in TYPO3 v14.0 and will be removed in TYPO3 v15.0.

This method was originally intended to register additional file collection types by directly manipulating the global $GLOBALS['TCA'] array for the sys_file_collection table. With modern TCA configuration patterns, this approach is no longer recommended.

Impact 

Calling this method will trigger a deprecation-level log entry and will stop working in TYPO3 v15.0.

Affected installations 

Instances using the FileCollectionRegistry->addTypeToTCA() method directly are affected.

The extension scanner will report usages as weak match.

Migration 

Instead of using this method, configure file collection types directly in your TCA configuration files. Move the TCA configuration from the method call to your extension's Configuration/TCA/Overrides/sys_file_collection.php file.

Before (deprecated)
use TYPO3\CMS\Core\Resource\Collection\FileCollectionRegistry;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$fileCollectionRegistry = GeneralUtility::makeInstance(
    FileCollectionRegistry::class
);
$fileCollectionRegistry->addTypeToTCA(
    'mytype',
    'My Collection Type',
    'description,my_field',
    ['my_field' => ['config' => ['type' => 'input']]]
);
Copied!
After (recommended) – EXT:my_extension/Configuration/TCA/Overrides/sys_file_collection.php
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

$GLOBALS['TCA']['sys_file_collection']['types']['mytype'] = [
    'showitem' => '
        sys_language_uid, l10n_parent, l10n_diffsource,
        title, --palette--;;1, type, description, my_field
    ',
];

$GLOBALS['TCA']['sys_file_collection']['columns']['type']['config']['items'][] =
    [
        'label' => 'My Collection Type',
        'value' => 'mytype',
    ];

// Add additional columns if needed
ExtensionManagementUtility::addTCAcolumns(
    'sys_file_collection',
    [
        'my_field' => [
            'config' => [
                'type' => 'input',
            ],
        ],
    ]
);
Copied!

Deprecation: #107413 - PathUtility getRelativePath(to) methods 

See forge#107413

Description 

The following methods in \TYPO3\CMS\Core\Utility\PathUtility have been marked as deprecated and will be removed in TYPO3 v15.0:

  • PathUtility::getRelativePath()
  • PathUtility::getRelativePathTo()

These methods are no longer needed, as TYPO3's path handling has been simplified due to the unification of the entry point URLs.

Since TYPO3 v13, both frontend and backend use the same main entry point (htdocs/index.php), making relative path calculations obsolete.

Impact 

Calling these methods will trigger a PHP deprecation warning. They will continue to work as before until they are removed in TYPO3 v15.0.

Affected installations 

TYPO3 installations with custom extensions or code that directly call the deprecated methods are affected:

  • PathUtility::getRelativePath()
  • PathUtility::getRelativePathTo()

The extension scanner will report any usage as a strong match.

Migration 

Instead of calculating relative paths manually, use absolute paths or the appropriate TYPO3 APIs for path handling:

  • Use GeneralUtility::getFileAbsFileName() for extension resources
  • Use the EXT: prefix when referencing extension resources
  • Use PathUtility::getPublicResourceWebPath() for public extension resources
  • Reference paths relative to the public web path of the TYPO3 installation
Before (deprecated)
use TYPO3\CMS\Core\Utility\PathUtility;

$relativePath = PathUtility::getRelativePath($sourcePath, $targetPath);
$relativeToPath = PathUtility::getRelativePathTo($absolutePath);
Copied!
After (recommended)
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;

// Use absolute paths or appropriate TYPO3 APIs
$absolutePath = GeneralUtility::getFileAbsFileName(
    'EXT:my_extension/Resources/Public/file.js'
);

$webPath = PathUtility::getPublicResourceWebPath(
    'EXT:my_extension/Resources/Public/file.js'
);
Copied!

Deprecation: #107436 - Localization Parsers 

See forge#107436

Description 

Due to the migration to Symfony Translation components (see Feature: #107436 - Symfony Translation Component integration), the following localization parser classes have been deprecated:

  • \TYPO3\CMS\Core\Localization\Parser\AbstractXmlParser
  • \TYPO3\CMS\Core\Localization\Parser\LocalizationParserInterface
  • \TYPO3\CMS\Core\Localization\Parser\XliffParser

The global configuration option $GLOBALS['TYPO3_CONF_VARS']['SYS']['lang']['parser'] has been removed and replaced with $GLOBALS['TYPO3_CONF_VARS']['LANG']['loader'] .

Impact 

Using any of the mentioned parser classes will raise a deprecation level log entry and will stop working in TYPO3 v15.0.

The extension scanner will report usages as strong match.

Affected installations 

Instances using the deprecated localization parser classes directly or configuring custom parsers via the removed global configuration option.

Migration 

Replace usage of the deprecated parser classes with Symfony Translation loaders. Use the new XliffLoader class for XLIFF file processing.

The Symfony Translator and its loaders are now responsible for file parsing and should be used instead of the deprecated TYPO3 parsers.

For custom localization needs, implement Symfony Translation loader interfaces instead of the deprecated TYPO3 parser interfaces.

Configuration changes:

use TYPO3\CMS\Core\Localization\Loader\XliffLoader;
use TYPO3\CMS\Core\Localization\Parser\XliffParser;

// Before
$GLOBALS['TYPO3_CONF_VARS']['SYS']['lang']['parser']['xlf'] = XliffParser::class;

// After
$GLOBALS['TYPO3_CONF_VARS']['LANG']['loader']['xlf'] = XliffLoader::class;
Copied!

Please note: This functionality only affects the internal handling of translation files ("locallang" files). The public API of the localization system remains unchanged.

Deprecation: #107537 - GeneralUtility::createVersionNumberedFilename 

See forge#107537

Description 

GeneralUtility::createVersionNumberedFilename adds cache busting to a file URL, when called in a certain and correct order with other legacy API methods to create URLs from system resources.

This class and its functionality is superseded by the System Resource API.

Impact 

Calling this method will trigger a PHP deprecation warning. The method will continue to work as is, until it is removed in TYPO3 v15.0.

Affected installations 

TYPO3 installations with custom extensions or code that directly call this deprecated method:

  • GeneralUtility::createVersionNumberedFilename

Migration 

Use the System Resource API instead.

Before:

MyClass
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;

public function renderUrl(string $file): string
{
    $file = GeneralUtility::getFileAbsFileName($file);
    $partialUrl = GeneralUtility::createVersionNumberedFilename($file);
    return PathUtility::getAbsoluteWebPath($partialUrl);
}
Copied!

After:

MyClass
use TYPO3\CMS\Core\Http\ServerRequestInterface;
use TYPO3\CMS\Core\Resource\SystemResourceFactory;
use TYPO3\CMS\Core\Resource\SystemResourcePublisherInterface;
use TYPO3\CMS\Core\Resource\UriGenerationOptions;

public function __construct(
    private readonly SystemResourceFactory $systemResourceFactory,
    private readonly SystemResourcePublisherInterface $resourcePublisher,
) {}

public function renderUrl(
    string $resourceIdentifier,
    ServerRequestInterface $request
): string {
    $resource = $this->systemResourceFactory->createPublicResource(
        $resourceIdentifier
    );
    return (string)$this->resourcePublisher->generateUri(
        $resource,
        $request,
        new UriGenerationOptions(absoluteUri: true),
    );
}
Copied!

Deprecation: #107537 - FilePathSanitizer service 

See forge#107537

Description 

\TYPO3\CMS\Frontend\Resource\FilePathSanitizer was introduced as a service class to replace the logic that was previously part of $TSFE->tmpl.

The goal of this class was to validate and manipulate given strings to be used for URL generation later in the process.

This class and its functionality are superseded by the System Resource API.

Impact 

All installations using the FilePathSanitizer will trigger a deprecation notice when the class is instantiated.

Otherwise, this class will continue to work as is, until removed in TYPO3 v15.0.

Affected installations 

TYPO3 installations with custom extensions or code that instantiate FilePathSanitizer .

The extension scanner will report any usage as a strong match.

Migration 

Use the System Resource API instead.

Before:

MyClass
use TYPO3\CMS\Frontend\Resource\FilePathSanitizer;
use Psr\Http\Message\ServerRequestInterface;

public function __construct(
    private readonly FilePathSanitizer $pathSanitizer,
) {}

public function renderUrl(string $someString): string
{
    $pathRelativeToPublicDir = $this->pathSanitizer->sanitize($someString);
    return $this->codeThatDoesDetectTheCorrectUrlPrefix()
        . $pathRelativeToPublicDir;
}
Copied!

After:

MyClass
use TYPO3\CMS\Core\Http\ServerRequestInterface;
use TYPO3\CMS\Core\Resource\SystemResourceFactory;
use TYPO3\CMS\Core\Resource\SystemResourcePublisherInterface;
use TYPO3\CMS\Core\Resource\UriGenerationOptions;

public function __construct(
    private readonly SystemResourceFactory $systemResourceFactory,
    private readonly SystemResourcePublisherInterface $resourcePublisher,
) {}

public function renderUrl(
    string $resourceIdentifier,
    ServerRequestInterface $request
): string {
    $resource = $this->systemResourceFactory->createPublicResource(
        $resourceIdentifier
    );
    return (string)$this->resourcePublisher->generateUri(
        $resource,
        $request,
        new UriGenerationOptions(absoluteUri: true),
    );
}
Copied!

Deprecation: #107537 - TYPO3\CMS\Core\Utility\PathUtility::getPublicResourceWebPath 

See forge#107537

Description 

The static method \TYPO3\CMS\Core\Utility\PathUtility::getPublicResourceWebPath($extResource) was marked internal since its introduction. Since there were no good alternatives to this API, it is now deprecated first, before being removed with TYPO3 v15.0.

Impact and affected installations 

TYPO3 installations using PathUtility::getPublicResourceWebPath($extResource) will receive a deprecation message for each call of this method.

Migration 

Use the System Resource API instead.

Before:

MyClass
use TYPO3\CMS\Core\Utility\PathUtility;

public function renderUrl(string $extResource): string
{
    return PathUtility::getPublicResourceWebPath($extResource);
}
Copied!

After:

MyClass
use TYPO3\CMS\Core\Http\ServerRequestInterface;
use TYPO3\CMS\Core\Resource\SystemResourceFactory;
use TYPO3\CMS\Core\Resource\SystemResourcePublisherInterface;
use TYPO3\CMS\Core\Resource\UriGenerationOptions;

public function __construct(
    private readonly SystemResourceFactory $systemResourceFactory,
    private readonly SystemResourcePublisherInterface $resourcePublisher,
) {}

public function renderUrl(
    string $resourceIdentifier,
    ServerRequestInterface $request
): string {
    $resource = $this->systemResourceFactory->createPublicResource(
        $resourceIdentifier
    );
    return (string)$this->resourcePublisher->generateUri(
        $resource,
        $request,
        new UriGenerationOptions(absoluteUri: true),
    );
}
Copied!

Deprecation: #107550 - Table Garbage Collection Task configuration via $GLOBALS 

See forge#107550

Description 

The \TYPO3\CMS\Scheduler\Task\TableGarbageCollectionTask has been migrated to use TYPO3's native TCA-based task configuration system. As part of this migration, the previous configuration method using

$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][TableGarbageCollectionTask::class]['options']['tables']

has been deprecated and will be removed in TYPO3 v15.0.

Impact 

Using the old configuration method will trigger a PHP deprecation warning. The functionality continues to work for now, with the deprecated configuration being merged with the new TCA-based configuration automatically.

Affected installations 

Any installation that configures custom tables for the TableGarbageCollectionTask using the deprecated $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'] configuration.

The extension scanner will report any usage as a weak match.

Migration 

Instead of configuring tables via $GLOBALS['TYPO3_CONF_VARS'] , tables should now be configured in TCA using the taskOptions configuration of the corresponding record type within Configuration/TCA/Overrides/.

Before (deprecated):

ext_localconf.php
use TYPO3\CMS\Scheduler\Task\TableGarbageCollectionTask;

$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks']
    [TableGarbageCollectionTask::class]['options']['tables']
    ['tx_myextension_my_table'] = [
        'dateField' => 'tstamp',
        'expirePeriod' => 90,
    ];
Copied!

After (new method):

Configuration/TCA/Overrides/scheduler_table_garbage_collection.php
use TYPO3\CMS\Scheduler\Task\TableGarbageCollectionTask;

if (isset($GLOBALS['TCA']['tx_scheduler_task'])) {
    $GLOBALS['TCA']['tx_scheduler_task']['types']
        [TableGarbageCollectionTask::class]['taskOptions']['tables']
        ['tx_myextension_my_table'] = [
            'dateField' => 'tstamp',
            'expirePeriod' => 90,
        ];
}
Copied!

It is also possible to modify the tables added by TYPO3, for example changing the expirePeriod of table sys_log:

use TYPO3\CMS\Scheduler\Task\TableGarbageCollectionTask;

if (isset($GLOBALS['TCA']['tx_scheduler_task'])) {
    $GLOBALS['TCA']['tx_scheduler_task']['types']
        [TableGarbageCollectionTask::class]['taskOptions']['tables']
        ['sys_log']['expirePeriod'] = 240;
}
Copied!

The new TCA-based configuration provides the same functionality while integrating better with TYPO3's native scheduler task system and FormEngine.

Deprecation: #107562 - IP Anonymization Task configuration via $GLOBALS 

See forge#107562

Description 

The \TYPO3\CMS\Scheduler\Task\IpAnonymizationTask has been migrated to use TYPO3's native TCA-based task configuration system. As part of this migration, the previous configuration method using $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][IpAnonymizationTask::class]['options']['tables'] has been deprecated and will be removed in TYPO3 v15.0.

Impact 

Using the old configuration method will trigger a PHP deprecation warning. The functionality continues to work for now, with the deprecated configuration being merged with the new TCA-based configuration automatically.

Affected installations 

Any installation that configures custom tables for the IpAnonymizationTask using the deprecated $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'] configuration.

The extension scanner will report any usage as a weak match.

Migration 

Instead of configuring tables via $GLOBALS['TYPO3_CONF_VARS'] , tables should now be configured in TCA using the taskOptions configuration of the corresponding record type within Configuration/TCA/Overrides/.

Before (deprecated):

ext_localconf.php
use TYPO3\CMS\Scheduler\Task\IpAnonymizationTask;

$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][IpAnonymizationTask::class]['options']['tables']['tx_myextension_my_table'] = [
        'dateField' => 'tstamp',
        'ipField' => 'private_ip',
    ];
Copied!

After (new method):

Configuration/TCA/Overrides/scheduler_ip_anonymization.php
use TYPO3\CMS\Scheduler\Task\IpAnonymizationTask;

if (isset($GLOBALS['TCA']['tx_scheduler_task'])) {
    $GLOBALS['TCA']['tx_scheduler_task']['types'][IpAnonymizationTask::class]['taskOptions']['tables'] ['tx_myextension_my_table'] = [
            'dateField' => 'tstamp',
            'ipField' => 'private_ip',
        ];
}
Copied!

It is also possible to modify the tables added by TYPO3, for example changing the dateField of sys_log:

use TYPO3\CMS\Scheduler\Task\IpAnonymizationTask;

if (isset($GLOBALS['TCA']['tx_scheduler_task'])) {
    $GLOBALS['TCA']['tx_scheduler_task']['types'][IpAnonymizationTask::class]['taskOptions']['tables']['sys_log']['dateField']
        = 'custom_date';
}
Copied!

The new TCA-based configuration provides the same functionality while integrating better with TYPO3's native scheduler task system and FormEngine.

Deprecation: #107648 - InfoboxViewHelper STATE_* constants 

See forge#107648

Description 

The public constants in \TYPO3\CMS\Fluid\ViewHelpers\Be\InfoboxViewHelper for defining the state or severity of an infobox have been deprecated:

  • InfoboxViewHelper::STATE_NOTICE
  • InfoboxViewHelper::STATE_INFO
  • InfoboxViewHelper::STATE_OK
  • InfoboxViewHelper::STATE_WARNING
  • InfoboxViewHelper::STATE_ERROR

These constants have been superseded by the dedicated enum \TYPO3\CMS\Core\Type\ContextualFeedbackSeverity , which provides a single source of truth for severity levels across the entire TYPO3 Core and improves type safety and maintainability.

Impact 

Using these constants will trigger a PHP deprecation warning. The constants will be removed in TYPO3 v15.0. The extension scanner will report usages as weak match.

Affected installations 

Instances using any of the STATE_* constants from InfoboxViewHelper in their PHP code or Fluid templates.

Migration 

Replace the deprecated constants with the corresponding ContextualFeedbackSeverity enum.

Example (PHP)
// Before
use TYPO3\CMS\Fluid\ViewHelpers\Be\InfoboxViewHelper;
$state = InfoboxViewHelper::STATE_ERROR;

// After - Recommended: Use the enum directly
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
$severity = ContextualFeedbackSeverity::ERROR;

// Alternative: Use the integer value when explicitly needed
$stateValue = ContextualFeedbackSeverity::ERROR->value;
Copied!

In Fluid templates, use the enum via f:constant():

Example (Fluid Template)
<!-- Before -->
<f:be.infobox
    title="Error!"
    state="{f:constant(name: 'TYPO3\CMS\Fluid\ViewHelpers\Be\InfoboxViewHelper::STATE_ERROR')}">
    Error message
</f:be.infobox>

<!-- After -->
<f:be.infobox
    title="Error!"
    state="{f:constant(name: 'TYPO3\CMS\Core\Type\ContextualFeedbackSeverity::ERROR')}">
    Error message
</f:be.infobox>
Copied!

The InfoboxViewHelper has been updated to accept both the enum directly and integer values for backward compatibility.

Mapping table 

Deprecated constant Replacement Value
InfoboxViewHelper::STATE_NOTICE ContextualFeedbackSeverity::NOTICE->value -2
InfoboxViewHelper::STATE_INFO ContextualFeedbackSeverity::INFO->value -1
InfoboxViewHelper::STATE_OK ContextualFeedbackSeverity::OK->value 0
InfoboxViewHelper::STATE_WARNING ContextualFeedbackSeverity::WARNING->value 1
InfoboxViewHelper::STATE_ERROR ContextualFeedbackSeverity::ERROR->value 2

Deprecation: #107725 - Deprecate usage of array in password for authentication in Redis cache backend 

See forge#107725

Description 

Since Redis 6.0, it is possible to authenticate against Redis using both a username and a password. Prior to this version, authentication was only possible with a password.

With this change, TYPO3's Redis cache backend supports username and password authentication directly. You can now configure the TYPO3 Redis cache backend as follows:

config/system/additional.php
use TYPO3\CMS\Core\Cache\Backend\RedisBackend;

$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['pages']['backend']
    = RedisBackend::class;

$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['pages']['options']
    = [
        'defaultLifetime' => 86400,
        'database' => 0,
        'hostname' => 'redis',
        'port' => 6379,
        'username' => 'redis',
        'password' => 'redis',
    ];
Copied!

Impact 

The password configuration option of the Redis cache backend is now typed as array|string.

Setting this configuration option with an array is deprecated and will be removed in TYPO3 v15.0.

Affected installations 

All installations using the Redis cache backend and configuring the password option as an array containing both username and password values are affected.

Migration 

Use the dedicated configuration options username and password instead of passing an array to password.

Before (deprecated):

config/system/additional.php
use TYPO3\CMS\Core\Cache\Backend\RedisBackend;

$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['pages']['backend']
    = RedisBackend::class;

$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['pages']['options']
    = [
        'defaultLifetime' => 86400,
        'database' => 0,
        'hostname' => 'redis',
        'port' => 6379,
        'password' => [
            'user' => 'redis',
            'pass' => 'redis',
        ],
    ];
Copied!

After (recommended):

config/system/additional.php
use TYPO3\CMS\Core\Cache\Backend\RedisBackend;

$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['pages']['backend']
    = RedisBackend::class;

$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['pages']['options']
    = [
        'defaultLifetime' => 86400,
        'database' => 0,
        'hostname' => 'redis',
        'port' => 6379,
        'username' => 'redis',
        'password' => 'redis',
    ];
Copied!

Deprecation: #107813 - Deprecate MetaInformation API in DocHeader 

See forge#107813

Description 

The \TYPO3\CMS\Backend\Template\Components\MetaInformation class and related methods in \TYPO3\CMS\Backend\Template\Components\DocHeaderComponent have been deprecated in favor of the new breadcrumb component architecture.

The following have been marked as deprecated:

  • DocHeaderComponent::setMetaInformation()
  • DocHeaderComponent::setMetaInformationForResource()
  • MetaInformation class

These APIs were previously used to display page navigation paths in the backend document header. This functionality is now handled by the breadcrumb component, which provides richer context and better navigation capabilities.

Impact 

Calling any of the deprecated methods will trigger a PHP E_USER_DEPRECATED error.

The MetaInformation class is now marked as @internal and should not be used in extensions.

Affected installations 

All installations using custom backend modules that call:

  • $view->getDocHeaderComponent()->setMetaInformation($pageRecord)
  • $view->getDocHeaderComponent()->setMetaInformationForResource($resource)

or any custom code relying on the MetaInformation class.

The Extension Scanner will detect usage of the deprecated methods and classes, making it easy to identify code that needs to be updated.

Migration 

Replace calls to the deprecated methods with the new convenience methods on DocHeaderComponent .

Before:

Example (before)
use TYPO3\CMS\Backend\Template\Components\DocHeaderComponent;

// For page records
$view->getDocHeaderComponent()->setMetaInformation($pageInfo);

// For file or folder resources
$view->getDocHeaderComponent()->setMetaInformationForResource($resource);
Copied!

After:

Example (after)
use TYPO3\CMS\Backend\Template\Components\DocHeaderComponent;

// For page records
$view->getDocHeaderComponent()->setPageBreadcrumb($pageInfo);

// For file or folder resources
$view->getDocHeaderComponent()->setResourceBreadcrumb($resource);
Copied!

An additional convenience method is available for records:

Example (record breadcrumb)
use TYPO3\CMS\Backend\Template\Components\DocHeaderComponent;

// For any record type (by table and UID)
$view->getDocHeaderComponent()->setRecordBreadcrumb('tt_content', 123);
Copied!

For advanced scenarios requiring custom breadcrumb logic (such as conditional context selection based on controller state), see the implementation in \TYPO3\CMS\Backend\Controller\EditDocumentController , which uses \TYPO3\CMS\Backend\Breadcrumb\BreadcrumbFactory directly with setBreadcrumbContext().

Deprecation: #107823 - ButtonBar, Menu, and MenuRegistry make* methods deprecated 

See forge#107823

Description 

The factory methods in \TYPO3\CMS\Backend\Template\Components\ButtonBar for creating button instances, in \TYPO3\CMS\Backend\Template\Components\Menu for creating menu item instances, and in \TYPO3\CMS\Backend\Template\Components\MenuRegistry for creating menu instances have been deprecated in favor of using the new \TYPO3\CMS\Backend\Template\Components\ComponentFactory class directly.

The following methods are now deprecated:

  • ButtonBar::makeGenericButton()
  • ButtonBar::makeInputButton()
  • ButtonBar::makeSplitButton()
  • ButtonBar::makeDropDownButton()
  • ButtonBar::makeLinkButton()
  • ButtonBar::makeFullyRenderedButton()
  • ButtonBar::makeShortcutButton()
  • ButtonBar::makeButton()
  • Menu::makeMenuItem()
  • MenuRegistry::makeMenu()

Impact 

Calling any of the deprecated make*() methods on ButtonBar , \Menu, or MenuRegistry will trigger a PHP deprecation notice.

The methods continue to work in TYPO3 v14 but will be removed in TYPO3 v15.

Affected installations 

All extensions using ButtonBar::make*() methods to create buttons, Menu::makeMenuItem() to create menu items, or MenuRegistry::makeMenu() to create menus are affected. The extension scanner will report any usages.

Migration 

Inject \TYPO3\CMS\Backend\Template\Components\ComponentFactory in your controller and use its create*() methods instead of ButtonBar::make*().

Before:

Example (before)
use Psr\Http\Message\ResponseInterface;

public function myAction(): ResponseInterface
{
    $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();

    $linkButton = $buttonBar->makeLinkButton()
        ->setHref($url)
        ->setTitle('My Link')
        ->setIcon($icon);

    $buttonBar->addButton($linkButton);
    // ...
}
Copied!

After:

Example (after)
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;

public function __construct(
    protected readonly ComponentFactory $componentFactory,
) {}

public function myAction(): ResponseInterface
{
    $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();

    $linkButton = $this->componentFactory->createLinkButton()
        ->setHref($url)
        ->setTitle('My Link')
        ->setIcon($icon);

    $buttonBar->addButton($linkButton);
    // ...
}
Copied!

Additionally, consider using the preconfigured button creation methods like createBackButton(), createCloseButton(), createSaveButton(), createReloadButton(), and createViewButton() for common button patterns.

For the low-level makeButton(string $className) method, use GeneralUtility::makeInstance() directly or the appropriate ComponentFactory::create*() method:

Example (button instantiation)
use TYPO3\CMS\Core\Utility\GeneralUtility;

// Before:
$button = $buttonBar->makeButton(MyCustomButton::class);

// After (option 1 - direct instantiation):
$button = GeneralUtility::makeInstance(MyCustomButton::class);

// After (option 2 - via factory if it's a standard button):
$button = $this->componentFactory->createLinkButton();
Copied!

For Menu::makeMenuItem(), use ComponentFactory::createMenuItem():

Example (menu items)
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;

// Before:
$menu = $menuRegistry->makeMenu();
$menuItem = $menu->makeMenuItem()
    ->setTitle('My View')
    ->setHref($url);
$menu->addMenuItem($menuItem);

// After:
public function __construct(
    protected readonly ComponentFactory $componentFactory,
) {}

$menu = $this->componentFactory->createMenu();
$menuItem = $this->componentFactory->createMenuItem()
    ->setTitle('My View')
    ->setHref($url);
$menu->addMenuItem($menuItem);
Copied!

For MenuRegistry::makeMenu(), use ComponentFactory::createMenu():

Example (menus)
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;

// Before:
$menuRegistry = $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry();
$menu = $menuRegistry->makeMenu();
$menu->setIdentifier('viewSelector')->setLabel('View');

// After:
public function __construct(
    protected readonly ComponentFactory $componentFactory,
) {}

$menuRegistry = $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry();
$menu = $this->componentFactory->createMenu();
$menu->setIdentifier('viewSelector')->setLabel('View');
Copied!

Additionally, note that Menu::addMenuItem() now returns static to support fluent interface patterns:

Example (fluent chaining)
// Now possible with fluent interface:
$menu->addMenuItem($menuItem1)
    ->addMenuItem($menuItem2)
    ->addMenuItem($menuItem3);
Copied!

Deprecation: #107938 - Deprecate unused XLIFF files 

See forge#107938

Description 

The following XLIFF files have been deprecated, as they are not used in the TYPO3 Core anymore:

  • EXT:backend/Resources/Private/Language/locallang_view_help.xlf
  • EXT:backend/Resources/Private/Language/locallang_sitesettings_module.xlf
  • EXT:backend/Resources/Private/Language/locallang_siteconfiguration_module.xlf
  • EXT:backend/Resources/Private/Language/locallang_mod.xlf
  • EXT:belog/Resources/Private/Language/locallang_mod.xlf
  • EXT:beuser/Resources/Private/Language/locallang_mod.xlf
  • EXT:core/Resources/Private/Language/locallang_mod_usertools.xlf
  • EXT:core/Resources/Private/Language/locallang_mod_system.xlf
  • EXT:core/Resources/Private/Language/locallang_mod_site.xlf
  • EXT:core/Resources/Private/Language/locallang_mod_file.xlf
  • EXT:core/Resources/Private/Language/locallang_mod_help.xlf
  • EXT:core/Resources/Private/Language/locallang_mod_admintools.xlf
  • EXT:core/Resources/Private/Language/locallang_tsfe.xlf
  • EXT:dashboard/Resources/Private/Language/locallang_mod.xlf
  • EXT:extensionmanager/Resources/Private/Language/locallang_mod.xlf
  • EXT:form/Resources/Private/Language/locallang_module.xlf
  • EXT:indexed_search/Resources/Private/Language/locallang_mod.xlf
  • EXT:install/Resources/Private/Language/ModuleInstallUpgrade.xlf
  • EXT:install/Resources/Private/Language/ModuleInstallSettings.xlf
  • EXT:install/Resources/Private/Language/ModuleInstallMaintenance.xlf
  • EXT:install/Resources/Private/Language/ModuleInstallEnvironment.xlf
  • EXT:install/Resources/Private/Language/BackendModule.xlf
  • EXT:info/Resources/Private/Language/locallang_mod_web_info.xlf
  • EXT:linkvalidator/Resources/Private/Language/Module/locallang_mod.xlf
  • EXT:recycler/Resources/Private/Language/locallang_mod.xlf

They will be removed with TYPO3 v15.0.

The console command vendor/bin/typo3 language:domain:list does not list deprecated language domains, unless the option --deprecated is used.

Impact 

Using a label reference from one of these files triggers a E_USER_DEPRECATED error.

Affected installations 

Third-party extensions and site packages that use labels from the listed sources will not be able to display the affected labels with TYPO3 v15.0.

Migration 

If the desired string is contained in another language domain, consider using that domain. Otherwise, move the required labels into your extension or site package.

Deprecation: #107963 - sys_redirect default type name changed to "default" 

See forge#107963

Description 

The default type name for the sys_redirect table has been changed from '1' to 'default' in TYPO3 v14.0 to align with TYPO3 naming conventions and allow for better type extensibility.

Extensions that directly access $GLOBALS['TCA']['sys_redirect']['types']['1'] to manipulate the redirect TCA configuration must be updated to use the new 'default' key instead.

Impact 

Direct access to $GLOBALS['TCA']['sys_redirect']['types']['1'] will no longer work, as the type has been renamed to 'default'.

Extensions that manipulate the sys_redirect TCA type configuration must be updated accordingly.

The TCA migration layer will automatically migrate any custom $GLOBALS['TCA']['sys_redirect']['types']['1'] definitions to 'default' during TCA compilation, but a deprecation message will be logged.

Affected installations 

Instances with extensions that directly manipulate $GLOBALS['TCA']['sys_redirect']['types']['1'] to customize the default redirect record type.

Migration 

Update your TCA override files to use the new type name 'default' instead of '1'.

// Before - In Configuration/TCA/Overrides/sys_redirect.php
$GLOBALS['TCA']['sys_redirect']['types']['1']['label']
    = 'My custom label';

// After - In Configuration/TCA/Overrides/sys_redirect.php
$GLOBALS['TCA']['sys_redirect']['types']['default']['label']
    = 'My custom label';
Copied!

Deprecation: #108008 - Manual shortcut button creation 

See forge#108008

Description 

Manually creating and adding \ShortcutButton instances to the button bar is deprecated and will trigger a deprecation warning.

Controllers should use the new \TYPO3\CMS\Backend\Template\Components\DocHeaderComponent::setShortcutContext() method instead, which automatically creates and positions the shortcut button.

Impact 

Controllers that manually create and add \ShortcutButton instances to the button bar will trigger a deprecation warning. The shortcut button will still work as expected.

Affected installations 

Installations with custom backend modules that manually create shortcut buttons.

Migration 

Replace manual shortcut button creation with the new API.

Before:

$shortcutButton = $this->componentFactory->createShortcutButton()
    ->setRouteIdentifier('my_module')
    ->setDisplayName('My Module')
    ->setArguments(['id' => $pageId]);
$view->addButtonToButtonBar($shortcutButton);
Copied!

After:

$view->getDocHeaderComponent()->setShortcutContext(
    routeIdentifier: 'my_module',
    displayName: 'My Module',
    arguments: ['id' => $pageId]
);
Copied!

Deprecation: #108148 - Fluid LenientArgumentProcessor 

See forge#108148

Description 

Fluid 5.0 deprecates \TYPO3Fluid\Fluid\Core\ViewHelper\LenientArgumentProcessor , which will be removed with Fluid 6.0. \TYPO3Fluid\Fluid\Core\ViewHelper\StrictArgumentProcessor is now used instead.

Impact 

The impact of the switch to \TYPO3Fluid\Fluid\Core\ViewHelper\StrictArgumentProcessor is documented in Breaking: #108148 - Strict Types in Fluid ViewHelpers.

Affected installations 

Installations that use the \TYPO3Fluid\Fluid\Core\ViewHelper\LenientArgumentProcessor programmatically.

Migration 

The class can be copied to the project/extension if it's still required.

Deprecation: #108227 - Usage of #[IgnoreValidation] and #[Validate] attributes for parameters at method level 

See forge#108227

Description 

Usage of the following extbase attribute properties is deprecated since TYPO3 v14.0:

  • $argumentName property of #[IgnoreValidation] attribute
  • $param property of #[Validate] attribute

Instead of using these properties, the containing attributes should be placed directly at the appropriate method parameters.

Example:

final class FooController extends ActionController
{
    public function barAction(
        #[IgnoreValidation]
        string $something,
    ): ResponseInterface {
        // Do something...
    }

    public function bazAction(
        #[Validate(validator: 'NotEmpty')]
        string $anythingNotEmpty,
    ): ResponseInterface {
        // Do something...
    }
}
Copied!

Impact 

Passing a value other than null to the mentioned attribute parameters will trigger a deprecation warning. Validation will still work as expected, but will stop working with TYPO3 v15.

Affected installations 

All installations using the #[IgnoreValidation] and #[Validate] attributes in Extbase context in combination with the mentioned attribute parameters are affected.

Migration 

Developers can easily migrate their implementations by moving parameter-related attributes next to the method parameters instead of the related method.

Before:

final class FooController extends ActionController
{
    #[IgnoreValidation(argumentName: 'something')]
    public function barAction(string $something): ResponseInterface
    {
        // Do something...
    }

    #[Validate(validator: 'NotEmpty', param: 'anythingNotEmpty')]
    public function bazAction(string $anythingNotEmpty): ResponseInterface
    {
        // Do something...
    }
}
Copied!

After:

final class FooController extends ActionController
{
    public function barAction(
        #[IgnoreValidation]
        string $something,
    ): ResponseInterface {
        // Do something...
    }

    public function bazAction(
        #[Validate(validator: 'NotEmpty')]
        string $anythingNotEmpty,
    ): ResponseInterface {
        // Do something...
    }
}
Copied!

Combined diff:

 final class FooController extends ActionController
 {
-    #[IgnoreValidation(argumentName: 'something')]
-    public function barAction(string $something): ResponseInterface
-    {
+    public function barAction(
+        #[IgnoreValidation]
+        string $something,
+    ): ResponseInterface {
         // Do something...
     }


-    #[Validate(validator: 'NotEmpty', param: 'anythingNotEmpty')]
-    public function bazAction(string $anythingNotEmpty): ResponseInterface
-    {
+    public function bazAction(
+        #[Validate(validator: 'NotEmpty')]
+        string $anythingNotEmpty,
+    ): ResponseInterface {
         // Do something...
     }
 }
Copied!

Important: #104027 - New ViewHelper argument "module" to define module context 

See forge#104027

Description 

A new optional argument module has been added to the following ViewHelpers:

  • <be:link.editRecord>
  • <be:link.newRecord>
  • <be:uri.editRecord>
  • <be:uri.newRecord>

The module argument allows integrators to explicitly define the backend module context used when opening the FormEngine to edit or create a record. When set, this module will be highlighted as active in the backend menu, providing better navigation context.

This is particularly useful in scenarios where the default context cannot be reliably inferred.

Usage Example 

Example usage of module argument
<be:link.editRecord table="tt_content" uid="{record.uid}" module="web_layout">
    Edit this content element
</be:link.editRecord>

<be:uri.newRecord table="custom_table" pid="123" module="web_list" />
Copied!

Impact 

When used, the module argument ensures a more accurate and predictable backend editing experience by controlling which module is marked as active when the FormEngine opens.

Important: #105244 - Updated default .htaccess template 

See forge#105244

Description 

When installing TYPO3 for the first time, a .htaccess file is added to the htdocs or public directory when running TYPO3 via an Apache web server.

In addition to several TYPO3-specific optimizations, this file mainly contains rules (using the "mod_rewrite" Apache module) that redirect all URL requests for non-existent files within a TYPO3 project to the main index.php entry point file.

For new installations, this file now contains updated configuration that can also be applied to existing TYPO3 setups to reflect the current default behavior.

Key changes:

  • URL requests within /_assets/ and /fileadmin/ are no longer redirected, as these directories contain resources either managed by TYPO3 or by editors.
  • The directory /_assets/ has been included since TYPO3 v12 in Composer-based installations and is now officially added.
  • The folder /uploads/ is no longer maintained by TYPO3 since v11 and is now removed from the default .htaccess configuration. This means that TYPO3 pages can now officially use the URL path /uploads.

It is recommended to apply these adjustments in existing TYPO3 installations as well, even for other web servers such as nginx or IIS, provided there is no custom usage of /_assets/ or /uploads/ (for example through a PSR-15 middleware, custom extension, or custom routing).

Apache example:

In Apache-based setups, look for this line:

RewriteRule ^(?:fileadmin/|typo3conf/|typo3temp/|uploads/) - [L]
Copied!

and replace it with:

RewriteRule ^(?:fileadmin/|typo3conf/|typo3temp/|_assets/) - [L]
Copied!

Important: #105310 - Create CHAR and BINARY as fixed-length columns 

See forge#105310

Description 

TYPO3 parses ext_tables.sql files into a Doctrine DBAL object schema to define a virtual database scheme, enriched with \TYPO3\CMS\Core\Schema\DefaultTcaSchema information for TCA managed tables and fields.

Fixed- and variable-length variants have been parsed in the past, but failed to flag the column as $fixed = true for the fixed-length database field types CHAR and BINARY. This resulted in the wrong creation of these columns as VARCHAR and VARBINARY, which is now corrected.

ext_tables.sql created as (before) created as (now)
CHAR(10) VARCHAR(10) CHAR(10)
VARCHAR(10) VARCHAR(10) VARCHAR(10)
BINARY(10) VARBINARY(10) BINARY(10)
VARBINARY(10) VARBINARY(10) VARBINARY(10)

Not all relational database management systems (RDBMS) behave the same way for fixed-length columns. Implementation differences need to be respected to ensure consistent query and data behaviour across all supported database systems.

Fixed-length CHAR 

Key difference between CHAR and VARCHAR

The main difference between CHAR and VARCHAR is how the database stores character data. CHAR, which stands for character, is a fixed-length data type. It always reserves a specific amount of storage space for each value, regardless of whether the actual data occupies that space entirely. For example, if a column is defined as CHAR(10) and the word apple is stored inside of it, it will still occupy 10 characters (not just 5). Unused characters are padded with spaces.

On the other hand, VARCHAR, short for variable character, is a variable-length data type. It only uses as much storage space as needed to store the actual data without padding. Thus, storing the word apple in a VARCHAR(10) column will only occupy 5 characters.

The main difference between PostgreSQL and MySQL, MariaDB or SQLite is that PostgreSQL also returns the padded spaces for values that do not fill the full defined length (for example, apple[space][space][space][space] [space]).

In addition, these padded spaces are respected in query conditions, sorting or calculations (such as concat()). These differences make a significant impact and must be considered when using CHAR fields.

Rule of thumb for fixed-length CHAR columns

  • Use only with guaranteed fixed-length values to avoid padding.
  • For 255 or more characters, VARCHAR or TEXT must be used.

More hints for fixed-length CHAR columns

  • Ensure that stored values are fixed-length (non-space characters), for example by using hash algorithms that produce fixed-length identifiers.
  • Ensure that query statements use trim or rightPad within WHERE, HAVING or SELECT operations when values are not guaranteed to be fixed-length.

Example of differences in behaviour of fixed-length CHAR types 

The following examples illustrate how different relational database management systems handle fixed-length CHAR values, and why the behaviour must be carefully considered when storing or querying such data.

Creating a fixed-length field 
Example ext_tables.sql defining a fixed-length tt_content field
CREATE TABLE `tt_content` (
    `some_label` CHAR(10) DEFAULT '' NOT NULL,
);
Copied!
Inserting example data 

Two example rows are added below: one value fits exactly 10 characters, the other uses only 6 characters.

Adding two example rows
<?php

use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
    ->getConnectionForTable('tt_content');
// adding a value with 10 chars
$queryBuilder->insert(
    'tt_content',
    [
        'some_label' => 'some-label',
    ],
    [
        'some_label' => Connection::PARAM_STR,
    ],
);
// adding a value with only 6 chars
$queryBuilder->insert(
    'tt_content',
    [
        'some_label' => 'label1',
    ],
    [
        'some_label' => Connection::PARAM_STR,
    ],
);
Copied!
Retrieving the records 
Get all records from table
<?php

use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
  ->getQueryBuilderForTable('tt_content');
$rows = $queryBuilder
    ->select('uid', 'some_label')
    ->from('tt_content')
    ->executeQuery()
    ->fetchAllAssociative();
Copied!
Result differences across platforms 

The returned values differ depending on the database platform.

Result rows MySQL, MariaDB or SQLite
<?php

$rows = [
    [
        'uid' => 1,
        'some_label' => 'some-label',
    ],
    [
        'uid' => 2,
        'some_label' => 'label1',
    ],
];
Copied!
Result rows with PostgreSQL
<?php

$rows = [
    [
        'uid' => 1,
        'some_label' => 'some-label',
    ],
    [
        'uid' => 2,
        // PostgreSQL applies the fixed length to the value directly,
        // filling it up with spaces
        'some_label' => 'label1    ',
    ],
];
Copied!
Result rows difference between database platforms (commented)
 <?php

 $rows = [
     [
         'uid' => 1,
         'some_label' => 'some-label',
     ],
     [
         'uid' => 2,
-        'some_label' => 'label1',      // MySQL, MariaDB, SQLite
+        'some_label' => 'label1    ',  // PostgreSQL
     ],
 ];
Copied!
Querying trimmed versus padded values 

Using a trimmed value in a WHERE clause can match the row, but the returned value will differ depending on the database platform.

Retrieve with trimmed value
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
  ->getQueryBuilderForTable('tt_content');
$rows = $queryBuilder
    ->select('uid', 'some_label')
    ->from('tt_content')
    ->where(
        $queryBuilder->eq(
            'some_label',
            $queryBuilder->createNamedParameter('label1'), // trimmed value!
        ),
    )
    ->executeQuery()
    ->fetchAllAssociative();

// $rows contains the record for
// PostgreSQL: $rows = [['uid' => 2, 'some_label' => 'label1    ']];
// Others....: $rows = [['uid' => 2, 'some_label' => 'label1']];
Copied!
Enforcing trimmed values in queries 
Retrieve with enforced trimmed value.
<?php

use Doctrine\DBAL\Platforms\TrimMode;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
  ->getQueryBuilderForTable('tt_content');
$rows = $queryBuilder
    ->select('uid')
    ->addSelectLiteral(
        $queryBuilder->expr()->as(
            $queryBuilder->expr()->trim(
                'fixed_title',
                TrimMode::TRAILING,
                ' '
            ),
            'fixed_title',
        ),
    )
    ->from('tt_content')
    ->where(
        $queryBuilder->eq(
            'some_label',
            $queryBuilder->createNamedParameter('label1'),
        ),
    )
    ->executeQuery()
    ->fetchAllAssociative();

// $rows contains the record for
// PostgreSQL: $rows = [['uid' => 2, 'some_label' => 'label1']];
// Others....: $rows = [['uid' => 2, 'some_label' => 'label1']];
// and ensures the same content across all supported database systems.
Copied!
Querying space-padded values in PostgreSQL 
Retrieve with space-padded value for PostgreSQL does not retrieve the record
<?php

use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;

// PostgreSQL specific query!

$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
  ->getQueryBuilderForTable('tt_content');
$rows = $queryBuilder
    ->select('uid', 'some_label')
    ->from('tt_content')
    ->where(
        $queryBuilder->eq(
            'some_label',
            $queryBuilder->createNamedParameter('label1    '), // untrimmed value!
        ),
    )
    ->executeQuery()
    ->fetchAllAssociative();

// $rows === []
Copied!

Additional tools for consistent behaviour 

Additional ExpressionBuilder methods can be used to ensure consistent behaviour across all supported platforms:

  • \TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder::trim()
  • \TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder::rightPad()

Recommendation 

CHAR and BINARY fields can be used for storage or performance adjustments, but only when composed data and queries account for the differences between database systems.

Otherwise, the safe bet is to consistently use VARCHAR and VARBINARY column types.

Important: #105538 - Plugin subtypes removed: Changes to configurePlugin() and TCA handling 

See forge#105538

Description 

Due to the removal of the plugin content element "Plugin" (list) and the corresponding plugin subtype field list_type, the fifth parameter $pluginType of ExtensionUtility::configurePlugin() is now unused and can be omitted. It is only kept for backwards compatibility.

Be aware that passing any value other than CType will trigger an \InvalidArgumentException.

Please also note that due to the removal of the list_type field in tt_content, passing list_type as the second parameter $field to ExtensionManagementUtility::addTcaSelectItemGroup() will now, as for any other non-existent field, trigger a \RuntimeException.

Important: #106192 - Add 'center' and 'font' to YAML processing removeTags 

See forge#106192

Description 

The HTML tags <font> and <center> have been officially deprecated for some time: see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/font and https://developer.mozilla.org/en-US/docs/Web/HTML/Element/center.

The default YAML processing configuration file EXT:rte_ckeditor/Configuration/RTE/Processing.yaml has been changed to remove these HTML tags <font> and <center> by default when saving RTE field contents to the database.

This new default is adjusted with the option processing.HTMLparser_db.removeTags, which now also lists these two tags.

A stored input like <p><font face="Arial">My text</font></p> will - when saved - be changed to <p>My text</p>.

Affected installations 

All installations having <font> and <center> stored in their database fields, and where no custom RTE YAML configuration is in place that allows these tags.

Please note that due to issue forge#104839, this removeTags option was never properly applied previously, so the chances are that an installation never had output for font and center properly working anyway.

Also, note that CKEditor by default uses <span style="..."> tags to apply font formatting when using the Full preset.

Thus, the real-life impact should be low, but for legacy installations, you may want to convert existing data to replace font/html tags with their appropriate modern counterparts.

Migration 

Either accept the removal of these tags and use specific HTML tags like <span> and <div> to apply formatting.

Or adapt the RTE processing via TypoScript/YAML configuration to not have center and font in the processing.HTMLparser_db.removeTags list.

If the tags center and font have been configured via the editor.conf.style.definitions YAML option (not set by default), CKEditor would allow the use of these tags, but they will now be removed both when saving or when being rendered in the frontend. So these style definitions should be removed and/or adapted to <span style="..."> configurations.

Important: #106532 - Changed database storage format for Scheduler Tasks 

See forge#106532

Description 

TYPO3's system extension scheduler has stored its tasks to be executed in a PHP-serialized format in the database since its inception.

This has led to many problems, for example, when changing a class property to be fully typed, when a class name changed to use PHP 5 namespaces, or when renaming a class or a class property.

This has now changed: the task object now stores the "tasktype" (typically the class name) and its options in a "parameters" JSON-encoded value, as well as the execution details (database field execution_details) in separate fields of the database table tx_scheduler_task. This way, the task object can be reconstituted through a properly defined API, avoiding issues in the future.

All existing tasks are compatible with the new internal format. An upgrade wizard ensures that the previously serialized objects are transferred into the new format. If this wizard does not disappear after being executed, it means there are tasks that failed to migrate and may need manual inspection or recreation. Inspect all tasks in tx_scheduler_task where the "tasktype" column is empty. The old serialized data format is somewhat human-readable (or can be inspected with PHP deserializers), so recreating a task with its former configuration options should be possible.

Please note that this upgrade step needs to be performed in the context of TYPO3 v14. Running the wizard in future TYPO3 versions may not succeed due to changes in the task objects.

Important: #106649 - Default language binding in layout module 

See forge#106649

Description 

The Content > Layout module now always uses default language binding ( mod.web_layout.defLangBinding) when displaying localized content in language comparison mode.

Default language binding makes editing translations easier: Editors can see what they are translating next to the default language. It also prevents confusion when localizations are incomplete. Editors can directly see which content is not yet translated. This improves the user experience in the backend for multilingual sites, which was also a result of recent "Jobs To Be Done" (JTBD) interviews.

Impact 

Editors will now always see the content elements next to each other within the Content > Layout module when in language comparison mode.

Migration 

Because default language binding is now always enabled, the previous Page TSconfig setting mod.web_layout.defLangBinding is not evaluated and can therefore be removed.

Important: #106656 - Allow DEFAULT NULL for varchar fields 

See forge#106656

Description 

In TCA, if an input field is configured to be nullable via 'nullable' => true, the database migration now respects this and creates or updates existing fields with DEFAULT NULL.

In Extbase, this may cause issues if properties and their accessor methods are not properly declared as nullable. Therefore, this change is introduced only in TYPO3 v14.

Example:

Example properly implementing a nullable property
<?php

declare(strict_types=1);

namespace Vendor\MyExtension\Domain\Model;

use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class MyExtbaseEntity extends AbstractEntity
{
    protected ?string $title;

    public function getTitle(): ?string
    {
        return $this->title;
    }

    public function setTitle(?string $title): void
    {
        $this->title = $title;
    }
}
Copied!

As stated above, this automatic detection is not provided in TYPO3 versions earlier than 14.0. Using DEFAULT NULL can be enforced via an extension's ext_tables.sql instead:

CREATE TABLE tx_myextension_table (
    title varchar(255) DEFAULT NULL
);
Copied!

Important: #106947 - Allow extensions to ship upgrade wizards without requiring EXT:install 

See forge#106947

Description 

EXT:install is no longer installed per default in composer based minimal installations.

However, this advantage could often not be utilised sensibly, since extensions still needed to require EXT:install as a dependency in order to ship upgrade wizards, because the implemented interfaces needed to be available.

The basic required interfaces and the PHP attribute are now moved to the EXT:core namespace. The old counterparts are deprecated with Deprecation: #106947 - Move upgrade wizard related interfaces and attribute to EXT:core but are still provided by EXT:install for the time being.

The following changes are required (when requiring TYPO3 14.0+) to make EXT:install optional:

  • Let custom upgrade wizards implement \TYPO3\CMS\Core\Upgrades\UpgradeWizardInterface .
  • Implement custom list-type to CType migrations extending \TYPO3\CMS\Core\Upgrades\AbstractListTypeToCTypeUpdate .
  • Use the attribute \TYPO3\CMS\Core\Attribute\UpgradeWizard to register custom upgrade wizards.

Extension authors supporting two major TYPO3 versions with one extension version can follow these strategies:

  • TYPO3 v13 and v14: Use deprecated EXT:install interfaces and PHP Attribute and require EXT:install as mandatory dependency, or add it as a suggestion to allow the decision to be made on project-level.
  • TYPO3 v14: Switch to EXT:core interfaces and the new PHP Attribute.

See Deprecation: #106947 - Move upgrade wizard related interfaces and attribute to EXT:core for details about moved interfaces, PHP attribute and a migration example.

Important: #107328 - $GLOBALS['TCA'] in base TCA files 

See forge#107328

Description 

The backward compatibility for using $GLOBALS['TCA'] in base TCA files has been removed. Base TCA files are the first TCA files loaded by the Core and do not have the fully loaded TCA available yet. Until now, this worked because the Core temporarily populated this global array during the loading process.

Note that the usage of $GLOBALS['TCA'] in base TCA files was never explicitly allowed or disallowed, only discouraged. It worked only due to internal system knowledge by the user, for example, knowing that the loading order of extensions affects when those files are loaded. The only place this array should be used is inside TCA/Overrides/*.php files. For all other cases, the TcaSchema should be preferred.

Migration 

In the uncommon case that you find usages of $GLOBALS['TCA'] in base TCA files, move that access to TCA/Overrides/*.php files instead.

Important: #107399 - Add more common file types to mediafile_ext 

See forge#107399

Description 

The default configuration for $GLOBALS['TYPO3_CONF_VARS']['SYS']['mediafile_ext'] has been extended with additional formats commonly uploaded to web-based CMS systems such as TYPO3:

  • 3gp
  • aac
  • aif
  • avif
  • heic
  • ico
  • m4a
  • m4v
  • mov
  • psd

This list is extended only for systems where the configuration option has not already been customized.

Adding these formats allows files with these extensions to be uploaded into the file list (and used in TCA fields with "common-media-types") when the security flag security.system.enforceAllowedFileExtensions is enabled.

This may be a relevant change if these file types are now used in records or content elements that do not expect such files for further operations (for example, in frontend display).

Integrators must ensure that all uploaded file types are handled appropriately (for example, embedding "mov" as a video, "ico" as an image, or "psd" as a download) or have a suitable fallback.

The <f:media> ViewHelper, for example, iterates over possible Renderer implementations that can handle several of these file types (and their MIME types) and attempts to render a file as an image as a fallback.

Important: #107536 - Install Tool now adapts to backend login routing 

See forge#107536

Description 

The Install Tool now integrates with TYPO3's backend routing system instead of using a separate typo3/install.php file. This modernization improves consistency while maintaining full backward compatibility.

If the TYPO3 installation is not working properly, the Install Tool can now be accessed via the ?__typo3_install parameter, ensuring administrators can rely on it for system maintenance and recovery.

Impact 

For Administrators:

All existing workflows continue to work without changes. However, the Install Tool is now accessible via:

  • The __typo3_install parameter (for example, https://example.com/?__typo3_install)
  • Backend routes such as /typo3/install and /typo3/install.php still work. If the $GLOBALS['TYPO3_CONF_VARS']['BE']['entryPoint'] is set, the Install Tool adapts accordingly.

System Recovery:

When the TYPO3 installation is not set up or not working properly, the magic __typo3_install parameter still redirects to the installer or maintenance tool as before, ensuring administrators can always access system recovery tools.

Technical Benefits:

The Install Tool now uses the same routing infrastructure as the rest of the TYPO3 backend, creating a more unified and maintainable architecture while supporting the long-term goal of simplifying TYPO3's directory structure.

Migration 

No migration is required. All existing documentation, scripts, and workflows using the Install Tool continue to function without modification.

Important: #107629 - Reference Index check moved to Install Tool 

See forge#107629

Description 

The Check and Update Reference Index functionality has been moved from the System > DB Check module (EXT:lowlevel) to the Maintenance section of the Install Tool (EXT:install) in the top level module now called System.

This change makes this essential administrative tool more accessible and better organized alongside other common maintenance tasks such as database comparison, cache management, and folder structure checks.

Why this change? 

The Reference Index is a critical system component that tracks relationships between records in TYPO3. Checking and updating is a routine maintenance task that administrators perform regularly, similar to:

  • Analyzing database structure
  • Clearing caches
  • Checking folder permissions

Previously, this functionality was hidden in the DB Check module of EXT:lowlevel, which made it:

  • Hard to discover: Administrators had to know where to look
  • Inconsistent: Other maintenance tools were in the Install Tool
  • Less accessible: Required an additional system extension

Impact 

For Administrators:

The Reference Index check and update functionality is now directly available in the Install Tool under Maintenance > Check and Update Reference Index.

Key benefits:

  • Better visibility: Found alongside other maintenance tools
  • No extra dependencies: Works out-of-the-box without EXT:lowlevel
  • Consistent location: All system maintenance tasks in one place
  • Faster access: Direct access via the Install Tool
  • Same functionality: Check and update operations work exactly as before

CLI Access:

The command-line interface remains unchanged and continues to work as before:

# Check reference index
vendor/bin/typo3 referenceindex:update --check

# Update reference index
vendor/bin/typo3 referenceindex:update
Copied!

Migration 

No migration is required. System Maintainers should use the new location in the System > Maintenance section instead of the System > DB Check module.

The functionality works identically to the previous implementation.

Important: #107735 - Internal methods removed from ResourceFactory 

See forge#107735

Description 

The following internal methods have been removed from \TYPO3\CMS\Core\Resource\ResourceFactory :

  • getDefaultStorage()
  • getStorageObject()
  • createFolderObject()
  • getFileObjectByStorageAndIdentifier()

These methods were marked as @internal and are replaced by using \TYPO3\CMS\Core\Resource\StorageRepository directly for better separation of concerns. However, some of these methods might have been used in custom extensions despite being marked as internal.

Migration 

Instead of using the removed methods from ResourceFactory, use the appropriate methods from StorageRepository or direct access to ResourceStorage:

// Before
$defaultStorage = $resourceFactory->getDefaultStorage();
$storage = $resourceFactory->getStorageObject($uid);
$folder = $resourceFactory->createFolderObject($storage, $identifier, $name);
$file = $resourceFactory->getFileObjectByStorageAndIdentifier($storage, $fileIdentifier);

// After
$defaultStorage = $storageRepository->getDefaultStorage();
$storage = $storageRepository->getStorageObject($uid);
$folder = $storage->getFolder($identifier);
$file = $storage->getFileByIdentifier($fileIdentifier);
Copied!

Important: #107789 - TCA tab labels consolidated into core.form.tabs 

See forge#107789

Description 

To improve consistency and maintainability of TCA tab labels across TYPO3 Core, commonly used tab labels from various extensions have been consolidated into the central EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf file.

This consolidation allows for better reusability and ensures consistent translation of common tab labels across all core extensions. It also makes it easier for extension developers to use standardized tab names.

New labels available in locallang_tabs.xlf 

The following new tab labels are now available and should be used via the core.form.tabs: prefix:

  • core.form.tabs:audio - "Audio"
  • core.form.tabs:video - "Video"
  • core.form.tabs:camera - "Camera"
  • core.form.tabs:permissions - "Permissions"
  • core.form.tabs:mounts - "Mounts"
  • core.form.tabs:personaldata - "Personal Data"

Previously existing labels (already migrated in core):

  • core.form.tabs:general - "General"
  • core.form.tabs:access - "Access"
  • core.form.tabs:categories - "Categories"
  • core.form.tabs:notes - "Notes"
  • core.form.tabs:language - "Language"
  • core.form.tabs:extended - "Extended"
  • core.form.tabs:appearance - "Appearance"
  • core.form.tabs:behaviour - "Behavior"
  • core.form.tabs:metadata - "Metadata"
  • core.form.tabs:resources - "Resources"
  • core.form.tabs:seo - "SEO"
  • core.form.tabs:socialmedia - "Social Media"
  • core.form.tabs:options - "Options"

Migrated extension-specific labels 

The following extension-specific tab labels have been migrated to the consolidated labels file and are marked as unused since TYPO3 v14.0 with the attribute x-unused-since="14.0" in the corresponding XLF files.

EXT:filemetadata

  • LLL:EXT:filemetadata/Resources/Private/Language/locallang_tca.xlf:tabs.metadata core.form.tabs:metadata
  • LLL:EXT:filemetadata/Resources/Private/Language/locallang_tca.xlf:tabs.camera core.form.tabs:camera
  • LLL:EXT:filemetadata/Resources/Private/Language/locallang_tca.xlf:tabs.audio core.form.tabs:audio
  • LLL:EXT:filemetadata/Resources/Private/Language/locallang_tca.xlf:tabs.video core.form.tabs:video

EXT:seo

  • LLL:EXT:seo/Resources/Private/Language/locallang_tca.xlf:pages.tabs.seo core.form.tabs:seo
  • LLL:EXT:seo/Resources/Private/Language/locallang_tca.xlf:pages.tabs.socialmedia core.form.tabs:socialmedia

EXT:core - Backend Users

  • LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:be_users.tabs.personal_data core.form.tabs:personaldata
  • LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:be_users.tabs.mounts_and_workspaces core.form.tabs:mounts
  • LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:be_users.tabs.options core.form.tabs:options

EXT:core - Backend User Groups

  • LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:be_groups.tabs.mounts_and_workspaces core.form.tabs:mounts
  • LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:be_groups.tabs.options core.form.tabs:options

EXT:frontend - Frontend Users

  • LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:fe_users.tabs.personalData core.form.tabs:personaldata

Affected installations 

Custom extensions using TCA configurations may benefit from using the new consolidated tab labels instead of creating their own labels for common tab names.

Extensions that were using any of the migrated extension-specific labels listed above will continue to work in TYPO3 v14.0, but should migrate to the consolidated labels. The old labels will be removed in TYPO3 v15.0.

Migration 

For custom extensions, consider using the consolidated core.form.tabs: labels instead of creating custom labels for common tab names.

Example migration for extensions using old labels:

File metadata tabs

// Before
'--div--;LLL:EXT:filemetadata/Resources/Private/Language/locallang_tca.xlf:tabs.metadata'

// After
'--div--;core.form.tabs:metadata'
Copied!

User and group tabs

// Before
'--div--;LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:be_users.tabs.personal_data'
'--div--;LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:fe_users.tabs.personalData'

// After
'--div--;core.form.tabs:personaldata'
Copied!

Using consolidated labels in custom extensions

// Example: Custom TCA using consolidated labels
'types' => [
    '1' => [
        'showitem' => '
            --div--;core.form.tabs:general,
                title, description,
            --div--;core.form.tabs:metadata,
                author, keywords,
            --div--;core.form.tabs:access,
                hidden, starttime, endtime,
            --div--;core.form.tabs:categories,
                categories,
            --div--;core.form.tabs:extended,
        ',
    ],
],
Copied!

New labels available in palettes.xlf 

In addition to tab labels, commonly used palette labels have also been consolidated into the central EXT:core/Resources/Private/Language/Form/palettes.xlf file.

The following palette labels are now available via the core.form.palettes: prefix:

  • core.form.palettes:general - "General"
  • core.form.palettes:account - "Account"
  • core.form.palettes:authentication - "Authentication"
  • core.form.palettes:permission_languages - "Language permissions"
  • core.form.palettes:permission_general - "General permissions"
  • core.form.palettes:permission_specific - "Specific permissions"
  • core.form.palettes:standard - "Page"
  • core.form.palettes:title - "Title"
  • core.form.palettes:visibility - "Visibility"
  • core.form.palettes:access - "Publish Dates and Access Rights"
  • core.form.palettes:abstract - "Abstract"
  • core.form.palettes:metatags - "Meta Tags"
  • core.form.palettes:editorial - "Editorial"
  • core.form.palettes:page_layout - "Page Layout"
  • core.form.palettes:use_as_container - "Use as Container"
  • core.form.palettes:replace - "Replace Content"
  • core.form.palettes:links - "Links to this Page"
  • core.form.palettes:caching - "Caching"
  • core.form.palettes:language - "Language"
  • core.form.palettes:miscellaneous - "Miscellaneous"
  • core.form.palettes:media - "Files"
  • core.form.palettes:storage - "Storage Page"
  • core.form.palettes:config - "Configuration"
  • core.form.palettes:headers - "Headlines"
  • core.form.palettes:header - "Header"
  • core.form.palettes:content_layout - "Content Element Layout"
  • core.form.palettes:media_behaviour - "Media Behaviour"
  • core.form.palettes:accessibility - "Accessibility"
  • core.form.palettes:downloads_layout - "Downloads Layout"
  • core.form.palettes:table_layout - "Table Layout"
  • core.form.palettes:links_appearance - "Links"
  • core.form.palettes:settings_gallery - "Gallery Settings"
  • core.form.palettes:media_adjustments - "Media Adjustments"
  • core.form.palettes:metrics - "Metrics"
  • core.form.palettes:geolocation - "Geo Location"
  • core.form.palettes:contentdate - "Content Date"
  • core.form.palettes:gps - "GPS"
  • core.form.palettes:seo - "General SEO settings"
  • core.form.palettes:robots - "Robot instructions"
  • core.form.palettes:opengraph - "Open Graph (Facebook)"
  • core.form.palettes:twittercards - "X / Twitter Cards"
  • core.form.palettes:canonical - "Canonical"
  • core.form.palettes:sitemap - "Sitemap"
  • core.form.palettes:additional - "Additional configuration"

Migrated palette labels 

The following palette labels have been migrated to use core.form.palettes:* and are marked as unused since TYPO3 v14.0 (attribute x-unused-since="14.0" in XLF files):

EXT:core - Backend Users & Groups:

  • be_users.palettes.account, be_users.palettes.authentication, be_users.palettes.permissionLanguages
  • be_groups.palettes.authentication, be_groups.palettes.permissionGeneral, be_groups.palettes.permissionLanguages, be_groups.palettes.permissionSpecific

EXT:frontend - Pages & Content Elements:

  • pages.palettes.* (17 labels: standard, title, visibility, access, abstract, metatags, editorial, layout, module, replace, links, caching, language, miscellaneous, media, storage, config)
  • palette.* in tt_content (13 labels: general, headers, header, visibility, access, frames, imagelinks, image_accessibility, uploads_layout, table_layout, appearanceLinks, gallerySettings, mediaAdjustments)

EXT:filemetadata:

  • palette.* (6 labels: metrics, geo_location, visibility, content_date, accessibility, gps)

EXT:seo:

  • pages.palettes.* (6 labels: seo, robots, opengraph, twittercards, canonical, sitemap)

Important: #107848 - DataHandler properties userid and admin removed 

See forge#107848

Description 

The internal properties \TYPO3\CMS\Core\DataHandling\DataHandler::$userid and \TYPO3\CMS\Core\DataHandling\DataHandler::$admin have been removed.

These properties contained information that is already available through the BE_USER property and were therefore redundant.

Impact 

Accessing these properties directly will result in a fatal PHP error.

Affected installations 

All installations with extensions that access the following properties:

  • \TYPO3\CMS\Core\DataHandling\DataHandler::$userid
  • \TYPO3\CMS\Core\DataHandling\DataHandler::$admin

While these properties were marked as @internal, they have been commonly used by extensions, especially the $admin property.

Migration 

Replace any usage of these properties with the appropriate methods from the BE_USER property.

For $userid 

// Before:
$userId = $dataHandler->userid;

// After:
$userId = $dataHandler->BE_USER->getUserId();
Copied!

For $admin 

// Before:
if ($dataHandler->admin) {
    // do something
}

// After:
if ($dataHandler->BE_USER->isAdmin()) {
    // do something
}
Copied!

14.x Changes by type 

This lists all changes to the TYPO3 Core of minor versions grouped by their type.

Table of contents

Breaking Changes 

Features 

Deprecations 

Important notes 

ChangeLog v13 

Every change to the TYPO3 Core which might affect your site is documented here.

Also available 

13.4.x Changes 

Table of contents

Breaking Changes 

None since TYPO3 v13.4.0 LTS release.

Features 

Deprecation 

Important 

Feature: #105638 - Modify fetched page content 

See forge#105638

Description 

With forge#103894 the new data processor PageContentFetchingProcessor has been introduced, to allow fetching page content based on the current page layout, taking the configured SlideMode into account.

Fetching content has previously mostly been done via the Content content object. A common example looked like this:

page.20 = CONTENT
page.20 {
    table = tt_content
    select {
        orderBy = sorting
        where = colPos=0
    }
}
Copied!

As mentioned in the linked changelog, using the page-content data processor, this can be simplified to:

page.20 = page-content
Copied!

This however reduces the possibility to modify the select configuration (SQL statement), used to define which content should be fetched, as this is automatically handled by the data processor. However, there might be some use cases in which the result needs to be adjusted, e.g. to hide specific page content, like it's done by EXT:content_blocks for child elements. For such use cases, the new PSR-14 AfterContentHasBeenFetchedEvent has been introduced, which allows to manipulate the list of fetched page content.

The following member properties of the event object are provided:

  • $groupedContent: The fetched page content, grouped by their column - as defined in the page layout
  • $request: The current request, which can be used to e.g. access the page layout in question

Example 

The event listener class, using the PHP attribute #[AsEventListener] for registration, removes some of the fetched page content elements based on specific field values.

my_extension/Classes/EventListener/MyEventListener.php
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Frontend\Event\AfterContentHasBeenFetchedEvent;

final class MyEventListener
{
    #[AsEventListener]
    public function removeFetchedPageContent(AfterContentHasBeenFetchedEvent $event): void
    {
        foreach ($event->groupedContent as $columnIdentifier => $column) {
            foreach ($column['records'] ?? [] as $key => $record) {
                if ($record->has('parent_field_name') && (int)($record->get('parent_field_name') ?? 0) > 0) {
                    unset($event->groupedContent[$columnIdentifier]['records'][$key]);
                }
            }
        }
    }
}
Copied!

Impact 

Using the new PSR-14 AfterContentHasBeenFetchedEvent, it's possible to manipulate the page content, which has been fetched by the PageContentFetchingProcessor, based on the page layout and corresponding columns configuration.

Important: #92187 - Evaluation of incoming HTTP Header X-Forwarded-Proto 

See forge#92187

Description 

When running TYPO3 behind a reverse proxy, the site owner needs to set two TYPO3 settings.

$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyHeaderMultiValue'] = 'first';
$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'] = '{ip-of-the-reverse-proxy}';
Copied!

At this point it is not known if the request between the client (the actual web browser for example) and the reverse proxy was made via HTTP or HTTPS, mainly because TYPO3 only evaluated the information from the reverse proxy to TYPO3 - which was typically faked on the TYPO3's webserver by setting "HTTPS=on" (for example via .htaccess file). In a typical setup, the communication between the reverse proxy and TYPO3's webserver is done via HTTP and irrelevant for TYPO3.

When the site owner knows that the reverse proxy acts as a SSL termination point and only communicates via https to the client, the $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxySSL'] option can be set, to identify all reverse proxy IPs that ensure a secure connection between client and reverse proxy.

In case, it is not known, and reverseProxyPrefixSSL is not in use, but reverseProxyIP is in use, the incoming HTTP header X-Forwarded-Proto is now evaluated to determine if the request was made, if the header is sent.

If it is NOT sent, TYPO3 will assume to detect a secure connection between SSL information as before via various other HTTP Headers or server configuration settintgs.

Important: #103140 - Allow to configure rate limiters in Message consumer (Symfony Messenger) 

See forge#103140

Description 

This change introduces missing configuration options for Symfony Messenger-based rate limiters.

A rate limiter controls how frequently a specific event (e.g., HTTP request or login attempt) is allowed to occur. It acts as a safeguard to prevent services from being overwhelmed — either accidentally or intentionally — thus helping to maintain their availability.

Rate limiters are also useful for controlling internal or outbound processes, such as limiting the simultaneous processing of messages.

More information about the rate limiter is available in the Symfony Rate Limiter component documentation.

Usage 

Configure a rate limiter per queue 

Rate limiters can be defined in your service configuration EXT:yourext/Configuration/Services.yaml. The name specified in the settings is resolved to a service tagged with messenger.rate_limiter and the corresponding identifier.

Example Configuration:

EXT:yourext/Configuration/Services.yaml
messenger.rate_limiter.demo:
  class: 'Symfony\Component\RateLimiter\RateLimiterFactory'
  arguments:
    $config:
      id: 'demo'
      policy: 'sliding_window'
      limit: '100'
      interval: '60 seconds'
    $storage: '@Symfony\Component\RateLimiter\Storage\InMemoryStorage'
  tags:
    - name: 'messenger.rate_limiter'
      identifier: 'demo'

messenger.rate_limiter.default:
  class: 'Symfony\Component\RateLimiter\RateLimiterFactory'
  arguments:
    $config:
      id: 'default'
      policy: 'sliding_window'
      limit: '100'
      interval: '60 seconds'
    $storage: '@Symfony\Component\RateLimiter\Storage\InMemoryStorage'
  tags:
    - name: 'messenger.rate_limiter'
      identifier: 'default'
Copied!

Important: #104477 - Remove hyphen prefix from sys_log's data field entry 

See forge#104477

Description 

The \TYPO3\CMS\Core\Log\Writer\DatabaseWriter is used to write logs into the database table sys_log. Additional log information data is persisted in the field data and has been prefixed with a - until now. As this makes it harder to parse the data, which is JSON-encoded anyway, the prefix has been removed.

Beware that existing log entries are not migrated automatically. This leads to a mixed structure in the database table until old records are cleaned. (TYPO3 itself does not interpret the content of the field.)

Important: #105310 - Create CHAR and BINARY as fixed-length columns 

See forge#105310

Description 

TYPO3 parses ext_tables.sql files into a Doctrine DBAL object schema to define a virtual database scheme, enriched with DefaultTcaSchema information for TCA-managed tables and fields.

Fixed and variable length variants have been parsed already in the past, but missed to flag the column as $fixed = true for the fixed-length database field types CHAR and BINARY. This resulted in the wrong creation of these columns as VARCHAR and VARBINARY, which is now corrected.

ext_tables.sql created as (before) created as (now)
CHAR(10) VARCHAR(10) CHAR(10)
VARCHAR(10) VARCHAR(10) VARCHAR(10)
BINARY(10) VARBINARY(10) BINARY(10)
VARBINARY(10) VARBINARY(10) VARBINARY(10)

Not all database systems (RDBMS) act the same way for fixed-length columns. Implementation differences need to be respected to ensure the same query/data behaviour across all supported database systems.

Fixed-length CHAR 

Key Difference Between CHAR and VARCHAR

The main difference between CHAR and VARCHAR is how the database stores character data in a database. CHAR, which stands for character, is a fixed-length data type, meaning it always reserves a specific amount of storage space for each value, regardless of whether the actual data occupies that space entirely. For example, if a column is defined as CHAR(10) and the word apple is stored inside of it, it will still occupy 10 characters worth of space (not just 5). Unusued characters are padded with extra spaces.

On the other hand, VARCHAR, short for variable character, is a variable-length data type. It only uses as much storage space as needed to store the actual data without padding. So, storing the word apple in a VARCHAR(10) column will only occupy 5 characters worth of space, leaving the remaining table row space available for other data.

The main difference from PostgreSQL to MySQL/MariaDB/SQLite is: PostgreSQL also returns the filler-spaces for a value not having the column length (returning apple[space][space][space][space][space]).

On top of that, the filled-up spaces are also respected for query conditions, sorting or data calculations ( concat() for example). These two facts makes a huge difference and must be carefully taken into account when using CHAR field.

Rule of thumb for fixed-length CHAR columns

  • Only use with ensured fixed-length values (so that no padding occurs).
  • For 255 or more characters VARCHAR or TEXT must be used.

More hints for fixed-length CHAR columns

  • Ensure to write fixed-length values for CHAR (non-space characters), for example use hash algorithms which produce fixed-length hash identifier values.
  • Ensure to use query statements to trim OR rightPad the value within WHERE, HAVING or SELECT operations, when values are not guaranteed to contain fixed-length values.

    Helper \TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder expressions can be used, for example \TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder->trim() or \TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder->rightPad() to.

  • Usage of CHAR must be avoided when using the column with the Extbase ORM, because fixed-value length cannot be ensured due to the lack of using trim/rightPad within the ORM generated queries. Only with ensured fixed-length values, it is usable with Extbase ORM.
  • Cover custom queries extensively with functional tests executed against all supported database platforms. Code within public extensions should ensure to test queries and their operations against all officially TYPO3-supported database platforms.

Example of difference in behaviour of fixed-length CHAR types 

Example ext_tables.sql defining a fixed-length tt_content field
CREATE TABLE `tt_content` (
    `some_label` CHAR(10) DEFAULT '' NOT NULL,
);
Copied!

Now, add some data. One row which fits exactly to 10 characters, and one row that only uses 6 characters:

Adding two example rows
<?php

use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
    ->getConnectionForTable('tt_content');
// adding a value with 10 chars
$queryBuilder->insert(
    'tt_content',
    [
        'some_label' => 'some-label',
    ],
    [
        'some_label' => Connection::PARAM_STR,
    ],
);
// adding a value with only 6 chars
$queryBuilder->insert(
    'tt_content',
    [
        'some_label' => 'label1',
    ],
    [
        'some_label' => Connection::PARAM_STR,
    ],
);
Copied!

Now see the difference in retrieving these records:

Get all records from table
<?php

use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
  ->getQueryBuilderForTable('tt_content');
$rows = $queryBuilder
    ->select('uid', 'some_label')
    ->from('tt_content')
    ->executeQuery()
    ->fetchAllAssociative();
Copied!

Depending on the used database platform, the retrieved rows would contain these strings:

Result rows MySQL, MariaDB or SQLite
<?php

$rows = [
    [
        'uid' => 1,
        'some_label' => 'some-label',
    ],
    [
        'uid' => 2,
        'some_label' => 'label1',
    ],
];
Copied!

but for PostgreSQL

Result rows with PostgreSQL
<?php

$rows = [
    [
        'uid' => 1,
        'some_label' => 'some-label',
    ],
    [
        'uid' => 2,
        // PostgreSQL applies the fixed length to the value directly,
        // filling it up with spaces
        'some_label' => 'label1    ',
    ],
];
Copied!

or as a diff to make this even more visible:

Result rows difference between database platforms (commented)
 <?php

 $rows = [
     [
         'uid' => 1,
         'some_label' => 'some-label',
     ],
     [
         'uid' => 2,
-        'some_label' => 'label1',      // MySQL, MariaDB, SQLite
+        'some_label' => 'label1    ',  // PostgreSQL
     ],
 ];
Copied!

To raise the awareness for problems on this topic, using the trimmed value inside a WHERE condition will match the record, but the returned value will be different from the value used in the condition:

Retrieve with trimmed value
<?php

use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
  ->getQueryBuilderForTable('tt_content');
$rows = $queryBuilder
    ->select('uid', 'some_label')
    ->from('tt_content')
    ->where(
        $queryBuilder->eq(
            'some_label',
            $queryBuilder->createNamedParameter('label1'), // trimmed value!
        ),
    )
    ->executeQuery()
    ->fetchAllAssociative();

// $rows contains the record for
// PostgreSQL: $rows = [['uid' => 2, 'some_label' => 'label1    ']];
// Others....: $rows = [['uid' => 2, 'some_label' => 'label1']];
Copied!
Retrieve with enforced trimmed value.
<?php

use Doctrine\DBAL\Platforms\TrimMode;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
  ->getQueryBuilderForTable('tt_content');
$rows = $queryBuilder
    ->select('uid')
    ->addSelectLiteral(
        $queryBuilder->expr()->as(
            $queryBuilder->expr()->trim(
                'fixed_title',
                TrimMode::TRAILING,
                ' '
            ),
            'fixed_title',
        ),
    )
    ->from('tt_content')
    ->where(
        $queryBuilder->eq(
            'some_label',
            $queryBuilder->createNamedParameter('label1'),
        ),
    )
    ->executeQuery()
    ->fetchAllAssociative();

// $rows contains the record for
// PostgreSQL: $rows = [['uid' => 2, 'some_label' => 'label1']];
// Others....: $rows = [['uid' => 2, 'some_label' => 'label1']];
// and ensures the same content across all supported database systems.
Copied!

On PostgreSQL, performing a query for a space-padded value will not actually return the expected row:

Retrieve with space-padded value for PostgreSQL does not retrieve the record
<?php

use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;

// PostgreSQL specific query!

$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
  ->getQueryBuilderForTable('tt_content');
$rows = $queryBuilder
    ->select('uid', 'some_label')
    ->from('tt_content')
    ->where(
        $queryBuilder->eq(
            'some_label',
            $queryBuilder->createNamedParameter('label1    '), // untrimmed value!
        ),
    )
    ->executeQuery()
    ->fetchAllAssociative();

// $rows === []
Copied!

Additional ExpressionBuilder methods can be used to ensure same behaviour on all platforms:

  • \TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder::trim()
  • \TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder::rightPad()

Recommendation 

CHAR and BINARY fields can be used (for storage or performance adjustments), but only when composed data and queries take care of database-system differences.

Otherwise, the "safe bet" is to consistently utilize VARCHAR and VARBINARY columns types.

Important: #105653 - Require a template filename in extbase module template rendering 

See forge#105653

Description 

With the introduction of the FluidAdapter in TYPO3 v13, the dependency between Fluid and Extbase has been decoupled. As part of this change, the behavior of the ModuleTemplate::renderResponse() and ModuleTemplate::render() methods has been adjusted.

The $templateFileName argument is now mandatory for the ModuleTemplate::renderResponse() and ModuleTemplate::render() methods. Previously, if this argument was not provided, the template was automatically resolved based on the controller and action names. Starting from TYPO3 13.4, calling these methods with an empty string or without a valid $templateFileName will result in an InvalidArgumentException.

Extensions using Extbase backend modules must explicitly provide the $templateFileName when calling these methods. Existing implementations relying on automatic template resolution need to be updated to prevent runtime errors.

Example:

Before:

$moduleTemplate->renderResponse();
Copied!

After:

$moduleTemplate->renderResponse('MyController/MyAction');
Copied!

Note, that it is already possible to explicitly provide the $templateFileName in TYPO3 12.4. It is therefore recommended to implement the new requirement for websites using TYPO3 12.4.

Important: #105703 - Premature end of script headers due to X-TYPO3-Cache-Tags 

See forge#105703

Description 

The X-TYPO3-Cache-Tags header is now split into multiple lines if it exceeds the maximum of 8000 characters. This change prevents premature end of script headers and ensures that the header is sent correctly, even if it contains a large number of cache tags.

Affected installations 

This change affects all TYPO3 installations that have $GLOBALS['TYPO3_CONF_VARS']['FE']['debug'] enabled and misusing the X-TYPO3-Cache-Tags header for anything else then debugging. If you have a large number of cache tags, the header is now split into multiple lines to avoid exceeding the maximum header size limit imposed by some web servers. As this header is for debugging purposes only, this does not effect any production environments.

Important: #106401 - Treat 0 as a defined value for nullable datetime fields 

See forge#106401

Description 

For nullable integer-based datetime fields, the value 0 now explicitly represents the Unix epoch time (1970-01-01T00:00:00Z) instead of being interpreted as an empty value by FormEngine.

Only an explicit null database value will be considered an empty value.

The default database schema that is generated from TCA has been adapted to generate datetime columns with DEFAULT NULL instead of DEFAULT 0 if they have been configured to be nullable.

Given the following TCA definition:

'columns' => [
    'mydatefield' => [
        'config' => [
            'type' => 'datetime',
            'nullable' => true,
        ],
    ],
],
Copied!

The previously generated SQL statement will be changed from DEFAULT 0 to DEFAULT NULL:

Nullable datetime schema before this change
`mydatefield` bigint(20) DEFAULT 0
Copied!
Nullable datetime schema after this change
`mydatefield` bigint(20) DEFAULT NULL
Copied!

Fields that have not been explicitly configured to be nullable are unaffected and will default to 0 as before.

Important: #106467 - Align Extbase DateTime handling to FormEngine and DataHandler 

See forge#106467

Description 

Extbase handling of \DateTimeInterface domain model properties has been aligned with the persistence and database value interpretation behavior of the TYPO3 Core Engine (FormEngine and DataHandler).

Since this change addresses bugs and value interpretation differences that existed since the introduction of Extbase and there are many workarounds in use, a feature flag 'extbase.consistentDateTimeHandling' is introduced which allows to enable the new behavior.

Existing TYPO3 v13 instances will use the old behavior by default and are advised to enable the new feature flag via InstallTool or via:

$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']['extbase.consistentDateTimeHandling'] = true;
Copied!

TYPO3 v14 (and new v13 instances) enable the consistent DateTime handling by default, but the feature can still be disabled manually, if needed for the time being.

There are four different behavioural changes that will be activated and are explained in the following sections.

Align persistence to database to match DataHandler algorithm 

Use the DataHandler algorithm for the mapping of DateTime objects to database values. This causes non-localtime timezone offsets in \DateTime objects (e.g. supplied by a frontend datepicker) to be respected for native datetime fields, like already done for integer based datetime fields. Note that the offset is not stored as-is, but mapped to PHP localtime, but the offset is no longer cropped off.

That means there is no need to force the server timezone on \DateTime objects before persisting an extbase model, since all dates will be normalized to localtime (for native datetime fields) or UTC (for interger based datetime fields) within the persistence layer.

Before:

public function setDatetime(\DateTime $datetime): void
{
    // Force local datetime zone in order to avoid
    // cropping non localtime offsets during persistence
    $datetime->setTimezone(
        new\DateTimeZone(date_default_timezone_get())
    );
    $this->datetime = $datetime;
}
Copied!

After:

public function setDatetime(\DateTime $datetime): void
{
    // No timezone enforcement needed, persistence layer will
    // persist correct point in time (UTC for integer, LOCALTIME for native
    // fields)
    $this->datetime = $datetime;
}
Copied!

Map date and datetime with named timezone instead of offset 

Extbase DataMapper converts dates of integer based database fields to \DateTime instances that use the current server date timezone (e.g., Europe/Berlin) and not just the time offset of the current server timezone (e.g., +01:00).

This prevents timezone shifts when modifying the resulting \DateTime object across daylight saving time boundaries.

Previous workarounds that explicitly added the server timezone for properties can be removed:

Before:

public function getDatetime(): ?\DateTime
{
    // object(DateTimeZone)#1 (2) {
    //   ["timezone_type"]=>
    //   int(1)
    //   ["timezone"]=>
    //   string(6) "+01:00"
    // }
    var_dump($this->datetime);

    $this->datetime->setTimezone(
        new\DateTimeZone(date_default_timezone_get())
    );

    return $this->datetime;
}
Copied!

After:

public function getDatetime(): ?\DateTime
{
    // object(DateTimeZone)#2 (2) {
    //   ["timezone_type"]=>
    //   int(3)
    //   ["timezone"]=>
    //   string(13) "Europe/Berlin"
    // }
    var_dump($this->datetime);

    // No explicit timezone needed for a proper named timezone
    return $this->datetime;
}
Copied!

Interpret integer based time fields as seconds without timezone offset 

The Extbase DataMapper will interpret format=time or format=timesec datetime fields as seconds without timezone offset, like FormEngine and DataHandler do. The database value is no longer considered as a UNIX timestamp, but as offset from midnight mapped on 1970-01-01T00:00:00 in PHP localtime.

For european timezones where Central Europe Time (CET) was active on 1970-01-01 that means an integer field value like 7200 (=02:00) will be mapped to 1970-01-01T02:00:00+01:00 instead of 1970-01-01T02:00:00+00:00 and the DateTime::$timezone property of the DateTime object will be set to the named timezone that is configured in PHP ini setting date.timezone instead of UTC.

That means the datetime value can be combined with explicit dates and is always using the server timezone.

Interpret 00:00:00 as non empty time value for nullable time properties 

Nullable format=time, format=timesec or dbType=time fields can now use 00:00:00 to represent midnight (this value has been used in non-nullable fields to represent an empty value). The DateTime mapper now understands this value instead of misinterpreting it as an empty value.

This behaviour could not be worked around before, that means existing implementations do not need to change or remove workarounds, but can basically support 00:00 as a value time field now.

Construct format=time and dbType=time properties based on 1970-01-01 

DateTime objects that map to native TIME fields or integer based fields configured with format=time are now initialized with 1970-01-01 as day-part instead of the current day which results in consistent mapped values independent from the day where the mapping is performed.

Before:

public function getDatetime(): ?\DateTime
{
    //object(DateTime)#2 (3) {
    //  ["date"]=>
    //  string(26) "2025-04-11 11:44:00.000000"
    //  ["timezone_type"]=>
    //  int(1)
    //  ["timezone"]=>
    //  string(6) "+02:00"
    //}

    var_dump($this->datetime);

    return $this->datetime;
}
Copied!

After:

public function getDatetime(): ?\DateTime
{
    //object(DateTime)#2 (3) {
    //  ["date"]=>
    //  string(26) "1970-01-01 11:44:00.000000"
    //  ["timezone_type"]=>
    //  int(3)
    //  ["timezone"]=>
    //  string(13) "Europe/Berlin"
    //}
    var_dump($this->datetime);

    return $this->datetime;
}
Copied!

Important: #106494 - Adapt custom instances of AbstractFormFieldViewHelper to deal with persistenceManager->getIdentifierByObject() methods 

See forge#106494

Description 

When dealing with relations to multilingual Extbase entities, these relations should always store their reference to the "original" (sys_language_uid=0) entity, so that later on, language record overlays can be properly applied.

For this to work in areas like persistence, internally an "identifier" is established that references these multilingual objects like [defaultLanguageRecordUid]_[localizedRecordUid].

Internally, this identifier should be converted back to only contain/reference the defaultLanguageRecordUid.

A bug has been fixed with #106494 to deal with this inside the <f:form.select> ViewHelper, which utilized an <option value="11_42"> (defaultLanguageRecordUid=11, localizedRecordUid=42), and when using an <f:form> to edit existing records, the currently attached records would NOT get pre-selected.

When such objects with relations were persisted (in frontend management interfaces with Extbase), if the proper option had not been selected again, the relation would get lost.

Important: Adapt custom ViewHelpers extended from AbstractFormFieldViewHelper or using persistenceManager->getIdentifierByObject() 

The bug has been fixed, but it is important that if third-party code created custom ViewHelpers based on \TYPO3\CMS\Fluid\ViewHelpers\Form\AbstractFormFieldViewHelper , these may need adoption too.

Instead of using code like this:

Example ViewHelper code utilizing persistenceManager->getIdentifierByObject()
if ($this->persistenceManager->getIdentifierByObject($valueElement) !== null) {
    return $this->persistenceManager->getIdentifierByObject($valueElement);
}
Copied!

the code should be adopted to not rely on getIdentifierByObject() but instead:

Refactored ViewHelper code preferring an object's getUid() method instead
if ($this->persistenceManager->getIdentifierByObject($valueElement) !== null) {
    if ($valueElement instanceof DomainObjectInterface) {
        return $valueElement->getUid() ?? $this->persistenceManager->getIdentifierByObject($valueElement);
    }
    return $this->persistenceManager->getIdentifierByObject($valueElement);
}
Copied!

This code ensures that retrieving the relational object's UID is done with the overlaid record, and only falls back to the full identifier, if it's not set, or not an object implementing the Extbase DomainObjectInterface.

Also note that the abstract's method convertToPlainValue() has been fixed to no longer return a value of format [defaultLanguageRecordUid]_[localizedRecordUid] but instead always use the original record's ->getUid() return value (=defaultLanguageRecordUid).

If this method convertToPlainValue() is used in 3rd-party code, make sure this is the expected result, too.

Important: #106508 - Respect column CHARACTER SET and COLLATE in ext_tables.sql 

See forge#106508

Description 

TYPO3 now reads column based CHARACTER SET and COLLATION from extension ext_tables.sql files and applies them on column level. This allows CHARACTER SET and COLLATION column settings different than defaults defined on table or schema level. This is limited to MySQL and MariaDB DBMS.

For now, CHARACTER SET ascii COLLATE ascii_bin is used for sys_refindex.hash to reduce required space for the index using single bytes instead of multiple bytes per character.

The introduced database change is considerable non-breaking, because:

  • Not applying the database changes still keeps a fully working state.
  • Applying database schema change does not require data migrations.
  • Targets only MySQL and MariaDB.
ext_tables.sql example
CREATE TABLE some_table (

    col1    CHAR(10) DEFAULT ''             NOT NULL CHARACTER SET ascii COLLATE ascii_bin,
    col2    CHAR(10) CHARACTER SET ascii COLLATE ascii_bin DEFAULT '' NOT NULL,
    col3    VARCHAR(10) DEFAULT ''          NOT NULL CHARACTER SET ascii COLLATE ascii_bin,
    col4    VARCHAR(10) CHARACTER SET ascii COLLATE ascii_bin DEFAULT '' NOT NULL,
    col5    TEXT DEFAULT ''                 NOT NULL CHARACTER SET ascii COLLATE ascii_bin,
    col6    TEXT CHARACTER SET ascii COLLATE ascii_bin DEFAULT '' NOT NULL,
    col7    MEDIUMTEXT DEFAULT ''           NOT NULL CHARACTER SET ascii COLLATE ascii_bin,
    col8    MEDIUMTEXT CHARACTER SET ascii COLLATE ascii_bin DEFAULT '' NOT NULL,
    col9    LONGTEXT DEFAULT ''             NOT NULL CHARACTER SET ascii COLLATE ascii_bin,
    col10   LONGTEXT CHARACTER SET ascii COLLATE ascii_bin DEFAULT '' NOT NULL,
Copied!

);

Important: #106894 - Site settings.yaml is now stored as a map 

See forge#106894

Description 

Site settings are defined as a map of keys, with a defined type and default.

The values were previously stored as a tree representation in settings.yaml, e.g.:

EXT:my_extension/Configuration/Sets/MySet/settings.yaml
foo:
  bar: 'value'
Copied!

This tree representation is easier to write, but has the drawback that arbitrary keys like foo.bar and foo.bar.baz exclude each other, as the subkey baz would be represented as a value of foo.bar in tree representation.

Note that TypoScript constants can express subkey constants since TypoScript can store a value and childnodes for every node, which means that existing extensions – that migrate to site sets – require this mixture of setting identifiers to be supported in order to avoid breaking existing settings.

The storage format of settings.yaml is now changed to use a map (like settings.definitions.yaml already do) to store setting values, in order to overcome the mentioned limitation. It is still supported to read from a tree, but the settings editor will convert the tree to a map when persisting values.

Given the following setting definition:

EXT:my_extension/Configuration/Sets/MySet/settings.definitions.yaml
settings:
  foo.bar:
    type: string
    default: ''
    label: FooBar
  foo.bar.baz:
    type: string
    default: ''
    label: FooBarBaz
Copied!

A map will be stored in settings.yaml that is able to store values for both setting identifiers:

typo3conf/sites/mysite/settings.yaml
foo.bar: 'Foo bar value'
foo.bar.baz: 'Foo Bar baz value'
Copied!

Also site sets are advised to use this format for settings provided in their sets settings.yaml file.

Existing anonymous settings (pre v13 style, e.g. settings without a matching settings.definitions.yaml definition) will be preserved as a tree, since it is not known which tree node is key or a value.

Important: #107062 - Avoid applying Content-Security-Policy nonce sources when not required 

See forge#107062

Description 

Using nonce sources in a Content-Security-Policy (CSP) HTTP header implicitly leads to having a Cache-Control: private, no-store HTTP response header and internally requires to renew the nonce value that is present in cached HTML contents, which has a negative impact on performance.

This change aims for having fully cached pages and tries to avoid nonce sources in the CSP header when actually feasible.

  • The ConsumableNonce class was refactored – it no longer extends ConsumableString.
  • Two new counters are introduced: consumeInline() – the nonce is required for an inline resource. consumeStatic() – the nonce is optional for a static resource.

Example usage:

<?php
$nonce = new ConsumableNonce();
$nonce->consumeInline(Directive::ScriptSrcElem); // inline script
$nonce->consumeStatic(Directive::StyleSrcElem);  // static style
Copied!

Nonce sources are removed from the CSP policy in the following situations, in case the request is supposed to be fully cacheable (config.no_cache = 0 and not having any USER_INT or COA_INT items):

  • The response body is readable and contains no bytes.
  • The nonce consumption counter for all usages equals zero.
  • A directive contains a source‑keyword exception (e.g. 'unsafe-inline') that makes a nonce unnecessary.
  • The PolicyPreparedEvent has been dispatched and explicitly tells the policy to avoid using nonce sources.

When the nonce should be removed, both the frontend and backend ContentSecurityPolicyHeaders middleware strip the nonce-related literals from the rendered HTML.

New PSR-14 event 

<?php
declare(strict_types=1);

namespace Example\MyPackage\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Disposition;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Event\PolicyPreparedEvent;

#[AsEventListener('my-package/content-security-policy/avoid-nonce')]
final class DropNonceEventListener
{
    public function __invoke(PolicyPreparedEvent $event): void
    {
        $policyBag = $event->policyBag;
        if (
            isset($policyBag->dispositionMap[Disposition::enforce])
            && $policyBag->scope->siteIdentifier === 'my-special-site'
            // YOLO: drop nonce sources, even it is consumed
            && $policyBag->nonce->count() > 0
        ) {
            $policyBag->behavior->useNonce = false;
        }
    }
}
Copied!

New useNonce property 

The \TYPO3\CMS\Core\Security\ContentSecurityPolicy\Configuration\Behavior class now contains the nullable boolean property useNonce:

  • true - explicitly allows using nonce sources
  • null - the default unspecific state (the system will detect and decide automatically)
  • false - explicitly denies using nonce sources, it also drops constraints like 'strict-dynamic' since that source keyword requires a nonce source

Important: #107063 - CKEditor 5 v46.1.0: TypeScript imports and CSS changes 

See forge#107063

Description 

With the upgrade to CKEditor 5 v46.1.0, three relevant changes are made:

  1. The API naming of TypeScript type imports has changed a lot. Any custom CKEditor 5 plugin using TypeScript type imports in their build chain will need to be adapted to match these imports. See https://ckeditor.com/docs/ckeditor5/latest/updating/nim-migration/migrating-imports.html for a large table of "before->after" renames. This is not considered a breaking change in context of TYPO3 integration, because existing JavaScript modules will continue to work, as TypeScript type imports are not part of the final output. Runtime imports that are exposed by the @ckeditor5/ckeditor-* modules have not been changed and will continue to work.
  2. A new opinionated default CSS is used by CKEditor to apply some improved styling over contents displayed within the RTE interface. Most of these are overruled by TYPO3's default CSS integration though. Possible customizations need to respect this.
  3. A few CSS classes have been renamed, see https://ckeditor.com/docs/ckeditor5/latest/updating/guides/update-to-46.html. These are for example referenced in custom CKEditor YAML configurations like the following diff, and need to replace the color subkey:

    Configuration/RTE/Full.yaml - Before/After
     - {
         model: 'yellowMarker',
         class: 'marker-yellow',
         title: 'Yellow marker',
         type: 'marker',
    -    color: 'var(--ck-highlight-marker-yellow)'
    +    color: 'var(--ck-content--highlight-marker-yellow)'
       }
     - {
         model: 'greenMarker',
         class: 'marker-green',
         title: 'Green marker',
         type: 'marker',
    -    color: 'var(--ck-highlight-marker-green)'
    +    color: 'var(--ck-content-highlight-marker-green)'
       }
     - {
         model: 'redPen',
         class: 'pen-red',
         title: 'Red pen',
         type: 'pen',
    -    color: 'var(--ck-highlight-pen-red)'
    +    color: 'var(--ck-content-highlight-pen-red)'
       }
    Copied!

Affected installations 

TYPO3 installation relying on custom or third-party CKEditor 5 TypeScript build chains, or CSS adaptations that no longer match the CKEditor 5 naming.

Possible Migration 

Follow the CKEditor 5 upgrade guide to change CSS class names and TypeScript imports.

Important: #107342 - Extend listForms method in FormPersistenceManagerInterface 

See forge#107342

Description 

With this change, the method signature of listForms(), defined by the FormPersistenceManagerInterface, has been extended by two arguments: $orderField and $orderDirection. The new definition is: public function listForms(array $formSettings, string $orderField = '', ?SortDirection $orderDirection = null): array;

Affected Installations 

Some TYPO3 installations may use this interface for their own FormPersistenceManager, even though it is marked as internal.

Possible Migration 

If you have implemented your own FormPersistenceManager, you need to update the method signature accordingly.

Important: #107594 - Icon overlay for TCA select items 

See forge#107594

Description 

The ability to define an icon overlay for items in the "New Content Element" wizard was originally introduced in forge#92942 using Page TSconfig, but was accidentally removed during the web-component migration in forge#100065 and then restored in forge#105253.

In the meantime, forge#102834 added auto-registration of wizard items directly from TCA. Since icon overlays defined in Page TSconfig duplicate configuration that can now be specified in TCA, the recommended approach is to define icon overlays directly in TCA using the new iconOverlay option for select items.

The iconOverlay property is now supported in the SelectItem component, enabling icon overlays for wizard items that are auto-registered via TCA.

Impact 

Icon overlays for New Content Element Wizard items can now be defined directly in TCA alongside other item properties like icon, label, description, and group.

This consolidates configuration in a single location and eliminates the need for separate Page TSconfig definitions. Page TSconfig icon overlays remain supported for backward compatibility, but TCA-based configuration is now the recommended approach.

Migration 

Previous approach using Page TSconfig (still works, but no longer recommended):

EXT:my_extension/Configuration/page.tsconfig
mod.wizards.newContentElement.wizardItems {
    my_group.elements {
        my_element {
            iconIdentifier = content-header
            iconOverlay = actions-approve
            title = LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:my_element_title
            description = LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:my_element_description
            tt_content_defValues {
                CType = my_element
            }
        }
    }
}
Copied!

Recommended approach using TCA:

EXT:my_extension/Configuration/TCA/Overrides/tt_content.php
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

ExtensionManagementUtility::addRecordType(
    [
        'label' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:my_element_title',
        'description' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:my_element_description',
        'value' => 'my_element',
        'icon' => 'content-header',
        'iconOverlay' => 'actions-approve',
        'group' => 'my_group'
    ],
    '...',
);
Copied!

Important: #107649 - Dependency Injection cache is now PHP version dependant 

See forge#107649

Description 

TYPO3 uses the PHP library symfony/dependency-injection to build a dependency injection container that contains class factories for services used by TYPO3 or by installed extensions.

With the update to symfony/dependency-injection v7.3 – which may be installed in TYPO3 v13 composer mode – the created factories are optimized to use certain PHP language level features, if available, which result in a cache that is incompatible when used with older PHP versions.

In a scenario where the dependency injection cache is created in a CLI PHP process (e.g. PHP v8.4), this may result in a cache to be created that is incompatible with a Web PHP process (e.g. PHP v8.2), if the minor versions of the CLI and Web environments differ.

For this reason the major and minor PHP version numbers are now hashed into the dependency injection cache identifier, resulting in a possible cache-miss on the first web-request after a deployment, if the system was prepared via bin/typo3 with a CLI PHP process version that is different to the Web PHP version.

Make sure to configure the PHP CLI process version php -v to use the same version number as configured for the Web process. The Web process version can be introspected in the backend toolbar entry System Information > PHP Version.

Important: #107681 - Disabled state for ShortcutButton and DropDownButton 

See forge#107681

Description 

The ShortcutButton and DropDownButton classes in the TYPO3 backend button bar system have been enhanced with new methods to support disabled state functionality. This brings them in line with other button types that already support the disabled state.

Two new methods have been added to both classes:

  • isDisabled(): bool - Checks if the button is disabled
  • setDisabled(bool $disabled) - Sets the disabled state of the button

When a button is disabled, it is rendered with appropriate HTML attributes and CSS classes to indicate its non-interactive state. For ShortcutButton, the disabled state is applied to both the simple button rendering and the dropdown rendering modes.

Impact 

Extension developers can now programmatically disable shortcut and dropdown buttons in the TYPO3 backend, preventing user interaction when needed. This is particularly useful for:

  • Preventing operations during form initialization
  • Disabling buttons during async operations
  • Conditional button availability based on application state
  • Improving user experience on slow network connections

The disabled state is properly propagated through the rendering process:

  • For shortcut buttons rendered as GenericButton, the disabled attribute is added to the button element
  • For shortcut buttons rendered as DropDownButton, the disabled state is passed to the dropdown button

Migration 

No migration is required. This change is fully backward compatible as it only adds new optional functionality. Existing code will continue to work without modifications.

Example usage for disabling a shortcut button:

EXT:my_extension/Classes/Controller/MyController.php
$shortcutButton = $buttonBar->makeShortcutButton()
    ->setRouteIdentifier('my_module')
    ->setDisplayName('My Module')
    ->setArguments(['id' => $pageId])
    ->setDisabled(true);
$buttonBar->addButton($shortcutButton);
Copied!

Example usage for disabling a dropdown button:

EXT:my_extension/Classes/Controller/MyController.php
$dropdownButton = GeneralUtility::makeInstance(DropDownButton::class)
    ->setLabel('Actions')
    ->setIcon($iconFactory->getIcon('actions-menu'))
    ->setDisabled(true);
$dropdownButton->addItem($item1);
$dropdownButton->addItem($item2);
$buttonBar->addButton($dropdownButton);
Copied!

Example usage for checking disabled state:

EXT:my_extension/Classes/Controller/MyController.php
if ($shortcutButton->isDisabled()) {
    // Handle disabled state
}
Copied!

13.4 Changes 

Table of contents

Breaking Changes 

None since TYPO3 v13.0 release.

Features 

None since TYPO3 v13.3 release.

Deprecation 

Important 

Deprecation: #105076 - Plugin content element and plugin sub types 

See forge#105076

Description 

Historically, plugins have been registered using the list content element and the plugin subtype list_type field. This functionality has been kept for backwards compatibility reasons. However, since the release of TYPO3 v12.4, the recommended way to create a plugin is by using a dedicated content type (CType) for each plugin.

This old "General Plugin" approach has always been ugly from a UX perspective point of view since it hides plugin selection behind "General plugin" content element, forcing a second selection step and making such plugins something special.

Therefore, the plugin content element (list) and the plugin sub types field ( list_type) have been marked as deprecated in TYPO3 v13.4 and will be removed in TYPO3 v14.0.

Additionally, the related PHP constant TYPO3\CMS\Extbase\Utility\ExtensionUtility::PLUGIN_TYPE_PLUGIN has been deprecated as well.

Impact 

Plugins added using TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPlugin() where the second parameter is list_type (which is still the default) will trigger a deprecation level log entry in TYPO3 v13 and will fail in v14.

Therefore, the same applies on using TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin() (to configure the plugin for frontend rendering), where no fifth parameter is provided or where the fifth parameter is list_type ( ExtensionUtility::PLUGIN_TYPE_PLUGIN), which is still the default.

The extension scanner will report any usage of configurePlugin(), where less than the required five arguments are provided. Actually, the only valid value for the fifth parameter $pluginType is CType, for which the ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT constant can be used.

Affected installations 

Extensions registering plugins as list_type plugin sub type.

Migration 

Existing plugins must be migrated to use the CType record type. Extension authors must implement the following changes:

  • Register plugins using the CType record type
  • Create update wizard which extends \TYPO3\CMS\Install\Updates\AbstractListTypeToCTypeUpdate and add list_type to CType mapping for each plugin to migrate. The migration wizard for indexed_search in class \IndexedSearchCTypeMigration can be used as reference example.
  • Migrate possible FlexForm registration and add dedicated showitem TCA configuration
  • Migrate possible PreviewRenderer registration in TCA
  • Adapt possible content element wizard items in Page TSConfig, where list_type is used
  • Adapt possible content element restrictions in backend layouts or container elements defined by third-party extensions like ichhabrecht/content-defender .

Common example 

// Before

$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist']['my_plugin'] = 'pi_flexform';
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_excludelist']['my_plugin'] = 'pages,layout,recursive';

// After
$GLOBALS['TCA']['tt_content']['types']['my_plugin']['showitem'] = '<Some Fields>,pi_flexform,<Other Fields>';
Copied!

Deprecation: #105171 - INCLUDE_TYPOSCRIPT TypoScript syntax 

See forge#105171

Description 

The old TypoScript syntax to import external TypoScript files based on <INCLUDE_TYPOSCRIPT: has been marked as deprecated with TYPO3 v13 and will be removed in v14.

Integrators should switch to @import.

There are multiple reasons to finally phase out this old construct:

  • The <INCLUDE_TYPOSCRIPT: syntax is clumsy and hard to grasp, especially when combining multiple options. It is hard to learn for integrators new to the project.
  • The implementation has a high level of complexity, is only partially tested and consists of edge cases that are hard to decide on and even harder to change since that may break existing usages in hard to debug ways.
  • The syntax can have negative security impact when not used wisely by loading TypoScript from editor related folders like fileadmin/. The syntax based on @import has been designed more thoughtful in this regard.
  • Loading TypoScript files from folders relative to the public web folder is unfortunate and can have negative side effects when switching "legacy" based instances to composer.
  • The syntax based on @import has been introduced in TYPO3 v9 already and has been designed to stay at this point in time. With TYPO3 v12, the last missing feature of <INCLUDE_TYPOSCRIPT: has been made possible for @import as well: @import can be loaded conditionally by putting them into the body of a casual TypoScript condition.
  • TYPO3 v12 discouraged using <INCLUDE_TYPOSCRIPT: within the documentation and already anticipated a future deprecation and removal.

Impact 

When using TypoScript <INCLUDE_TYPOSCRIPT: syntax a deprecation level log entry in TYPO3 v13 is emitted. The syntax will stop working with TYPO3 v14 and will be detected as an "invalid line" in the TypoScript related Backend modules.

Affected installations 

Instances TypoScript syntax based on <INCLUDE_TYPOSCRIPT:.

Migration 

Most usages of <INCLUDE_TYPOSCRIPT: can be turned into @import easily. A few examples:

# Before
<INCLUDE_TYPOSCRIPT: source="FILE:EXT:my_extension/Configuration/TypoScript/myMenu.typoscript">
# After
@import 'EXT:my_extension/Configuration/TypoScript/myMenu.typoscript'

# Before
# Including .typoscript files in a single (non recursive!) directory
<INCLUDE_TYPOSCRIPT: source="DIR:EXT:my_extension/Configuration/TypoScript/" extensions="typoscript">
# After
@import 'EXT:my_extension/Configuration/TypoScript/*.typoscript'

# Before
# Including .typoscript and .ts files in a single (non recursive!) directory
<INCLUDE_TYPOSCRIPT: source="DIR:EXT:my_extension/Configuration/TypoScript/" extensions="typoscript,ts">
# After
@import 'EXT:my_extension/Configuration/TypoScript/*.typoscript'
# Rename all files ending on .ts to .typoscript

# Before
# Including a file conditionally
<INCLUDE_TYPOSCRIPT: source="FILE:EXT:my_extension/Configuration/TypoScript/user.typoscript" condition="[frontend.user.isLoggedIn]">
# After
[frontend.user.isLoggedIn]
    @import 'EXT:my_extension/Configuration/TypoScript/user.typoscript'
[END]
Copied!

There are a few more use cases that cannot be transitioned so easily since @import is a bit more restrictive.

As one restriction @import cannot include files from arbitrary directories like fileadmin/, but only from extensions by using the EXT: prefix. Instances that use <INCLUDE_TYPOSCRIPT: with source="FILE:./someDirectory/..." should move this TypoScript into a project or site extension. Such instances are also encouraged to look into the TYPO3 v13 "Site sets" feature and eventually transition towards it along the way.

@import allows to import files with the file ending .typoscript and .tsconfig. If you used any of the outdated file endings like .ts or .txt rename those files before switching to the @import syntax.

The @import feature does not support recursive directory inclusion, as it does not allow wildcards in directory paths. Having directories like TypoScript/foo and TypoScript/bar, each having .typoscript files, could be included using <INCLUDE_TYPOSCRIPT: source=DIR:EXT:my_extension/Configuration/TypoScript extensions="typoscript">, which would find such files in foo and bar, and any other directory. This level of complexity was not wished to allow in the @import syntax since it can make file includes more intransparent with too much attached magic. Instances using this should either reorganize their files, or have multiple dedicated @import statements. The need for recursive includes may also be mitigated by restructuring TypoScript based functionality using "Site sets".

The transition from <INCLUDE_TYPOSCRIPT: can be often further relaxed with these features in mind:

Deprecation: #105213 - TCA sub types 

See forge#105213

Description 

One of the main features of TCA are the record types. This allows to use a single table for different purposes and in different contexts. The most known examples of using record types are the "Page Types" of pages and the "Content Types" of tt_content. For every specific type of such table, it's possible to define the fields to be used and even manipulate them e.g. change their label.

A special case since ever has been the plugin registration. This for a long time has been done using the so called "sub types" feature of TCA. This is another layer below record types and allows to further customize the behaviour of a record type using another select field, defined via subtype_value_field as well as defining fields to be added - subtypes_addlist - or excluded - subtypes_excludelist - for the record type, depending on the selected sub type.

For a couple of version now, it's encouraged to register plugins just as standard content elements via the tt_content type field CType. Therefore, the special registration via the combination of the list record type and the selection of a sub type via the list_type field has already been deprecated with Deprecation: #105076 - Plugin content element and plugin sub types.

Since the "sub types" feature was mainly used for this scenario only, it has now been deprecated as well. Registration of custom types should therefore always be done by using record types. This makes configuration much cleaner and more comprehensible.

Impact 

Using subtype_value_field in a TCA types configurations will lead to a deprecation log entry containing information about where adaptations need to take place.

Affected installations 

All installations using the sub types feature by defining a subtype_value_field in a TCA types configuration, which is really uncommon as the feature was mainly used for plugin registration in the tt_content table only.

Migration 

Replace any subtype_value_field configuration with dedicated record types. Please also consider migrating corresponding subtypes_addlist and subtypes_excludelist definitions accordingly.

Before 

'ctrl' => [
    'type' => 'type',
],
'columns' => [
    'type' => [
        'config' => [
            'type' => 'select',
            'renderType' => 'selectSingle',
            'items' => [
                [
                    'label' => 'A record type',
                    'value' => 'a_record_type'
                ]
            ]
        ]
    ],
    'subtype' => [
        'config' => [
            'type' => 'select',
            'renderType' => 'selectSingle',
            'items' => [
                [
                    'label' => 'A sub type',
                    'value' => 'a_sub_type'
                ]
            ]
        ]
    ],
],
'types' => [
    'a_record_type' => [
        'showitem' => 'aField,bField',
        'subtype_value_field' => 'subtype',
        'subtypes_addlist' => [
            'a_sub_type' => 'pi_flexform'
        ],
        'subtypes_excludelist' => [
            'a_sub_type' => 'bField'
        ]
    ]
]
Copied!

After 

'ctrl' => [
    'type' => 'type',
],
'columns' => [
    'type' => [
        'config' => [
            'type' => 'select',
            'renderType' => 'selectSingle',
            'items' => [
                [
                    'label' => 'A record type',
                    'value' => 'a_record_type'
                ],
                [
                    'label' => 'A sub type',
                    'value' => 'a_sub_type'
                ]
            ]
        ]
    ],
],
'types' => [
    'a_record_type' => [
        'showitem' => 'aField,bField'
    ],
    'a_sub_type' => [
        'showitem' => 'aField,pi_flexform'
    ]
]
Copied!

Deprecation: #105230 - TypoScriptFrontendController and $GLOBALS['TSFE'] 

See forge#105230

Description 

Class \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController and its global instance $GLOBALS['TSFE'] have been marked as deprecated. The class will be removed with TYPO3 v14.

Impact 

Calling TypoScriptFrontendController methods, or accessing state from $GLOBALS['TSFE'] is considered deprecated.

Affected installations 

Various instances may still retrieve information from $GLOBALS['TSFE'] . Remaining uses should be adapted. The extension scanner will find possible matches.

To keep backwards compatibility in TYPO3 v13, some calls can not raise deprecation level log messages.

Migration 

See Breaking: #102621 - Most TSFE members marked internal or read-only for details on substitutions. In general, most state used by extensions has been turned into request attributes.

Deprecation: #105252 - DataProviderContext getters and setters 

See forge#105252

Description 

The backend layout related data object class \TYPO3\CMS\Backend\View\BackendLayout\DataProviderContext has been turned into a data object using public constructor property promotion (PCPP). All setX() and getX() methods have been marked as deprecated in TYPO3 v13.4 and will be removed with TYPO3 v14.0. The class will be declared readonly in TYPO3 v14.0 which will enforce instantiation using PCPP. The class has been declared final since it is an API contract that must never be changed or extended. The constructor arguments will be declared non-optional in TYPO3 v14.0.

  • TYPO3\CMS\Backend\View\BackendLayout\DataProviderContext->setPageId()
  • TYPO3\CMS\Backend\View\BackendLayout\DataProviderContext->setTableName()
  • TYPO3\CMS\Backend\View\BackendLayout\DataProviderContext->setFieldName()
  • TYPO3\CMS\Backend\View\BackendLayout\DataProviderContext->setData()
  • TYPO3\CMS\Backend\View\BackendLayout\DataProviderContext->setPageTsConfig()
  • TYPO3\CMS\Backend\View\BackendLayout\DataProviderContext->getPageId()
  • TYPO3\CMS\Backend\View\BackendLayout\DataProviderContext->getTableName()
  • TYPO3\CMS\Backend\View\BackendLayout\DataProviderContext->getFieldName()
  • TYPO3\CMS\Backend\View\BackendLayout\DataProviderContext->getData()
  • TYPO3\CMS\Backend\View\BackendLayout\DataProviderContext->getPageTsConfig()

Impact 

Calling the getters or setters raises deprecation level log errors and will stop working in TYPO3 v14.0.

Affected installations 

This data object is only relevant for instances with extensions that add custom backend layout data providers using $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['BackendLayoutDataProvider'] . There are few known extensions that do this. The extension scanner is not configured to find possible usages since the method names are too generic and would lead to too many false positives.

Migration 

Create new objects using PCPP with named arguments instead of the setters. Instances should be created using new():

// Before
$dataProviderContext = GeneralUtility::makeInstance(DataProviderContext::class);
$dataProviderContext
    ->setPageId($pageId)
    ->setData($parameters['row'])
    ->setTableName($parameters['table'])
    ->setFieldName($parameters['field'])
    ->setPageTsConfig($pageTsConfig);

// After
$dataProviderContext = new DataProviderContext(
    pageId: $pageId,
    tableName: $parameters['table'],
    fieldName: $parameters['field'],
    data: $parameters['row'],
    pageTsConfig: $pageTsConfig,
);
Copied!

Use the properties instead of the getters, example:

// Before
$pageId = $dataProviderContext->getPageId()
// After
$pageId = $dataProviderContext->pageId
Copied!

Deprecation: #105279 - Replace TYPO3 EnumType with Doctrine DBAL EnumType 

See forge#105279

Description 

TYPO3 did provide a custom Doctrine DBAL column type implementation for the native SQL type ENUM that was only compatible with MySQL and MariaDB connections.

The Doctrine DBAL Team implemented ENUM support with Release 4.2.0 in class \Doctrine\DBAL\Types\EnumType, only supporting MySQL and MariaDB as well.

TYPO3 removed its custom implementation with TYPO3 v13.4.0.

Class \TYPO3\CMS\Core\Database\Schema\Types\EnumType has been marked as deprecated and is replaced with an class alias of \EnumType. The alias will be removed with TYPO3 v14.

See Release 4.2.0

Impact 

doctrine/dbal >= 4.2.0 is incompatible with TYPO3 versions before v13.4.0. Composer-based instances using TYPO3 v13.3 or older should add an according conflict to their composer.json.

Affected installations 

Instances using the the ENUM type directly or by any third party extension using TYPO3 13.0 to 13.3 in Composer mode will break, when the doctrine/dbal Composer packages is updated to version 4.2.0 or newer.

Migration 

Upgrade (directly) to TYPO3 v13.4 or ensure to avoid updating Doctrine DBAL to 4.2.x or newer versions in Composer-based instances.

Replace \TYPO3\CMS\Core\Database\Schema\Types\EnumType type declarations with \Doctrine\DBAL\Types\EnumType.

Deprecation: #105297 - tableoptions and collate connection configuration 

See forge#105297

Description 

The possibility to configure default table options like charset and collation for the database analyzer has been introduced using the array $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['tableoptions'] with sub-array keys charset and collate. These were only used for MySQL and MariaDB connections.

Since TYPO3 v11 the tableoptions keys were silently migrated to defaultTableOptions, which is the proper Doctrine DBAL connection option for for MariaDB and MySQL.

Furthermore, Doctrine DBAL 3.x switched from using they array key collate to collation, ignoring the old array key with Doctrine DBAL 4.x. This was silently migrated by TYPO3, too.

These options and migration are now deprecated in favor of using the final array keys and will be removed with TYPO3 v15 (or later) as breaking change.

Impact 

Instances using the database connection options in $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['tableoptions'] array, or using the collate key will trigger a E_USER_DEPRECATED notification.

Affected installations 

All instances using the mentioned options.

Migration 

Review settings.php and additional.php and adapt the deprecated configuration by renaming affected array keys.

# before
'DB' => [
    'Connections' => [
        'Default' => [
            'tableoptions' => [
                'collate' => 'utf8mb4_unicode_ci',
            ],
        ],
    ],
],

# after
'DB' => [
    'Connections' => [
        'Default' => [
            'defaultTableOptions' => [
                'collation' => 'utf8mb4_unicode_ci',
            ],
        ],
    ],
],
Copied!

Important: #105175 - Move FrontendBackendUserAuthentication into EXT:frontend 

See forge#105175

Description 

The internal \TYPO3\CMS\Frontend\Authentication\FrontendBackendUserAuthentication class, used for frontend requests while being logged in the backend has been moved from EXT:backend to EXT:frontend, since its dependencies are limited to EXT:core and EXT:frontend.

While for v13 a class alias mapping and a legacy notation for IDE's is available, the class is marked as @internal and therefore does not fall under TYPO3's Core API deprecation policy.

13.3 Changes 

Table of contents

Breaking Changes 

None since TYPO3 v13.0 release.

Features 

Deprecation 

Important 

Feature: #93100 - Allow to directly declare static route variables 

See forge#93100

Description 

Instead of having to use custom route aspect mappers, implementing \TYPO3\CMS\Core\Routing\Aspect\StaticMappableAspectInterface , to avoid having &cHash= signatures being applied to the generated URL, variables now can be simply declared static in the corresponding route enhancer configuration.

Impact 

By using the new static route configuration directive, custom aspect mapper implementations can be avoided. However, static route variables are only applied for a particular variable name if

  • there is no aspect mapper configured - aspect mappers are considered more specific and will take precedence
  • there is a companion requirements definition which narrows the set of possible values, and should be as restrictive as possible to avoid potential cache flooding - static routes variables are ignored, if there is no corresponding requirements definition

Example 

routeEnhancers:
  Verification:
    type: Simple
    routePath: '/verify/{code}'
    static:
      code: true
    requirements:
      # only allows SHA1-like hex values - which still allows lots
      # of possible combinations - thus, for this particular example
      # the handling frontend controller should be uncached as well
      #
      # hint: if `static` is set, `requirements` must be set as well
      code: '[a-f0-9]{40}'
Copied!

As a result, using the URI query parameters &code=11f6ad8ec52a2984abaafd7c3b516503785c2072 would generate the URL https://example.org/verify/11f6ad8ec52a2984abaafd7c3b516503785c2072.

Feature: #99418 - Enable recycler by default 

See forge#99418

Description 

The TYPO3 system extension typo3/cms-recycler is now enabled by default for new TYPO3 installations.

Impact 

New composer-based TYPO3 installations based on the TYPO3 CMS Base Distribution, and new legacy installations (tarball / zip download) have the system extension recycler enabled by default.

Feature: #99510 - Add file embedding option to asset ViewHelpers 

See forge#99510

Description 

The ViewHelpers <f:asset.css> and <f:asset.script> have been extended with a new argument inline. If this argument is set, the referenced asset file is rendered inline.

Setting the argument will therefore load the file content of the defined href / src as inline style or script. This is especially useful for content elements which are used as first element on a page and need some custom CSS to improve the Cumulative Layout Shift (CLS).

Impact 

To add inline styles and scripts from a referenced file, the new inline argument can be set. For example, to add above-the-fold styles, the priority option can be set, which will put the file contents of EXT:sitepackage/Resources/Public/Css/my-hero.css as inline styles to the <head> section.

<f:asset.css identifier="my-hero" href="EXT:sitepackage/Resources/Public/Css/my-hero.css" inline="1" priority="1"/>
Copied!

To add JavaScript:

<f:asset.script identifier="my-hero" src="EXT:sitepackage/Resources/Public/Js/my-hero.js" inline="1" priority="1"/>
Copied!

Feature: #101252 - Introduce ErrorHandler for 403 errors with redirect option 

See forge#101252

Description 

The new error handler \TYPO3\CMS\Core\Error\PageErrorHandler\RedirectLoginErrorHandler has been added, which makes it possible to redirect the user to a configurable page.

Requesting a login-protected URL would usually return a generic HTTP 403 error in case of a missing fulfilled access permissions and the configuration typolinkLinkAccessRestrictedPages = NONE (default) is set.

By enabling this new handler via the site settings, the 403 response can be handled and a custom redirect can be performed.

The RedirectLoginErrorHandler allows to define a loginRedirectTarget, which must be configured to the page, where the login process is handled. Additionally, the loginRedirectParameter must be set to the URL parameter that will be used to hand over the original URL to the target page.

The redirect ensures that the original URL is added to the configured GET parameter loginRedirectParameter, so that the user can be redirected back to the original page after a successful login.

The error handler allows return_url or redirect_url as values for loginRedirectParameter. Those values are used in extensions like EXT:felogin or EXT:oidc.

The new error handler works (with some minor exceptions) similar to the "Forbidden (HTTP Status 403)" handler in TYPO3 extension plan2net/sierrha . It will still emit generic 403 HTTP error messages in certain scenarios, like when a user is already logged in, but the permissions are not satisfied.

Impact 

It is now possible to configure a login redirection process when a user has no access to a page and a 403 error is thrown, so that after login the originating URL is requested again. Previously, this required custom Middlewares or implementations of PageErrorHandlerInterface .

Feature: #101391 - Add base64 attribute to ImageViewHelper 

See forge#101391

Description 

The ViewHelpers <f:image> and <f:uri.image> now support the attribute base64="true" that will provide a possibility to return the value of the image's src attribute encoded in base64.

<f:image base64="true" src="EXT:backend/Resources/Public/Images/typo3_logo_orange.svg" height="20" class="pr-2" />
<img src="{f:uri.image(base64: 'true', src:'EXT:backend/Resources/Public/Images/typo3_logo_orange.svg')}">
Copied!

Will result in the according HTML tag providing the image encoded in base64.

<img class="pr-2" src="data:image/svg+xml;base64,PHN2...cuODQ4LTYuNzU3Ii8+Cjwvc3ZnPgo=" alt="" width="20" height="20">
<img src="data:image/svg+xml;base64,PHN2...cuODQ4LTYuNzU3Ii8+Cjwvc3ZnPgo=">
Copied!

This can be particularly useful inside \TYPO3\CMS\Core\Mail\FluidEmail or to prevent unneeded HTTP calls.

Feature: #101472 - Allow static routes to assets 

See forge#101472

Description 

It is now possible to configure static routes with the type asset to link to resources which are typically located in the directory EXT:my_extension/Resources/Public/.

config/sites/my-site/config.yaml
routes:
  -
    route: example.svg
    type: asset
    asset: 'EXT:backend/Resources/Public/Icons/Extension.svg'
Copied!

Note that the asset URL can be configured on a per-site basis. This allows to deliver site-dependent custom favicon or manifest assets, for example.

Impact 

Static routes to files shipped with extensions can now be configured in the site configuration.

Feature: #102255 - Option to skip URL processing in AssetRenderer 

See forge#102255

Description 

The \TYPO3\CMS\Core\Page\AssetCollector options have been extended to include an external flag. When set for asset files using $assetCollector->addStyleSheet() or $assetCollector->addJavaScript(), all processing of the asset URI (like the addition of the cache busting parameter) is skipped and the input path will be used as-is in the resulting HTML tag.

Example 

The following code skips the cache busting parameter ?1726090820 for the supplied CSS file:

$assetCollector->addStyleSheet(
    'myCssFile',
    PathUtility::getAbsoluteWebPath(GeneralUtility::getFileAbsFileName('EXT:my_extension/Resources/Public/MyFile.css')),
    [],
    ['external' => true]
);
Copied!

Resulting in the following HTML output:

<link rel="stylesheet" href="/_assets/<hash>/myFile.css" />
Copied!

Impact 

Developers can now use the AssetCollector API to embed JavaScript or CSS files without any processing of the supplied asset URI.

Feature: #102353 - AVIF support for images generated by GIFBUILDER 

See forge#102353

Description 

GIFBUILDER, the image manipulation library for TypoScript based on GDlib, a PHP extension bundled into PHP, now also supports generating resulting files of type "avif".

AVIF is an image format, that is supported by most modern browsers, and usually has a better compression (= smaller file size) than jpg files.

Impact 

If defined via format=avif within a GifBuilder setup, the generated files are now AVIF files instead of png (the default).

It is possible to define the quality of a AVIF image similar to jpg images globally via $TYPO3_CONF_VARS['GFX']['avif_quality'] or via TypoScript's "quality" property on a per-image basis. Via TypoScript it is also possible to use the new property "speed" - see https://www.php.net/manual/en/function.imageavif.php for more details.

Example 

page.10 = IMAGE
page.10 {
  file = GIFBUILDER
  file {
    backColor = yellow
    XY = 1024,199
    format = avif
    quality = 44
    speed = 1

    10 = IMAGE
    10.offset = 10,10
    10.file = 1:/my-image.jpg
  }
}
Copied!

A new test in the Environment module / Install Tool can be used to check if the bundled GDlib extension of your PHP version supports the AVIF image format.

Feature: #102422 - Introduce CacheDataCollector Api 

See forge#102422

Description 

A new API has been introduced to collect cache tags and their corresponding lifetime. This API is used in TYPO3 to accumulate cache tags from page cache and content object cache.

The API is implemented as a new PSR-7 request attribute 'frontend.cache.collector', which makes this API independent from TSFE.

Every cache tag has a lifetime. The minimum lifetime is calculated from all given cache tags. By default, the lifetime of a cache tag is set to PHP_INT_MAX, so it expires many years in the future. API users must therefore define the lifetime of a cache tag individually.

The current TSFE API is deprecated in favor of the new API, as the current cache tag API implementation does not allow to set lifetime and extension authors had to work around it.

Example 

Add a single cache tag with 24 hours lifetime
use TYPO3\CMS\Core\Cache\CacheTag;

$cacheDataCollector = $request->getAttribute('frontend.cache.collector');
$cacheDataCollector->addCacheTags(
    new CacheTag('tx_myextension_mytable', 86400)
);
Copied!
Add multiple cache tags with different lifetimes
use TYPO3\CMS\Core\Cache\CacheTag;

$cacheDataCollector = $request->getAttribute('frontend.cache.collector');
$cacheDataCollector->addCacheTags(
    new CacheTag('tx_myextension_mytable_123', 3600),
    new CacheTag('tx_myextension_mytable_456', 2592000)
);
Copied!
Remove a cache tag
use TYPO3\CMS\Core\Cache\CacheTag;

$cacheDataCollector = $request->getAttribute('frontend.cache.collector');
$cacheDataCollector->removeCacheTags(
    new CacheTag('tx_myextension_mytable_123')
);
Copied!
Remove multiple cache tags
use TYPO3\CMS\Core\Cache\CacheTag;

$cacheDataCollector = $request->getAttribute('frontend.cache.collector');
$cacheDataCollector->removeCacheTags(
    new CacheTag('tx_myextension_mytable_123'),
    new CacheTag('tx_myextension_mytable_456')
);
Copied!
Get minimum lifetime, calculated from all cache tags
$cacheDataCollector = $request->getAttribute('frontend.cache.collector');
$cacheDataCollector->resolveLifetime();
Copied!
Get all cache tags
$cacheDataCollector = $request->getAttribute('frontend.cache.collector');
$cacheDataCollector->getCacheTags();
Copied!

The following event should only be used in code that has no access to the request attribute 'frontend.cache.collector', it is marked @internal and may vanish: It designed to allow passive cache-data signaling, without exactly knowing the current context and not having the current request at hand. It is not meant to allow for cache tag interception or extension.

Add cache tag without access to the request object
$this->eventDispatcher->dispatch(
    new AddCacheTagEvent(
        new CacheTag('tx_myextension_mytable_123', 3600)
    )
);
Copied!

Feature: #103511 - Introduce Extbase file upload and deletion handling 

See forge#103511

Description 

TYPO3 now provides an API for file upload- and deletion-handling in Extbase extensions, which allows extension developers to implement file uploads more easily into Extbase Domain Models.

The scope of this API is to cover some of the most common use cases and to keep the internal file upload and deletion process in Extbase as simple as possible.

The API supports mapping and handling of file uploads and deletions for the following scenarios:

  • Property of type FileReference in a domain model
  • Property of type \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\TYPO3\CMS\Extbase\Domain\Model\FileReference> in a domain model

File uploads can be validated by the following rules:

  • minimum and maximum file count
  • minimum and maximum file size
  • allowed MIME types
  • image dimensions (for image uploads)

Additionally, it is ensured, that the filename given by the client is valid, meaning that no invalid characters (null-bytes) are added and that the file does not contain an invalid file extension. The API has support for custom validators, which can be created on demand.

To avoid complexity and maintain data integrity, a file upload is only processed if the validation of all properties of a domain model is successful. In this first implementation, file uploads are not persisted/cached temporarily, so this means in any case of a validation failure ("normal" validators and file upload validation) a file upload must be performed again by users.

Possible future enhancements of this functionality could enhance the existing #[FileUpload] attribute/annotation with configuration like a temporary storage location, or specifying additional custom validators (which can be done via the PHP-API as described below)

Nesting of domain models 

File upload handling for nested domain models (e.g. modelA.modelB.fileReference) is not supported.

File upload configuration with the FileUpload attribute 

File upload for a property of a domain model can be configured using the newly introduced \TYPO3\CMS\Extbase\Annotation\FileUpload attribute.

Example:

#[FileUpload([
    'validation' => [
        'required' => true,
        'maxFiles' => 1,
        'fileSize' => ['minimum' => '0K', 'maximum' => '2M'],
        'mimeType' => ['allowedMimeTypes' => ['image/jpeg', 'image/png']],
        'fileExtension' => ['allowedFileExtensions' => ['jpg', 'jpeg', 'png']],
    ],
    'uploadFolder' => '1:/user_upload/files/',
])]
protected ?FileReference $file = null;
Copied!

All configuration settings of the \TYPO3\CMS\Extbase\Mvc\Controller\FileUploadConfiguration object can be defined using the FileUpload attribute. It is however not possible to add custom validators using the FileUpload attribute, which you can achieve with a manual configuration as shown below.

The currently available configuration array keys are:

  • validation ( array with keys required, maxFiles, minFiles, fileSize, fileExtension, allowedMimeTypes, mimeType, imageDimensions, see File upload validation)
  • uploadFolder ( string, destination folder)
  • duplicationBehavior ( object, behaviour when file exists)
  • addRandomSuffix ( bool, suffixing files)
  • createUploadFolderIfNotExist ( bool, whether to create missing directories)

It is also possible to use the FileUpload annotation to configure file upload properties, but it is recommended to use the FileUpload attribute due to better readability.

Manual file upload configuration 

A file upload configuration can also be created manually and should be done in the initialize*Action.

Example:

public function initializeCreateAction(): void
{
    $mimeTypeValidator = GeneralUtility::makeInstance(MimeTypeValidator::class);
    $mimeTypeValidator->setOptions(['allowedMimeTypes' => ['image/jpeg']]);
    $fileExtensionValidator = GeneralUtility::makeInstance(FileExtensionValidator::class);
    $fileExtensionValidator->setOptions(['allowedFileExtensions' => ['jpg', 'jpeg']]);

    $fileHandlingServiceConfiguration = $this->arguments->getArgument('myArgument')->getFileHandlingServiceConfiguration();
    $fileHandlingServiceConfiguration->addFileUploadConfiguration(
        (new FileUploadConfiguration('myPropertyName'))
            ->setRequired()
            ->addValidator($mimeTypeValidator)
            ->addValidator($fileExtensionValidator)
            ->setMaxFiles(1)
            ->setUploadFolder('1:/user_upload/files/')
    );

    $this->arguments->getArgument('myArgument')->getPropertyMappingConfiguration()->skipProperties('myPropertyName');
}
Copied!

Configuration options for file uploads 

The configuration for a file upload is defined in a FileUploadConfiguration object.

This object contains the following configuration options.

Property name: 

Defines the name of the property of a domain model to which the file upload configuration applies. The value is automatically retrieved when using the FileUpload attribute. If the FileUploadConfiguration object is created manually, it must be set using the $propertyName constructor argument.

Validation: 

File upload validation is defined in an array of validators in the FileUploadConfiguration object.

The validators \TYPO3\CMS\Extbase\Validation\Validator\FileNameValidator , (ensures that no executable PHP files can be uploaded) and \TYPO3\CMS\Extbase\Validation\Validator\FileExtensionMimeTypeConsistencyValidator (ensuring that the file extension matches the expected mime-type assumptions), are enforced and executed by default.

In addition, Extbase includes the following validators to validate an UploadedFile object:

  • \TYPO3\CMS\Extbase\Validation\Validator\FileExtensionValidator
  • \TYPO3\CMS\Extbase\Validation\Validator\FileSizeValidator
  • \TYPO3\CMS\Extbase\Validation\Validator\MimeTypeValidator
  • \TYPO3\CMS\Extbase\Validation\Validator\ImageDimensionsValidator

Those validators can either be configured with the FileUpload attribute or added manually to the configuration object with the addValidator method.

Required: 

Defines whether a file must be uploaded. If it is set to true, the minFiles configuration is set to 1.

Minimum files: 

Defines the minimum amount of files to be uploaded.

Maximum files: 

Defines the maximum amount of files to be uploaded.

Upload folder: 

Defines the upload path for the file upload. This configuration expects a storage identifier (e.g. 1:/user_upload/folder/). If the given target folder in the storage does not exist, it is created automatically.

Upload folder creation, when missing: 

The default creation of a missing storage folder can be disabled via the configuration attribute createUploadFolderIfNotExist ( bool, default true).

Add random suffix: 

When enabled, the filename of an uploaded and persisted file will contain a random 16 char suffix. As an example, an uploaded file named job-application.pdf will be persisted as job-application-<random-hash>.pdf in the upload folder.

The default value for this configuration is true and it is recommended to keep this configuration active.

This configuration only has an effect when uploaded files are persisted.

Duplication behavior: 

Defines the FAL behavior, when a file with the same name exists in the target folder. Possible values are DuplicationBehavior::RENAME (default), DuplicationBehavior::REPLACE and DuplicationBehavior::CANCEL.

Modifying existing configuration 

File upload configuration defined by the FileUpload attribute can be changed in the initialize*Action.

Example:

public function initializeCreateAction(): void
{
    $validator = GeneralUtility::makeInstance(MyCustomValidator::class);

    $argument = $this->arguments->getArgument('myArgument');
    $configuration = $argument->getFileHandlingServiceConfiguration()->getFileUploadConfigurationForProperty('file');
    $configuration?->setMinFiles(2);
    $configuration?->addValidator($validator);
    $configuration?->setUploadFolder('1:/user_upload/custom_folder');
}
Copied!

The example shows how to modify the file upload configuration for the argument item and the property file. The minimum amount of files to be uploaded is set to 2 and a custom validator is added.

To remove all defined validators except the FileNameValidator, use the resetValidators() method.

Using TypoScript configuration for file uploads configuration 

When a file upload configuration for a property has been added using the FileUpload attribute, it may be required make the upload folder or other configuration options configurable with TypoScript.

Extension authors should use the initialize*Action to apply settings from TypoScript to a file upload configuration.

Example:

public function initializeCreateAction(): void
{
    $argument = $this->arguments->getArgument('myArgument');
    $configuration = $argument->getFileHandlingServiceConfiguration()->getFileUploadConfigurationForProperty('file');
    $configuration?->setUploadFolder($this->settings['uploadFolder'] ?? '1:/fallback_folder');
}
Copied!

File upload validation 

Each uploaded file can be validated against a configurable set of validators. The validation section of the FileUpload attribute allows to configure commonly used validators using a configuration shorthand.

The following validation rules can be configured in the validation section of the FileUpload attribute:

  • required
  • minFiles
  • maxFiles
  • fileExtension (for \TYPO3\CMS\Extbase\Validation\Validator\FileExtensionValidator )
  • fileSize (for \TYPO3\CMS\Extbase\Validation\Validator\FilesizeValidator)
  • imageDimensions (for \TYPO3\CMS\Extbase\Validation\Validator\ImageDimensionsValidator )
  • mimeType (for \TYPO3\CMS\Extbase\Validation\Validator\MimeTypeValidator )
  • allowedMimeTypes (shorthand notation for configuration option allowedMimeTypes of the MimeTypeValidator)

Example:

#[FileUpload([
    'validation' => [
        'required' => true,
        'maxFiles' => 1,
        'fileSize' => ['minimum' => '0K', 'maximum' => '2M'],
        'mimeType' => ['allowedMimeTypes' => ['image/jpeg']],
        'fileExtension' => ['allowedFileExtensions' => ['jpg', 'jpeg']],
        'imageDimensions' => ['maxWidth' => 4096, 'maxHeight' => 4096]
    ],
    'uploadFolder' => '1:/user_upload/extbase_single_file/',
])]
Copied!

Extbase will internally use the Extbase file upload validators for fileExtensionMimeTypeConsistency, fileExtension, fileSize, mimeType and imageDimensions validation.

Custom validators can be created according to project requirements and must extend the Extbase AbstractValidator . The value to be validated is always a PSR-7 UploadedFile object. Custom validators can however not be used in the FileUpload attribute and must be configured manually.

Shorthand notation for allowedMimeTypes 

Using the mimeType configuration array, all options of the MimeTypeValidator can be set as sub-keys (since TYPO3 13.4.1):

#[FileUpload([
    'validation' => [
        'required' => true,
        'mimeType' => [
            'allowedMimeTypes' => ['image/jpeg'],
            'ignoreFileExtensionCheck' => false,
            'notAllowedMessage' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:validation.mimetype.notAllowedMessage',
            'invalidExtensionMessage' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:validation.mimetype.invalidExtensionMessage',
        ],
    ],
    'uploadFolder' => '1:/user_upload/files/',
])]
Copied!

The shorthand notation via 'allowedMimeTypes' continues to exist, in case only the mime type validation is needed. However, it is recommended to utilize the full 'mimeType' configuration array.

Deletion of uploaded files and file references 

The new Fluid ViewHelper Form.uploadDeleteCheckbox ViewHelper <f:form.uploadDeleteCheckbox> can be used to show a "delete file" checkbox in a form.

Example for object with FileReference property:

<f:form.uploadDeleteCheckbox property="file" fileReference="{object.file}" />
Copied!

Example for an object with an TYPO3\CMS\Extbase\Persistence\ObjectStorage<\TYPO3\CMS\Extbase\Domain\Model\FileReference> property, containing multiple files and allowing to delete the first one (iteration is possible within Fluid, to do that for every object of the collection):

<f:form.uploadDeleteCheckbox property="file.0" fileReference="{object.file}" />
Copied!

Extbase will then handle file deletion(s) before persisting a validated object. It will:

  • validate that minimum and maximum file upload configuration for the affected property is fulfilled (only if the property has a FileUpload )
  • delete the affected sys_file_reference record
  • delete the affected file

Internally, Extbase uses FileUploadDeletionConfiguration objects to track file deletions for properties of arguments. Files are deleted directly without checking whether the current file is referenced by other objects.

Apart from using this ViewHelper, it is of course still possible to manipulate FileReference properties with custom logic before persistence.

New PSR-14 events 

The following new PSR-14 event has been added to allow customization of file upload related tasks:

ModifyUploadedFileTargetFilenameEvent 

The ModifyUploadedFileTargetFilenameEvent allows event listeners to alter a filename of an uploaded file before it is persisted.

Event listeners can use the method getTargetFilename() to retrieve the filename used for persistence of a configured uploaded file. The filename can then be adjusted via setTargetFilename(). The relevant configuration can be retrieved via getConfiguration().

Impact 

Extension developers can use the new feature to implement file uploads and file deletions in Extbase extensions easily with commonly known Extbase property attributes/annotations.

Feature: #103521 - Change table restrictions UI to combine read and write permissions 

See forge#103521

Description 

The tables_select and tables_modify fields of the be_groups table store information about permissions to read and write into selected database tables.

Due to TYPO3's internal behavior, when write permissions are granted for some tables, those tables are also automatically available for reading.

To make managing table permissions much easier and more efficient for integrators, the separate form fields for Tables (listing) [tables_select] and Tables (modify) [tables_modify] have been combined into a single UI element. This field now offers separate radio buttons to define which tables the backend user group should have permission to read and / or write. This is done by selecting one of the "No Access", "Read" or "Read & Write" options.

To further improve the user experience, it is also possible to use the "Check All", "Uncheck All" and "Toggle Selection" options for each permission.

Under the hood, when these permissions are processed, they are still saved separately in the tables_select and tables_modify columns in the be_groups table, as they were before.

To render this new table view and handle its behavior, a dedicated form renderType tablePermission has been introduced, which is now set for the tables_modify column. The tables_select column has been changed to TCA type passthrough.

The new form element is defined through: \TYPO3\CMS\Backend\Form\Element\TablePermissionElement . It uses a dedicated data provider defined in: \TYPO3\CMS\Backend\Form\FormDataProvider\TcaTablePermission . The JavaScript code is handled by a new web component: @typo3/backend/form-engine/element/table-permission-element.js.

When the TcaTablePermission data provider handles the configuration, it reads table lists from both the tables_select and tables_modify columns and combines them into a single array with unique table names.

Impact 

Managing table permissions for backend user groups has been improved by visually combining the Tables (listing) [tables_select] and Tables (modify) [tables_modify] options, as well as by adding the multi record selection functionality.

Feature: #103576 - Allow defining opacity in TCA type=color element 

See forge#103576

Description 

A new boolean property opacity has been added to the TCA configuration of a TCA type color element to allow defining colors with an opacity using the RRGGBBAA color notation.

'my_color' => [
    'label' => 'My Color',
    'config' => [
        'type' => 'color',
        'opacity' => true,
    ],
],
Copied!

Impact 

If opacity is enabled, editors can select not only a color but also its opacity in a corresponding color element.

Feature: #103581 - Automatically transform TCA field values for record objects 

See forge#103581

Description 

With forge#103783 the new \TYPO3\CMS\Core\Domain\Record object has been introduced. It is an object representing a raw database record, based on TCA and is usually used in the frontend (via Fluid Templates), when fetching records with the RecordTransformationProcessor ( record-transformation) or by collecting content elements with the PageContentFetchingProcessor ( page-content).

The Records API - introduced together with the Schema API in forge#104002 - now expands the record's values for most common field types (known from the TCA Schema) from their raw database value into "rich-flavored" values, which might be Record , FileReference , \TYPO3\CMS\Core\Resource\Folder or \DateTimeImmutable objects.

This works for the following "relation" TCA types:

  • category
  • file
  • folder
  • group
  • inline
  • select with MM and foreign_table

In addition, the values of following TCA types are also resolved and expanded automatically:

  • datetime
  • flex
  • json
  • link
  • select with a static list of entries

Each of the fields receives a full-fledged resolved value, based on the field configuration from TCA.

In case of relations ( category, group, inline, select with MM and foreign_table), a collection ( LazyRecordCollection) of new Record objects is attached as value. In case of file, a collection ( LazyFileReferenceCollection) of FileReference objects and in case of type folder, a collection ( LazyFolderCollection) of Folder objects are attached.

Example 

<f:for each="{myContent.main.records}" as="record">
    <f:for each="{record.image}" as="image">
        <f:image image="{image}" />
    </f:for>
</f:for>
Copied!

New TCA option relationship 

In order to define cardinality on TCA level, the option relationship is introduced for all "relation" TCA types listed above. If this option is set to oneToOne or manyToOne, then relations are resolved directly without being wrapped into collection objects. In case the relation can not be resolved, NULL is returned.

'image' => [
    'config' => [
        'type' => 'file',
        'relationship' => 'manyToOne',
    ]
]
Copied!
<f:for each="{myContent.main.records}" as="record">
    <f:image image="{record.image}" />
</f:for>
Copied!

Field expansion 

For TCA type flex, the corresponding FlexForm is resolved and therefore all values within this FlexForm are processed and expanded as well.

Fields of TCA type datetime will be transformed into a full \DateTimeInterface object.

Fields of TCA type json will provide the decoded JSON value.

Fields of TCA type link will provide the \TYPO3\CMS\Core\LinkHandling\TypolinkParameter object, which is an object oriented representation of the corresponding TypoLink parameter configuration.

Fields of TCA type select without a relationship will always provide an array of static values.

Impact 

When using Record objects through the \TYPO3\CMS\Core\Domain\RecordFactory API, e.g. via RecordTransformationProcessor ( record-transformation) or PageContentFetchingProcessor (page-content), the corresponding Record objects are now automatically processed and enriched.

Those can not only be used in the frontend but also for Backend Previews in the page module. This is possible by configuring a Fluid Template via Page TSconfig to be used for the page preview rendering:

mod.web_layout.tt_content.preview {
    textmedia = EXT:site/Resources/Private/Templates/Preview/Textmedia.html
}
Copied!

In such template the newly available variable {record} can be used to access the resolved field values. It is advised to migrate existing preview templates to this new object, as the former values will probably vanish in the next major version.

By utilizing the new API for fetching records and content elements, the need for further data processors, e.g. FilesProcessor ( files), becomes superfluous since all relations are resolved automatically when requested.

Feature: #103789 - Add "close"-button to page layout, if returnUrl is set 

See forge#103789

Description 

A "close"-button is now displayed in the page module, if the returnUrl argument is set. When this button is clicked, the previous module leading to the page module (or a custom link defined in returnUrl) will be displayed again.

In order to utilize this, backend module links set in extensions must pass the returnUrl argument. If returnUrl is not set, the "close"-button will not be displayed.

Examples 

Here is an example, using the Fluid <be:moduleLink> ViewHelper:

Fluid example
<a href="{be:moduleLink(route:'web_layout', arguments:'{id:pageUid, returnUrl: returnUrl}')}"
   class="btn btn-default"
   title="{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:title')}">
    <core:icon identifier="actions-document" size="small"/>
</a>
Copied!

The behaviour is similar to the <be:uri.editRecord> ViewHelper, where setting the returnUrl argument will also cause a "close"-button to be displayed.

The returnUrl should usually return to the calling (originating) module.

You can build the returnUrl with the Fluid ViewHelper be:uri:

Fluid example for building returnUrl to module "linkvalidator"
<f:be.uri route="web_linkvalidator" parameters="{id: pageUid}"/>
Copied!

Here is an example for building the returnUrl via PHP:

Backend module controller
use TYPO3\CMS\Backend\Routing\UriBuilder;

public function __construct(
    protected readonly UriBuilder $uriBuilder
) {}

protected function generateModuleUri(array $parameters = []): string
{
    return $this->uriBuilder->buildUriFromRoute('web_linkvalidator',  $parameters);
}

public function __invoke(ServerRequestInterface $request): ResponseInterface
{
    // ...
    $this->view->assign('returnUrl', $this->generateModuleUri(['pageUid' => $this->id]));
    // ...
}
Copied!

Impact 

The change has no impact, unless the functionality is being used. Extension authors can make use of the new functionality to also conveniently link back to an originating or custom module for a streamlined linear backend user-experience.

Feature: #104126 - Add configuration setting to define backend-locking file 

See forge#104126

Description 

TYPO3 supports the ability to lock the backend for maintenance reasons. This is controlled with a LOCK_BACKEND file that was previously stored in typo3conf/.

With Important: #104126 - Drop "typo3conf" directory from system status check and backend locking this directory is no longer needed, so now the location to this file can be adjusted via the new configuration setting $GLOBALS['TYPO3_CONF_VARS']['BE']['lockBackendFile'] .

When empty, it falls back to a file LOCK_BACKEND, which is now stored by default in:

  • var/lock/ for Composer Mode
  • config/ for Legacy Mode

If you previously manually maintained the LOCK_BACKEND file (for example via deployment or other maintenance automation), please either adjust your automations to the new file location, or change the setting to the desired file location, or at best use the CLI commands vendor/bin/typo3 backend:lock and vendor/bin/typo3 backend:unlock.

The backend locking functionality is now contained in a distinct service class \TYPO3\CMS\Backend\Authentication\BackendLocker to allow future flexibility.

When upgrading an installation to Composer Mode with a locked backend in effect, please ensure your backend can remain locked by moving (or copying) the file to the new location var/lock/.

Remember, if you want locked backend state to persist between deployments, ensure that the used directory (var/lock by default) is shared between deployment releases.

Impact 

The location for LOCK_BACKEND to lock (and unlock) the backend can now be controlled by maintainers of a TYPO3 installation, and has moved outside of typo3conf/ by default to either var/lock/ (Composer) or config/ (Legacy).

Feature: #104168 - PSR-14 event for modifying countries 

See forge#104168

Description 

A new PSR-14 event \TYPO3\CMS\Core\Country\Event\BeforeCountriesEvaluatedEvent has been introduced to modify the list of countries provided by \TYPO3\CMS\Core\Country\CountryProvider .

This event allows to add, remove and alter countries from the list used by the provider class itself and ViewHelpers like <f:form.countrySelect />.

Example 

An example corresponding event listener class:

<?php
declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Country\Country;
use TYPO3\CMS\Core\Country\Event\BeforeCountriesEvaluatedEvent;

final readonly class EventListener
{
    #[AsEventListener(identifier: 'my-extension/before-countries-evaluated')]
    public function __invoke(BeforeCountriesEvaluatedEvent $event): void
    {
        $countries = $event->getCountries();
        unset($countries['BS']);
        $countries['XX'] = new Country(
            'XX',
            'XYZ',
            'Magic Kingdom',
            '987',
            '🔮',
            'Kingdom of Magic and Wonders'
        );
        $event->setCountries($countries);
    }
}
Copied!
EXT:my_extension/ext_localconf.php
$GLOBALS['TYPO3_CONF_VARS']['SYS']['locallangXMLOverride']
    ['EXT:core/Resources/Private/Language/Iso/countries.xlf'][]
        = 'EXT:my_extension/Resources/Private/Language/countries.xlf';
Copied!
EXT:my_extension/Resources/Private/Language/countries.xlf
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<xliff version="1.0">
    <file source-language="en" datatype="plaintext" date="2024-01-08T18:44:59Z" product-name="my_extension">
        <body>
            <trans-unit id="XX.name" approved="yes">
                <source>Magic Kingdom</source>
            </trans-unit>
            <trans-unit id="XX.official_name" approved="yes">
                <source>Kingdom of Magic and Wonders</source>
            </trans-unit>
        </body>
    </file>
</xliff>
Copied!

Impact 

Using the PSR-14 event BeforeCountriesEvaluatedEvent allows modification of countries provided by CountryProvider .

Feature: #104221 - PSR-14 events for RTE <-> Persistence transformations 

See forge#104221

Description 

When using an RTE HTML content element, two transformations take place within the TYPO3 backend:

  • From database: Fetching the current content from the database (persistence) and preparing it to be displayed inside the RTE HTML component.
  • To database: Retrieving the data returned by the RTE and preparing it to be persisted into the database.

This takes place in the \TYPO3\CMS\Core\Html\RteHtmlParser class, by utilizing the methods transformTextForRichTextEditor and transformTextForPersistence.

With forge#96107 and forge#92992, the former hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_parsehtml_proc.php']['transformation'] was removed, which took care of applying custom user-transformations. The suggested replacement for this was to use the actual RTE YAML configuration and API like allowAttributes.

Now, four PSR-14 Events are introduced to allow more granular control over data for persistence -> RTE and RTE -> persistence. This allows developers to apply more customized transformations, apart from the internal and API ones:

Modify data when saving RTE content to the database (persistence):

  • \TYPO3\CMS\Core\Html\Event\BeforeTransformTextForPersistenceEvent
  • \TYPO3\CMS\Core\Html\Event\AfterTransformTextForPersistenceEvent

Modify data when retrieving content from the database and pass to the RTE:

  • \TYPO3\CMS\Core\Html\Event\BeforeTransformTextForRichTextEditorEvent
  • \TYPO3\CMS\Core\Html\Event\AfterTransformTextForRichTextEditorEvent

All four events have the same structure (for now):

  • getHtmlContent() - retrieve the current HTML content
  • setHtmlContent() - used to set modifications of the HTML content
  • getInitialHtmlContent() - retrieve the untampered initial HTML content
  • getProcessingConfiguration() - retrieve processing configuration array

The event is meant to be used so that developers can change the HTML content either before the internal TYPO3 modifications, or after those.

The before events are executed before TYPO3 applied any kind of internal transformations, like for links. Event Listeners that want to modify output so that TYPO3 additionally operates on that, should listen to those before-Events.

When Event Listeners want to perform on the final result, the corresponding after-Events should be utilized.

Event listeners can use $value = $event->getHtmlContent() to get the current contents, apply changes to $value and then store the manipulated data via $event->setHtmlContent($value), see example:

Example 

An event listener class is constructed which will take an RTE input TYPO3 and internally store it in the database as [tag:typo3]. This could allow a content element data processor in the frontend to handle this part of the content with for example internal glossary operations.

The workflow would be:

  • Editor enters "TYPO3" in the RTE instance.
  • When saving, this gets stored as "[tag:typo3]".
  • When the editor sees the RTE instance again, "[tag:typo3]" gets replaced to "TYPO3" again.
  • So: The editor will always only see "TYPO3" and not know how it is internally handled.
  • The frontend output receives "[tag:typo3]" and could do its own content element magic, other services accessing the database could also use the parseable representation.

The corresponding event listener class:

EXT:MyExtension/Classes/EventListener/TransformListener.php
<?php
declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;

class TransformListener
{
    /**
     * Transforms the current value the RTE delivered into a value that is stored (persisted) in the database.
     */
    #[AsEventListener('rtehtmlparser/modify-data-for-persistence')]
    public function modifyPersistence(AfterTransformTextForPersistenceEvent $event): void
    {
        $value = $event->getHtmlContent();
        $value = str_replace('TYPO3', '[tag:typo3]', $value);
        $event->setHtmlContent($value);
    }

    /**
     * Transforms the current persisted value into something the RTE can display
     */
    #[AsEventListener('rtehtmlparser/modify-data-for-richtexteditor')]
    public function modifyRichTextEditor(AfterTransformTextForRichTextEditorEvent $event): void
    {
        $value = $event->getHtmlContent();
        $value = str_replace('[tag:typo3]', 'TYPO3', $value);
        $event->setHtmlContent($value);
    }
}
Copied!

Impact 

Using the new PSR-14 events

  • \TYPO3\CMS\Core\Html\Event\BeforeTransformTextForPersistenceEvent
  • \TYPO3\CMS\Core\Html\Event\AfterTransformTextForPersistenceEvent
  • \TYPO3\CMS\Core\Html\Event\BeforeTransformTextForRichTextEditorEvent
  • \TYPO3\CMS\Core\Html\Event\AfterTransformTextForRichTextEditorEvent

allows to apply custom transformations for database <-> RTE content transformations.

Feature: #104311 - Auto created system TCA columns 

See forge#104311

Description 

Introduction 

There are various TCA table ctrl settings that define fields used to enable certain TYPO3 table capabilities and to specify the database column to store this row state.

An example is $GLOBALS['TCA']['ctrl']['enablecolumns']['starttime'] = 'starttime' , which makes the table "start time aware", resulting in the automatic exclusion of a record if the given start time is in the future, when rendered in the frontend.

Such ctrl settings require TCA columns definitions. Default definitions of such columns are now automatically added to TCA if not manually configured. Extension developers can now remove and avoid a significant amount of boilerplate field definitions in columns and rely on TYPO3 Core to create them automatically. Note the Core does not automatically add such columns to TCA types or palettes definitions: Developers still need to place them, to show the columns when editing record rows, and need to add according access permissions.

Let us have a quick look on what happened within TCA and its surrounding code lately, to see how this feature embeds within the general TYPO3 Core strategy in this area and why the above feature has been implemented at this point in time:

TCA has always been a central cornerstone of TYPO3. The TYPO3 Core strives to maintain this central part while simplifying and streamlining less desirable details.

TYPO3 version v12 aimed to simplify single column definitions by implementing new column types like file, category, email, and more. These are much easier to understand and require far fewer single property definitions than previous solutions. With this in place, auto-creation of database column definitions derived from TCA has been established with TYPO3 v13, making the manual definition of database table schemas in ext_tables.sql largely unnecessary. Additionally, an object-oriented approach called TcaSchema has been introduced to harmonize and simplify information retrieval from TCA.

With the step described in this document - the auto-creation of TCA columns from ctrl properties - the amount of manual boilerplate definitions is significantly reduced, and the TYPO3 Core gains more control over these columns to harmonize these fields throughout the system. Note that the TYPO3 Core has not yet altered the structure of TCA types and palettes. This will be one of the next steps in this area, but details have not been decided upon yet.

All these steps streamline TCA and its surrounding areas, simplify the system, and reduce the amount of details developers need to be aware of when defining their own tables and fields.

This document details the "column auto-creation from 'ctrl' fields" feature: It first lists all affected settings with their derived default definitions. It concludes with a section relevant for instances that still need to override certain defaults of these columns by explaining the order of files and classes involved in building TCA and the available options to change defaults and where to place these changes.

Auto-created columns from 'ctrl' 

The configuration settings below enable single table capabilities. Their values are a database column name responsible for storing the row data of the capability.

If a setting is defined in a "base" TCA table file (Configuration/TCA, not in Configuration/TCA/Overrides), the TYPO3 Core will add default columns definition for this field name if no definition exists in a base file.

$GLOBALS['TCA']['ctrl']['enablecolumns']['disabled'] 

This setting makes database table rows "disable aware": A row with this flag being set to 1 is not rendered in the frontend to casual website users.

Typical usage:

'ctrl' => [
    'enablecolumns' => [
        'disabled' => 'disabled',
    ],
],
Copied!

Default configuration added by the TYPO3 Core:

'disabled' => [
    'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.enabled',
    'exclude' => true,
    'config' => [
        'type' => 'check',
        'renderType' => 'checkboxToggle',
        'default' => 0,
        'items' => [
            [
                'label' => '',
                'invertStateDisplay' => true,
            ],
        ],
    ],
],
Copied!

$GLOBALS['TCA']['ctrl']['enablecolumns']['starttime'] 

This setting makes database table rows "starttime aware": A row having a start time in the future is not rendered in the frontend.

Typical usage:

'ctrl' => [
    'enablecolumns' => [
        'starttime' => 'starttime',
    ],
],
Copied!

Default configuration added by the TYPO3 Core:

'starttime' => [
    'exclude' => true,
    'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.starttime',
    'config' => [
        'type' => 'datetime',
        'default' => 0,
    ],
],
Copied!

$GLOBALS['TCA']['ctrl']['enablecolumns']['endtime'] 

This setting makes database table rows "endtime aware": A row having an end time in the past is not rendered in the frontend.

Typical usage:

'ctrl' => [
    'enablecolumns' => [
        'endtime' => 'endtime',
    ],
],
Copied!

Default configuration added by the TYPO3 Core:

'endtime' => [
    'exclude' => true,
    'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.endtime',
    'config' => [
        'type' => 'datetime',
        'default' => 0,
        'range' => [
            'upper' => mktime(0, 0, 0, 1, 1, 2106),
        ],
    ],
],
Copied!

$GLOBALS['TCA']['ctrl']['enablecolumns']['fe_group'] 

This setting makes database table rows "frontend group aware": A row can be defined to be shown only to frontend users who are a member of selected groups.

Typical usage:

'ctrl' => [
    'enablecolumns' => [
        'fe_group' => 'fe_group',
    ],
],
Copied!

Default configuration added by the TYPO3 Core:

'fe_group' => [
    'exclude' => true,
    'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.fe_group',
    'config' => [
        'type' => 'select',
        'renderType' => 'selectMultipleSideBySide',
        'size' => 5,
        'maxitems' => 20,
        'items' => [
            [
                'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.hide_at_login',
                'value' => -1,
            ],
            [
                'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.any_login',
                'value' => -2,
            ],
            [
                'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.usergroups',
                'value' => '--div--',
            ],
        ],
        'exclusiveKeys' => '-1,-2',
        'foreign_table' => 'fe_groups',
    ],
],
Copied!

$GLOBALS['TCA']['ctrl']['editlock'] 

This setting makes database table rows "backend lock aware": A row with this being flag enabled can only be edited by backend administrators.

Typical usage:

'ctrl' => [
    'editlock' => 'editlock',
],
Copied!

Default configuration added by the TYPO3 Core:

'endtime' => [
    'displayCond' => 'HIDE_FOR_NON_ADMINS',
    'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:editlock',
    'config' => [
        'type' => 'check',
        'renderType' => 'checkboxToggle',
    ],
],
Copied!

$GLOBALS['TCA']['ctrl']['descriptionColumn'] 

This setting makes database table rows "description aware": Backend editors have a database field to add row specific notes.

Typical usage:

'ctrl' => [
    'descriptionColumn' => 'description',
],
Copied!

Default configuration added by the TYPO3 Core:

'description' => [
    'exclude' => true,
    'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.description',
    'config' => [
        'type' => 'text',
        'rows' => 5,
        'cols' => 30,
        'max' => 2000,
    ],
],
Copied!

$GLOBALS['TCA']['ctrl']['languageField'] and transOrigPointerField` 

These setting make database table rows "localization aware": Backend editors can create localized versions of a record. Note when languageField is set, and transOrigPointerField is not, the TYPO3 Core will automatically set transOrigPointerField to l10n_parent since both fields must be always set in combination.

Typical usage:

'ctrl' => [
    'languageField' => 'sys_language_uid',
    'transOrigPointerField' => 'l10n_parent',
],
Copied!

Default configuration added by the TYPO3 Core, note string $table corresponds to the current table name.

'sys_language_uid' => [
    'exclude' => true,
    'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language',
    'config' => [
        'type' => 'language',
    ],
],
'l10n_parent' => [
    'displayCond' => 'FIELD:sys_language_uid:>:0',
    'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent',
    'config' => [
        'type' => 'select',
        'renderType' => 'selectSingle',
        'items' => [
            [
                'label' => '',
                'value' => 0,
            ],
        ],
        'foreign_table' => $table,
        'foreign_table_where' => 'AND {#' . $table . '}.{#pid}=###CURRENT_PID### AND {#' . $table . '}.{#' . $languageFieldName . '} IN (-1,0)',
        'default' => 0,
    ],
],
Copied!

$GLOBALS['TCA']['ctrl']['transOrigDiffSourceField'] 

This setting makes database table rows "parent language record change aware": Backend editors can have an indicator when the parent column has been changed.

Typical usage:

'ctrl' => [
    'transOrigDiffSourceField' = 'l10n_diffsource',
],
Copied!

Default configuration added by the TYPO3 Core:

'l10n_diffsource' => [
    'config' => [
        'type' => 'passthrough',
        'default' => '',
    ],
],
Copied!

$GLOBALS['TCA']['ctrl']['translationSource'] 

This setting makes database table rows "parent language source aware" to determine the difference between "connected mode" and "free mode".

Typical usage:

'ctrl' => [
    'translationSource' = 'l10n_source',
],
Copied!

Default configuration added by the TYPO3 Core:

'l10n_source' => [
    'config' => [
        'type' => 'passthrough',
        'default' => '',
    ],
],
Copied!

Load order when building TCA 

To understand if and when TCA column auto-creation from ctrl definitions kicks in, it is important to have an overview of the order of the single loading steps:

  1. Load single files from extension Configuration/TCA files
  2. NEW - Enrich columns from ctrl settings
  3. Load single files from extension Configuration/TCA/Overrides files
  4. Apply TCA migrations
  5. Apply TCA preparations

As a result of this strategy, columns fields are not auto-created, when a ctrl capability is added in a Configuration/TCA/Overrides file, and not in a Configuration/TCA "base" file. In general, such capabilities should be set in base files only: Adding them at a later point - for example in a different extension - is brittle and there is a risk the main extension can not deal with such an added capability properly.

Overriding definitions from auto-created TCA columns 

I most cases, developers do not need to change definitions of columns auto-created by the TYPO3 Core. In general, it is advisable to not actively do this. Developers who still want to change detail properties of such columns should generally stick to "display" related details only.

There are two options to have own definitions: When a column is already defined in a "base" TCA file (Configuration/TCA), the TYPO3 Core will not override it. Alternatively, a developer can decide to let the TYPO3 Core auto-create a column, to then override single properties in Configuration/TCA/Overrides files.

As example, "base" pages file defines this (step 1 above):

'ctrl' => [
    'enablecolumns' => [
        'disabled' => 'disabled',
    ],
],
Copied!

The TYPO3 Core thus creates this columns definition (step 2 above):

'columns' => [
    'disabled' => [
        'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.enabled',
        'exclude' => true,
        'config' => [
            'type' => 'check',
            'renderType' => 'checkboxToggle',
            'default' => 0,
            'items' => [
                [
                    'label' => '',
                    'invertStateDisplay' => true,
                ],
            ],
        ],
    ],
],
Copied!

When an editor creates a new page, it should be "disabled" by default to avoid having a new page online in the website before it is set up completely. A Configuration/TCA/Overrides/pages.php file does this:

<?php
// New pages are disabled by default
$GLOBALS['TCA']['pages']['columns']['hidden']['config']['default'] = 1;
Copied!

Impact 

Extension developers can typically remove columns definitions of all the above fields and rely on TYPO3 Core creating them with a good default definition.

It is only required to define the desired table capabilities in ctrl with its field names, and the system will create the according columns definitions automatically.

Feature: #104321 - Allow handling of argument mapping exceptions in ActionController 

See forge#104321

Description 

A new method handleArgumentMappingExceptions has been introduced in Extbase \TYPO3\CMS\Extbase\Mvc\Controller\ActionController to improve handling of exceptions that occur during argument mapping.

The new method supports optional handling of the following exceptions:

  • \TYPO3\CMS\Extbase\Property\Exception\TargetNotFoundException , which occurs, when a given object UID can not be resolved to an existing record.
  • \TYPO3\CMS\Extbase\Mvc\Controller\Exception\RequiredArgumentMissingException , which occurs, when a required action argument is missing.

Handling of the exceptions can be enabled globally with the following TypoScript configuration.

  • config.tx_extbase.mvc.showPageNotFoundIfTargetNotFoundException = 1
  • config.tx_extbase.mvc.showPageNotFoundIfRequiredArgumentIsMissingException = 1

The exception handling can also be configured on extension level with the following TypoScript configuration.

  • plugin.tx_yourextension.mvc.showPageNotFoundIfTargetNotFoundException = 1
  • plugin.tx_yourextension.mvc.showPageNotFoundIfRequiredArgumentIsMissingException = 1
  • plugin.tx_yourextension_plugin1.mvc.showPageNotFoundIfTargetNotFoundException = 1
  • plugin.tx_yourextension_plugin1.mvc.showPageNotFoundIfRequiredArgumentIsMissingException = 1

By default, these options are set to 0, which will lead to exceptions being thrown (and would lead to errors, if not caught). This is the current behavior of TYPO3.

When setting one of these values to 1, the configured exceptions will not be thrown. Instead, a pageNotFound response is propagated, resulting in a 404 error being shown.

Additionally, extension authors can extend or override the method handleArgumentMappingExceptions in relevant Controllers in order to implement custom argument mapping exception handling.

Impact 

Extension authors can now handle exceptions in implementations of a ActionController , which are thrown during argument mapping.

Feature: #104451 - Redis backends support for key prefixing 

See forge#104451

Description 

It is now possible to add a dedicated key prefix for all invocations of a Redis cache or session backend. This allows to use the same Redis database for multiple caches or even for multiple TYPO3 instances if the provided prefix is unique.

Possible use cases are:

  • Using Redis caching for multiple caches, if only one Redis database is available
  • Pre-fill caches upon deployments using a new prefix (zero downtime deployments)
additional.php example for using Redis as session backend
$GLOBALS['TYPO3_CONF_VARS']['SYS']['session']['BE'] = [
    'backend' => \TYPO3\CMS\Core\Session\Backend\RedisSessionBackend::class,
    'options' => [
        'hostname' => 'redis',
        'database' => '11',
        'compression' => true,
        'keyPrefix' => 'be_sessions_',
    ],
];
$GLOBALS['TYPO3_CONF_VARS']['SYS']['session']['FE'] = [
    'backend' => \TYPO3\CMS\Core\Session\Backend\RedisSessionBackend::class,
    'options' => [
        'hostname' => 'redis',
        'database' => '11',
        'compression' => true,
        'keyPrefix' => 'fe_sessions_',
        'has_anonymous' => true,
    ],
];
Copied!
additional.php example for pages cache
$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['pages'] = [
    'backend' => \TYPO3\CMS\Core\Cache\Backend\RedisBackend::class,
    'options' => [
        'hostname' => 'redis',
        'database' => 11,
        'compression' => true,
        'keyPrefix' => 'pages_';
    ],
];

$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['rootline'] = [
    'backend' => \TYPO3\CMS\Core\Cache\Backend\RedisBackend::class,
    'options' => [
        'hostname' => 'redis',
        'database' => 11,
        'compression' => true,
        'keyPrefix' => 'rootline_';
    ],
];
Copied!

Impact 

The new feature allows to use the same Redis database for multiple caches or even for multiple TYPO3 instances while having no impact on existing configuration.

Feature: #104482 - Add if() support to ExpressionBuilder 

See forge#104482

Description 

The TYPO3 \TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder provides a new method to phrase "if-then-else" expressions. Those are translated into IF or CASE statements depending on the used database engine.

ExpressionBuilder::if() 

Creates an IF-THEN-ELSE expression.

Method signature
/**
 * Creates IF-THEN-ELSE expression construct compatible with all supported database vendors.
 * No automatic quoting or escaping is done, which allows to build up nested expression statements.
 *
 * **Example:**
 * ```
 * $queryBuilder
 *   ->selectLiteral(
 *     $queryBuilder->expr()->if(
 *       $queryBuilder->expr()->eq('hidden', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)),
 *       $queryBuilder->quote('page-is-visible'),
 *       $queryBuilder->quote('page-is-not-visible'),
 *       'result_field_name'
 *     ),
 *   )
 *   ->from('pages');
 * ```
 *
 * **Result with MySQL:**
 * ```
 * SELECT (IF(`hidden` = 0, 'page-is-visible', 'page-is-not-visible')) AS `result_field_name` FROM `pages`
 * ```
 */
public function if(
    CompositeExpression|\Doctrine\DBAL\Query\Expression\CompositeExpression|\Stringable|string $condition,
    \Stringable|string $truePart,
    \Stringable|string $falsePart,
    \Stringable|string|null $as = null
): string {
    $platform = $this->connection->getDatabasePlatform();
    $pattern = match (true) {
        $platform instanceof DoctrineSQLitePlatform => 'IIF(%s, %s, %s)',
        $platform instanceof DoctrinePostgreSQLPlatform => 'CASE WHEN %s THEN %s ELSE %s END',
        $platform instanceof DoctrineMariaDBPlatform,
        $platform instanceof DoctrineMySQLPlatform => 'IF(%s, %s, %s)',
        default => throw new \RuntimeException(
            sprintf('Platform "%s" not supported for "%s"', $platform::class, __METHOD__),
            1721806463
        )
    };
    $expression = sprintf($pattern, $condition, $truePart, $falsePart);
    if ($as !== null) {
        $expression = $this->as(sprintf('(%s)', $expression), $as);
    }
    return $expression;
}
Copied!

Impact 

Extension authors can use the new expression method to build more advanced queries without the requirement to deal with the correct implementation for all supported database vendors.

Feature: #104493 - Add castText() expression support to ExpressionBuilder 

See forge#104493

Description 

The TYPO3 \TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder provides a new method to cast expression results to text like datatypes. This is done to large VARCHAR/CHAR types using the CAST/CONVERT or similar methods based on the used database engine.

ExpressionBuilder::castText() 

Creates a CAST expression.

Method signature
/**
 * Creates a cast for the `$expression` result to a text datatype depending on the database management system.
 *
 * Note that for MySQL/MariaDB the corresponding CHAR/VARCHAR types are used with a length of `16383` reflecting
 * 65554 bytes with `utf8mb4` and working with default `max_packet_size=16KB`. For SQLite and PostgreSQL the text
 * type conversion is used.
 *
 * Main purpose of this expression is to use it in a expression chain to convert non-text values to text in chain
 * with other expressions, for example to {@see self::concat()} multiple values or to ensure the type,  within
 * `UNION/UNION ALL` query parts for example in recursive `Common Table Expressions` parts.
 *
 * This is a replacement for {@see QueryBuilder::castFieldToTextType()} with minor adjustments like enforcing and
 * limiting the size to a fixed variant to be more usable in sensible areas like `Common Table Expressions`.
 *
 * Alternatively the {@see self::castVarchar()} can be used which allows for dynamic length setting per expression
 * call.
 *
 * **Example:**
 * ```
 * $queryBuilder->expr()->castText(
 *    '(' . '1 * 10' . ')',
 *    'virtual_field'
 * );
 * ```
 *
 * **Result with MySQL:**
 * ```
 * CAST((1 * 10) AS CHAR(16383) AS `virtual_field`
 * ```
 *
 * @throws \RuntimeException when used with a unsupported platform.
 */
public function castText(CompositeExpression|\Stringable|string $expression, string $asIdentifier = ''): string
{
    $platform = $this->connection->getDatabasePlatform();
    if ($platform instanceof DoctrinePostgreSQLPlatform) {
        return $this->as(sprintf('((%s)::%s)', $expression, 'text'), $asIdentifier);
    }
    if ($platform instanceof DoctrineSQLitePlatform) {
        return $this->as(sprintf('(CAST((%s) AS %s))', $expression, 'TEXT'), $asIdentifier);
    }
    if ($platform instanceof DoctrineMariaDBPlatform) {
        // 16383 is the maximum for a VARCHAR field with `utf8mb4`
        return $this->as(sprintf('(CAST((%s) AS %s(%s)))', $expression, 'VARCHAR', '16383'), $asIdentifier);
    }
    if ($platform instanceof DoctrineMySQLPlatform) {
        // 16383 is the maximum for a VARCHAR field with `utf8mb4`
        return $this->as(sprintf('(CAST((%s) AS %s(%s)))', $expression, 'CHAR', '16383'), $asIdentifier);
    }
    throw new \RuntimeException(
        sprintf(
            '%s is not implemented for the used database platform "%s", yet!',
            __METHOD__,
            get_class($this->connection->getDatabasePlatform())
        ),
        1722105672
    );
}
Copied!

Impact 

Extension authors can use the new expression method to build more advanced queries without the requirement to deal with the correct implementation for all supported database vendors - at least to some grade.

Feature: #104526 - Provide validators for PSR-7 UploadedFile objects in Extbase 

See forge#104526

Description 

4 new Extbase validators have been added to allow common validation tasks of a PSR-7 \TYPO3\CMS\Core\Http\UploadedFile object or an \TYPO3\CMS\Extbase\Persistence\ObjectStorage containing PSR-7 UploadedFile objects.

Note, that the new validators can only be applied to the TYPO3 implementation of the PSR-7 \Psr\Http\Message\UploadedFileInterface because they validate the uploaded files before it has been moved.

Custom implementations of the \UploadedFileInterface must continue to implement their own validators.

FileNameValidator 

This validator ensures, that files with PHP executable file extensions can not be uploaded. The validator has no options.

FileSizeValidator 

This validator can be used to validate an uploaded file against a given minimum and maximum file size.

Validator options:

  • minimum - The minimum size as string (e.g. 100K)
  • maximum - The maximum size as string (e.g. 100K)

MimeTypeValidator 

This validator can be used to validate an uploaded file against a given set of accepted MIME types. The validator additionally verifies, that the given file extension of the uploaded file matches allowed file extensions for the detected mime type.

Validator options:

  • allowedMimeTypes - An array of allowed MIME types
  • ignoreFileExtensionCheck - If set to "true", it is checked, the file extension check is disabled

ImageDimensionsValidator 

This validator can be used to validate an uploaded image for given image dimensions. The validator must only be used, when it is ensured, that the uploaded file is an image (e.g. by validating the MIME type).

Validator options:

  • width - Fixed width of the image as integer
  • height - Fixed height of the image as integer
  • minWidth - Minimum width of the image as integer. Default is 0
  • maxWidth - Maximum width of the image as integer. Default is PHP_INT_MAX
  • minHeight - Minimum height of the image as integer. Default is 0
  • maxHeight - Maximum height of the image as integer. Default is PHP_INT_MAX

Impact 

TYPO3 extension autors can now use the new validators to validate a given UploadedFile object.

Feature: #104631 - Add UNION Clause support to the QueryBuilder 

See forge#104631

Description 

The UNION clause is used to combine the result sets of two or more SELECT statements, which all database vendors support, each with their own specific variations.

However, there is a commonly shared subset that works across all of them:

SELECT column_name(s) FROM table1
WHERE ...

UNION <ALL | DISTINCT>

SELECT column_name(s) FROM table2
WHERE ...

ORDER BY ...
LIMIT x OFFSET y
Copied!

with shared requirements:

  • Each SELECT must return the same fields in number, naming and order.
  • Each SELECT must not have ORDER BY, expect MySQL allowing it to be used as sub query expression encapsulated in parentheses.

Generic UNION clause support has been contributed to Doctrine DBAL and is included since Release 4.1.0 which introduces two new API method on the \QueryBuilder:

  • union(string|QueryBuilder $part) to create first UNION query part
  • addUnion(string|QueryBuilder $part, UnionType $type = UnionType::DISTINCT) to add additional UNION (ALL|DISTINCT) query parts with the selected union query type.

TYPO3 decorates the Doctrine DBAL \QueryBuilder to provide for most API methods automatic quoting of identifiers and values and to apply database restrictions automatically for SELECT queries.

The Doctrine DBAL API has been adopted now to provide the same surface for the TYPO3 \TYPO3\CMS\Core\Database\Query\QueryBuilder and the intermediate \TYPO3\CMS\Core\Database\Query\ConcreteQueryBuilder to make it easier to create UNION clause queries. The API on both methods allows to provide dedicated QueryBuilder instances or direct queries as strings in case it is needed.

In queries containing subqueries, only named placeholders (such as :username) can be used and must be registered on the outermost QueryBuilder object, similar to advanced query creation with SUB QUERIES.

UnionType::DISTINCT and UnionType::ALL 

Each subsequent part needs to be defined either as UNION DISTINCT or UNION ALL which could have not so obvious effects.

For example, using UNION ALL for all parts in between except for the last one would generate larger result sets first, but discards duplicates when adding the last result set. On the other side, using UNION ALL tells the query optimizer not to scan for duplicates and remove them at all which can be a performance improvement - if you can deal with duplicates it can be ensured that each part does not produce same outputs.

Example: Compose a UNION clause query 

Custom service class using a UNION query to retrieve data.
use Doctrine\DBAL\Query\UnionType;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;

final readonly class MyService {
  public function __construct(
    private ConnectionPool $connectionPool,
  ) {}

  public function executeUnionQuery(
    int $pageIdOne,
    int $pageIdTwo,
  ): ?array {
    $connection = $this->connectionPool->getConnectionForTable('pages');
    $unionQueryBuilder = $connection->createQueryBuilder();
    $firstPartQueryBuilder = $connection->createQueryBuilder();
    $secondPartQueryBuilder = $connection->createQueryBuilder();
    // removing automatic TYPO3 restriction for the sake of the example
    // to match the PLAIN SQL example when executed. Not removing them
    // will generate corresponding restriction SQL code for each part.
    $firstPartQueryBuilder->getRestrictions()->removeAll();
    $secondPartQueryBuilder->getRestrictions()->removeAll();
    $expr = $unionQueryBuilder->expr();

    $firstPartQueryBuilder
      // The query parts **must** have the same column counts, and these
      // columns **must** have compatible types
      ->select('uid', 'pid', 'title')
      ->from('pages')
      ->where(
        $expr->eq(
          'pages.uid',
          // !!! Ensure to use most outer / top / main QueryBuilder
          //   instance for creating parameters and the complete
          //   query can be executed in the end.
          $unionQueryBuilder->createNamedParameter($pageIdOne, Connection::PARAM_INT),
        )
      );
    $secondPartQueryBuilder
      ->select('uid', 'pid', 'title')
      ->from('pages')
      ->where(
        $expr->eq(
          'pages.uid',
          // !!! Ensure to use most outer / top / main QueryBuilder instance
          $unionQueryBuilder->createNamedParameter($pageIdTwo, Connection::PARAM_INT),
        )
      );

    // Set first and second union part to the main (union)
    // QueryBuilder and return the retrieved rows.
    return $unionQueryBuilder
      ->union($firstPartQueryBuilder)
      ->addUnion($secondPartQueryBuilder, UnionType::DISTINCT)
      ->orderBy('uid', 'ASC')
      ->executeQuery()
      ->fetchAllAssociative();
  }
}
Copied!

This would create the following query for MySQL with $pageIdOne = 100 and $pageIdTwo = 10:

    (SELECT `uid`, `pid`, `title` FROM pages WHERE `pages`.`uid` = 100)
UNION
    (SELECT `uid`, `pid`, `title` FROM pages WHERE `pages`.`uid` = 10)
ORDER BY `uid` ASC
Copied!

Impact 

Extension authors can use the new QueryBuilder methods to build more advanced queries.

Feature: #104655 - Add console command to mark upgrade wizards as undone 

See forge#104655

Description 

A new CLI command typo3 upgrade:mark:undone has been introduced. It allows to mark a previously executed upgrade wizard as "undone", so it can be run again.

This makes the existing functionality from the install tool also available on CLI.

Impact 

You can now mark an already executed upgrade wizard as "undone" with typo3 upgrade:mark:undone <wizardIdentifier>

Feature: #104773 - Generic view factory 

See forge#104773

Description 

Class \TYPO3\CMS\Core\View\ViewFactoryInterface has been added as a generic view interface to create views that return an instance of \TYPO3\CMS\Core\View\ViewInterface . This implements the "V" of "MVC" in a generic way and is used throughout the TYPO3 Core.

This obsoletes all custom view instance creation approaches within the TYPO3 Core and within TYPO3 extensions. Extensions should retrieve view instances based on this ViewFactoryInterface .

Impact 

Instances of this interface should be injected using dependency injection. The default injected implementation is a Fluid view, and can be reconfigured using dependency injection configuration, typically in a Services.yaml file.

A casual example to create and render a view looks like this.

use TYPO3\CMS\Core\View\ViewFactoryInterface;

class MyController
{
    public function __construct(
        private readonly ViewFactoryInterface $viewFactory,
    ) {}

    public function myAction(ServerRequestInterface $request): string
    {
        $viewFactoryData = new ViewFactoryData(
            templateRootPaths: ['EXT:myExt/Resources/Private/Templates'],
            partialRootPaths: ['EXT:myExt/Resources/Private/Partials'],
            layoutRootPaths: ['EXT:myExt/Resources/Private/Layouts'],
            request: $request,
        );
        $view = $this->viewFactory->create($viewFactoryData);
        $view->assign('myData', 'myData');
        return $view->render('path/to/template');
    }
}
Copied!

Note Extbase-based extensions create a view instance based on this factory by default and are accessible as $this->view.

Feature: #104789 - Support for contentArgumentName in AbstractViewHelper 

See forge#104789

Description 

ContentArgumentName has been a feature on Fluid ViewHelpers for some time now. It allows ViewHelpers to link a ViewHelper argument to the children of the ViewHelper name. As a result, an input value can either be specified as an argument or as the ViewHelper's children, leading to the same result.

Example:

<!-- Tag syntax -->
<f:format.json value="{data}" />
<f:format.json>{data}</f:format.json>

<!-- Inline syntax -->
{f:format.json(value: data)}
{data -> f:format.json()}
Copied!

Previously, this feature was only available to ViewHelpers using the trait \CompileWithContentArgumentAndRenderStatic . It is now available to all ViewHelpers since it has been integrated into the \AbstractViewHelper . The trait is no longer necessary.

To use the new feature, all the ViewHelper implementation needs to do is to define a method getContentArgumentName() which returns the name of the argument to be linked to the ViewHelper's children:

Example:

public function getContentArgumentName(): string
{
    return 'value';
}
Copied!

Impact 

ViewHelpers using the trait \CompileWithContentArgumentAndRenderStatic should be migrated to the new feature.

\CompileWithContentArgumentAndRenderStatic will continue to work in Fluid v4, but will log a deprecation level error message. It will be removed in Fluid v5.

Feature: #104794 - Introduce Site Settings Editor 

See forge#104794

Description 

A new Site Settings editor has been introduced that allows to configure per-site settings in file:config/sites/*/settings.yaml.

The new backend module Site Management > Settings provides an overview of sites which offer configurable settings and makes them editable based on Site Set provided Settings Definitions.

The editor shows a list of settings categories and respective settings. It will persist all settings into config/sites/*/settings.yaml. The module will only persist settings that deviate from the site-scoped default value. That means it will only change the minimal difference to the settings set defined by the active sets for the respective site.

The backend module is currently available for administrators only, but will likely be extended to be made available for editors in future.

Anonymous (undefined) site settings – as supported since TYPO3 v10 – will not be made editable, but will be preserved as-is when persisting changes through the settings editor.

Categorization 

Sets can define categories and settings definitions can reference the category by ID in order to assign a setting to a specific category. These definitions are placed in settings.definitions.yaml next to the site set file config.yaml.

EXT:my_extension/Configuration/Sets/MySet/settings.definitions.yaml
categories:
  myCategory:
    label: 'My Category'

settings:
  my.example.setting:
    label: 'My example setting'
    category: myCategory
    type: string
    default: ''

  my.seoRelevantSetting:
    label: 'My SEO relevant setting'
    # show in EXT:seo provided category "seo"
    category: seo
    type: int
    default: 5
Copied!

The settings ordering is defined through the loading order of extensions and by the order of categories. Uncategorized settings will be grouped into a virtual "Other" category and shown at the end of the list of available settings.

Readonly 

Site settings can be made readonly. They can be overridden only by editing the config/sites/my-site/settings.yaml but not from within the editor.

The value of the field is displayed in a readonly field in the settings editor.

EXT:my_extension/Configuration/Sets/MySet/settings.definitions.yaml
settings:
  my.readonlySetting:
    label: 'My readonly setting'
    type: int
    default: 5
    readonly: true
Copied!

Enumeration of strings 

Site settings can provide possible options via the enum specifier, that will be selectable in the editor GUI:

EXT:my_extension/Configuration/Sets/MySet/settings.definitions.yaml
settings:
  my.enumSetting:
    label: 'My setting with options'
    type: string
    enum:
      valueA: 'Label of value A'
      valueB: 'Label of value B'
Copied!

Impact 

Site-scoped settings will most likely be the place to configure site-wide configuration, which was previously only possible to modify via Constant Editor, modifying TypoScript constants.

It is recommended to use site-sets and their UI configuration in favor of TypoScript Constants in the future.

Feature: #104814 - Automatically add system fields to content types 

See forge#104814

Description 

All content elements types ( CType) are usually equipped with the same system fields (language, hidden, etc.) - see also Feature: #104311 - Auto created system TCA columns. Adding them to the editor form has previously been done by adding those fields to each content types' showitem definition.

In the effort to simplify content element creation, to unify the available fields and position for the editor and to finally reduce configuration effort for integrators, those system fields are now added automatically based on the ctrl definition.

The following tabs / palettes are now added automatically:

  • The General tab with the general palette at the very beginning
  • The Language tab with the language palette after custom fields
  • The Access tab with the hidden and access palettes
  • The Notes tab with the rowDescription field

As mentioned, in case one of those palettes has been changed to no longer include the corresponding system fields, those fields are added individually depending on their definition in the table's ctrl section:

  • The ctrl[type] field (usually CType)
  • The colPos field
  • The ctrl[languageField] (usually sys_language_uid)
  • The ctrl[editlock] field (usually editlock)
  • The ctrl[enablecolumns][disabled] field (usually hidden)
  • The ctrl[enablecolumns][starttime] field (usually starttime)
  • The ctrl[enablecolumns][endtime] field (usually endtime)
  • The ctrl[enablecolumns][fe_group] field (usually fe_group)
  • The ctrl[descriptionColumn] field (usually rowDescription)

By default, all custom fields - the ones still defined in showitem - are added after the general palette and are therefore added to the General tab, unless a custom tab (e.g. Plugin, or Categories) is defined in between. It is also possible to start with a custom tab by defining a --div-- as the first item in the showitem. In this case, the General tab will be omitted.

All those system fields, which are added based on the ctrl section are also automatically removed from any custom palette and from the customized type's showitem definition.

If the content element defines the Extended tab, it will be inserted at the end, including all fields added to the type via API methods, without specifying a position, e.g. via ExtensionManagementUtility::addToAllTcaTypes().

Impact 

Creating content elements has been simplified by removing the need to define the system fields for each element again and again. This shrinks down a content element's showitem to just the element specific fields.

A usual migration will therefore look like the following:

Before:

'slider' => [
    'showitem' => '
        --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general,
            --palette--;;general,
            --palette--;;headers,
            slider_elements,
            bodytext;LLL:EXT:awesome_slider/Resources/Private/Language/locallang_ttc.xlf:bodytext.ALT.slider_description,
        --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:appearance,
            --palette--;;frames,
            --palette--;;appearanceLinks,
        --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language,
            --palette--;;language,
        --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access,
            --palette--;;hidden,
            --palette--;;access,
        --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:categories,
            categories,
        --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:notes,
            rowDescription,
        --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:extended,
    ',
],
Copied!

After:

'slider' => [
    'showitem' => '
            --palette--;;headers,
            slider_elements,
            bodytext;LLL:EXT:awesome_slider/Resources/Private/Language/locallang_ttc.xlf:bodytext.ALT.slider_description,
        --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:categories,
            categories,
        --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:extended,
    ',
],
Copied!

Since all fields, palettes and tabs, which are defined in the showitem are added after the general palette, also the Categories tab - if defined - is displayed before the system tabs / fields. The only special case is the Extended tab, which is always added at the end.

Feature: #104832 - PSR-14 Event to alter the results of PageTreeRepository 

See forge#104832

Description 

Until TYPO3 v9, it was possible to alter the rendering of one of TYPO3's superpowers — the page tree in the TYPO3 Backend User Interface.

This was done via a "Hook", but was removed due to the migration towards an SVG-based tree rendering.

As the Page Tree Rendering has evolved, and the hook system has been replaced in favor of PSR-14 Events, a new event \TYPO3\CMS\Backend\Tree\Repository\AfterRawPageRowPreparedEvent has been introduced.

Example 

The event listener class, using the PHP attribute #[AsEventListener] for registration, will remove any children for displaying for the page with the UID 123:

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Backend\Tree\Repository\AfterRawPageRowPreparedEvent;

final class MyEventListener
{
    #[AsEventListener]
    public function __invoke(AfterRawPageRowPreparedEvent $event): void
    {
        $rawPage = $event->getRawPage();
        if ((int)$rawPage['uid'] === 123) {
            $rawPage['_children'] = [];
            $event->setRawPage($rawPage);
        }
    }
}
Copied!

Impact 

Using the new PSR-14 event, it is now possible to modify the populated page properties or its children records.

Feature: #104844 - Add widgets for listing all the sys_notes inside the TYPO3 system 

See forge#104844

Description 

To make it easier for TYPO3 users to view all the internal notes (EXT:sys_note) in their TYPO3 system, TYPO3 now offers dashboard widgets for each internal note type. The backend user must have access to the sys_note table and view permission to the page where the record is located.

Impact 

TYPO3 users who have access to the Dashboard module and are granted access to the new widgets can now add and use these widgets.

Feature: #104846 - Custom field transformations for new records 

See forge#104846

Description 

With forge#103783 the new \TYPO3\CMS\Core\Domain\Record object has been introduced, which is an object representing a raw database record based on TCA and is usually used in the frontend (via Fluid Templates).

Since Feature: #103581 - Automatically transform TCA field values for record objects the properties of those Record objects are transformed / expanded from their raw database value into "rich-flavored" values. Those values might be relations to e.g. Record , FileReference , Folder or \DateTimeImmutable objects.

However, TYPO3 does not know about custom field meanings, e.g. latitude and longitude information, stored in an input field or user settings stored as JSON in an TCA type json field. For such custom needs, the new PSR-14 \TYPO3\CMS\Core\Domain\Event\RecordCreationEvent has been introduced. It is dispatched right before a Record is created and therefore allows to fully manipulate any property, even the ones already transformed by TYPO3.

The new event is stoppable (implementing \StoppableEventInterface ), which allows listeners to actually create a Record object completely on their own.

The new event features the following methods:

  • setRecord() - Manually adds a RecordInterface object (stops the event propagation)
  • hasProperty() - Whether a property exists
  • setProperty() - Add or overwrite a property
  • setProperties() - Set properties for the RecordInterface
  • unsetProperty() - Unset a single property
  • getProperty() - Get the value for a single property
  • getProperties() - Get all properties
  • getRawRecord() - Get the RawRecord object
  • getSystemProperties() - Get the calculated SystemProperties
  • getContext() - Get the current Context (used to fetch the raw database row)
  • isPropagationStopped() - Whether the event propagation is stopped

Example 

The event listener class, using the PHP attribute #[AsEventListener] for registration, creates a Coordinates object based on the field value of the coordinates field for the custom maps content type.

final class RecordCreationEventListener
{
    #[AsEventListener]
    public function __invoke(\TYPO3\CMS\Core\Domain\Event\RecordCreationEvent $event): void
    {
        $rawRecord = $event->getRawRecord();

        if ($rawRecord->getMainType() === 'tt_content' && $rawRecord->getRecordType() === 'maps' && $event->hasProperty('coordinates')) {
            $event->setProperty(
                'coordinates',
                new Coordinates($event->getProperty('coordinates'))
            );
        }
    }
}
Copied!

Impact 

Using the new PSR-14 RecordCreationEvent , extension authors are able to apply any field transformation to any property before a Record is created.

It is even possible to completely create a new RecordInterface object on their own.

Feature: #104868 - Add color scheme switching 

See forge#104868

Description 

Options have been added to switch between the available color schemes in TYPO3. A set of buttons for each available color scheme in the user dropdown at the top right and a setting in User Settings.

As the dark color scheme is currently regarded experimental until further notice, color scheme switching logic is currently hidden behind the UserTS setting setup.fields.colorScheme.disabled.

Impact 

It is now possible to switch to an automatic, light or dark color scheme for use in the backend.

Feature: #104878 - Introduce dashboard widget for pages with latest changes 

See forge#104878

Description 

To make it easier for TYPO3 users to view the latest changed pages in their TYPO3 system, TYPO3 now offers a dashboard widget that lists the latest changed pages.

Widget Options: - limit The limit of pages, displayed in the widget, default is 10 - historyLimit The maximum number of history records to check, default 1000

Impact 

TYPO3 users who have access to the Dashboard module and are granted access to the new widgets can now add and use this widget.

Feature: #104896 - Raise Fluid Standalone to 4.0 

See forge#104896

Description 

TYPO3 13 now uses Fluid 4 as the new base version. Old TYPO3 versions will keep using Fluid 2, which will still receive bugfixes if necessary. For detailed information about this release, please refer to the dedicated release notes on GitHub.

With the update to Fluid 4, tag-based ViewHelpers now have proper support for boolean attributes. Before this change, it was very cumbersome to generate these with Fluid, now it is implemented similar to popular JavaScript frameworks by using the newly introduced boolean literals:

<my:viewhelper async="{true}" />
Result: <tag async="async" />

<my:viewhelper async="{false}" />
Result: <tag />
Copied!

Of course, any variable containing a boolean can be supplied as well:

<my:viewhelper async="{isAsync}" />
Copied!

This can also be used in combination with variable casting:

<my:viewhelper async="{myString as boolean}" />
Copied!

For compatibility reasons empty strings still lead to the attribute being omitted from the tag.

Impact 

For existing installations, negative consequences of this update should be minimal as deprecated features will still work. Users are however advised to look into the already announced deprecations and to update their code accordingly. This update helps with this by now writing log messages to the deprecation log (if activated) if any deprecated feature is used in the TYPO3 instance.

Feature: #104904 - Ignore Fluid syntax error in <f:comment> 

See forge#104904

Description 

Fluid 4 brings a new template processor \TYPO3Fluid\Fluid\Core\Parser\TemplateProcessor\RemoveCommentsTemplateProcessor which removes Fluid comments created with the Debug ViewHelper <f:debug> from the template source string before the parsing process starts. It retains the original line breaks to ensure that error messages still refer to the correct line in the template.

By applying this template processor to all Fluid instances in the Core, it is now possible to use invalid Fluid code inside <f:comment> without triggering a Fluid error.

Impact 

This feature is helpful during template development because developers don't need to take care for commented-out code being valid Fluid code.

Feature: #104914 - Updated HTTP headers for frontend rendering and new TypoScript setting for proxies 

See forge#104914

Description 

In a typical frontend rendering scenario, TYPO3 sends HTTP response headers to deny caching to clients (= browsers) when e.g. a frontend user is logged in, a backend user is previewing a page, or a non-cacheable plugin is on a page.

When a frontend page is "client-cacheable", TYPO3 does not send any HTTP headers by default, but only when config.sendCacheHeaders = 1 is set via TypoScript.

In this case, TYPO3 sends the following HTTP Headers (example):

Expires: Thu, 26 Aug 2024 08:52:00 GMT
ETag: "d41d8cd98f00b204ecs00998ecf8427e"
Cache-Control: max-age=86400
Pragma: public
Copied!

However, in the past, this could lead to problems, because recurring website users might see outdated content for up to 24 hours (by default) or even longer, even if other website visitors already see new content, depending on various cache_timeout settings.

Thus, config.sendCacheHeaders = 1 should be used with extreme care.

However, this option was also used when a proxy / CDN / shared cache such as Varnish was put in between TYPO3 / the webserver and the client. The reverse proxy can then evaluate the HTTP Response Headers sent by TYPO3 frontend, put the TYPO3 response from the actual webserver into its "shared cache" and send a manipulated / adapted response to the client.

However, when working with proxies, it is much more helpful to take load off of TYPO3 / the webserver by keeping a cached version for a period of time and answering requests from the client, while still telling the client to not cache the response inside the browser cache.

This is now achieved with a new option config.sendCacheHeadersForSharedCaches = auto.

With this option enabled, TYPO3 now evaluates if the current TYPO3 frontend request is executed behind a Reverse Proxy, and if so, TYPO3 sends the following HTTP Response Headers at a cached response:

Expires: Thu, 26 Aug 2024 08:52:00 GMT ETag: "d41d8cd98f00b204ecs00998ecf8427e" Cache-Control: max-age=0, s-maxage=86400 Pragma: public

With config.sendCacheHeadersForSharedCaches = force the reverse proxy evaluation can be omitted, which can be used for local webserver internal caches.

"s-maxage" is a directive to tell shared caches - CDNs and reverse proxies - to keep a cached version of the HTTP response for a period of time (based on various cache settings) in their shared cache, while max-age=0 is evaluated at the client level. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control for more details and if your reverse proxy supports this directive.

The new option takes precedence over config.sendCacheHeaders = 1 if running behind a reverse proxy.

Impact 

By utilizing the new TypoScript setting, TYPO3 caches cacheable contents, and also instructs shared caches such as reverse proxies or CDNs to cache the HTTP Response, while always delivering fresh content to the client, if certain routines for cache invalidation are in place. The latter is typically handled by TYPO3 extensions which hook into the cache invalidation process of TYPO3 to also invalidate cache entries in the reverse proxies.

In addition, compared to previous TYPO3 versions, client-cacheable HTTP Responses now send "Cache-Control: private, no-store" if no option applies.

Feature: #104935 - Allow moving content elements via page tree 

See forge#104935

Description 

To make managing content across pages easier, a backend user may now drag content elements from the Web > Page module into a page in the page tree.

Once dropped, a modal window opens, allowing the backend user to select the position for placing the content element and to change the target page if needed.

Impact 

Content elements can be moved from the Web > Page module into the page tree to initiate the moving process.

Feature: #104973 - Activate the shipped LintYaml executable for TYPO3 

See forge#104973

Description 

The typo3 executable received a new command lint:yaml to ease and encourage linting of YAML files before deploying to production and therefore avoid failures.

Usage as follows:

# Validates a single file
bin/typo3 lint:yaml path/to/file.yaml

# Validates multiple files
bin/typo3 lint:yaml path/to/file1.yaml path/to/file2.yaml

# Validates all files in a directory (also in sub-directories)
bin/typo3 lint:yaml path/to/directory

# Validates all files in multiple directories (also in sub-directories)
bin/typo3 lint:yaml path/to/directory1 path/to/directory2

# Exclude one or more files from linting
bin/typo3 lint:yaml path/to/directory --exclude=path/to/directory/foo.yaml --exclude=path/to/directory/bar.yaml

# Show help
bin/typo3 lint:yaml --help
Copied!

The help argument will list possible usage elements.

Impact 

Integrate easy made linting of YAML files from Core, custom extensions or any other source into your quality assurance workflow in the known format of the typo3 executable.

Feature: #104990 - Automatic frontend cache tagging 

See forge#104990

Description 

When database records are used in the frontend, and the rendered result is put into caches like the page cache, the TYPO3 frontend now automatically tags cache entries with lists of used records.

When changing such records in the backend, affected cache entries are dropped, leading to automatic cache eviction.

This is a huge improvement to previous TYPO3 versions where tagging and cache eviction had to configured manually.

This feature - automatically tagging cache entries - is the final solution to consistent caches at any point in time. It is however a bit tricky to get right in a performant way: There are still details to rule out, and the core will continue to improve in this area. The basic implementation in TYPO3 v13 however already resolves many use cases. Core development now goes ahead to see how this features behaves in the wild.

This feature is encapsulated in the feature toggle frontend.cache.autoTagging: It is enabled by default with new instances based on TYPO3 v13, and needs to be manually enabled for instances being upgrades from previous versions.

Impact 

Instances configured with the feature toggle automatically tag caches. Affected cache entries will be removed when changing records.

Deprecation: #101559 - Extbase uses ext:core ViewInterface 

See forge#101559

Description 

The default view of ext:extbase now returns a view that implements \TYPO3\CMS\Core\View\ViewInterface and not only \TYPO3Fluid\Fluid\View\ViewInterface anymore. This allows implementing any view that implements ViewInterface , and frees the direct dependency to Fluid.

The default return object is an instance of \TYPO3\CMS\Fluid\View\FluidViewAdapter which implements all special methods tailored for Fluid. Extbase controllers should check for instance of this object before calling these methods, especially:

  • getRenderingContext()
  • setRenderingContext()
  • renderSection()
  • renderPartial()

Method calls not being part of ViewInterface or the above listed method names have been marked as deprecated and will be removed in TYPO3 v14.

Impact 

Extbase controllers that extend ActionController and call methods not part of ViewInterface , should test for $view instanceof FluidViewAdapter before calling getRenderingContext(), setRenderingContext(), php:renderSection() and renderPartial().

All other Fluid related methods called on $view have been marked as deprecated and will log a deprecation level error message.

Affected installations 

Instances with Extbase based extensions that call $view methods without testing for FluidViewAdapter .

Migration 

Methods on "old" Fluid instances were wrapper methods for RenderingContext . Controllers should call $view->getRenderingContext() to perform operations instead.

Deprecation: #102422 - TypoScriptFrontendController->addCacheTags() and ->getPageCacheTags() 

See forge#102422

Description 

The methods \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->addCacheTags() and \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->getPageCacheTags() have been marked as deprecated.

Impact 

Calling the methods \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->addCacheTags() and \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->getPageCacheTags() will trigger a PHP deprecation warning.

Affected installations 

TYPO3 installations calling \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->addCacheTags() or \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->getPageCacheTags().

Migration 

// Before
$GLOBALS['TSFE']->addCacheTags([
    'tx_myextension_mytable_123',
    'tx_myextension_mytable_456'
]);

// After
use TYPO3\CMS\Core\Cache\CacheTag;

$request->getAttribute('frontend.cache.collector')->addCacheTags(
    new CacheTag('tx_myextension_mytable_123', 3600),
    new CacheTag('tx_myextension_mytable_456', 3600)
);
Copied!
// Before
$GLOBALS['TSFE']->getPageCacheTags();

// After
$request->getAttribute('frontend.cache.collector')->getCacheTags();
Copied!

Deprecation: #102821 - ExtensionManagementUtility::addPItoST43() 

See forge#102821

Description 

The method \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPItoST43() has been marked as deprecated in TYPO3 v13 and will be removed with TYPO3 v14.

Impact 

Using the ExtensionManagementUtility::addPItoST43() will raise a deprecation level log entry and a fatal error in TYPO3 v14.

Affected installations 

Extensions using ExtensionManagementUtility::addPItoST43() are affected: Using ExtensionManagementUtility::addPItoST43() triggers a deprecation level log message. The extension scanner will find usages of ExtensionManagementUtility::addPItoST43() as strong match.

Migration 

// Before:
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPItoST43('my_extkey', '', '_pi1');
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTypoScript(
    'tx_myextkey',
    'setup',
    'plugin.tx_myextkey_pi1.userFunc = MY\MyExtkey\Plugins\Plugin->main'
);

// After:
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTypoScript(
    'my_extkey',
    'setup',
    'plugin.tx_myextkey_pi1 = USER_INT
     plugin.tx_myextkey_pi1.userFunc = MY\MyExtkey\Plugins\Plugin->main'
);
Copied!

Deprecation: #104223 - Fluid standalone methods 

See forge#104223

Description 

Some methods in Fluid standalone v2 have been marked as deprecated:

  • \TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper->registerUniversalTagAttributes()
  • \TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper->registerTagAttribute()

Impact 

Calling these methods is discouraged. They will log a deprecation level error when used with Fluid standalone v4.

Affected installations 

Instances with extensions calling above methods.

Migration 

registerUniversalTagAttributes() 

Within tag based ViewHelpers, calls to registerUniversalTagAttributes() should be removed. This method has been marked as @deprecated with Fluid standalone 2.12, will log a deprecation level error with Fluid standalone v4, and will be removed with v5.

When removing the call, attributes registered by the call are now available in $this->additionalArguments, and no longer in $this->arguments. This may need adaption within single ViewHelpers, if they handle such attributes on their own. For example, the common ViewHelper f:image was affected within the TYPO3 Core. The following attributes may need attention when removing registerUniversalTagAttributes(): class, dir, id, lang, style, title, accesskey, tabindex, onclick.

registerTagAttribute() 

Within tag based ViewHelpers, calls to registerTagAttribute() should be removed. This method has been marked as @deprecated with Fluid standalone 2.12, will log a deprecation level error with Fluid standalone v4, and will be removed with v5.

The call be often simply removed since arbitrary attributes not specifically registered are just added as-is by \TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper . This only needs attention if single view helpers deal with such attributes within the render() method: When removing the call, those arguments are no longer available in $this->arguments, but in $this->additionalArguments. Additional attention is needed with attributes registered with type boolean: Those usually have some handling within render(). To stay compatible, it can be helpful to not simply remove the registerTagAttribute() call, but to turn it into a call to registerArgument().

Deprecation: #104304 - BackendUtility::getTcaFieldConfiguration 

See forge#104304

Description 

The method \TYPO3\CMS\Backend\Utility\BackendUtility::getTcaFieldConfiguration was introduced back in 2010 to add a simple abstraction to access "TCA" definitions of a field.

However, apart from the set up that it is not part of a flexible API without knowing the context, it was used seldom in TYPO3 Core.

The method has now been deprecated, as one could and can easily write the same PHP code with $GLOBALS['TCA'] in mind already (which the TYPO3 Core already did in several other places).

Now that Schema API was introduced, the last parts have been migrated to use the new API.

Impact 

Calling the PHP method BackendUtility::getTcaFieldConfiguration will trigger a PHP deprecation warning.

Affected installations 

TYPO3 installations with custom extensions using this method.

Migration 

Either access $GLOBALS['TCA'] directly (in order to support TYPO3 v12 and TYPO3 v13), or migrate to the new Schema API:

public function __construct(
    private readonly TcaSchemaFactory $tcaSchemaFactory
) {}

private function getFieldConfiguration(string $table, string $fieldName): array
{
    return $this->tcaSchemaFactory
        ->get($table)
        ->getField($fieldName)
        ->getConfiguration();
}
Copied!

Deprecation: #104325 - DiffUtility->makeDiffDisplay() 

See forge#104325

Description 

Method \TYPO3\CMS\Core\Utility\DiffUtility->makeDiffDisplay() and class property DiffUtility->stripTags have been deprecated in favor of new method DiffUtility->diff(). The new method no longer applies strip_tags() to the input strings.

This change makes class DiffUtility stateless: Property $stripTags will vanish in v14.

Impact 

Using method DiffUtility->makeDiffDisplay() will trigger a deprecation level error message.

Affected installations 

Instances with extensions calling DiffUtility->makeDiffDisplay().

Migration 

If DiffUtility->stripTags is not explicitly set to false, a typical migration looks like this:

// before
$diffUtility->DiffUtility->makeDiffDisplay($from, $to);

// after
$diffUtility->DiffUtility->diff(strip_tags($from), stripTags($to));
Copied!

If DiffUtility->stripTags = false is set before calling DiffUtility->makeDiffDisplay(), method diff() can be called as before, and DiffUtility->stripTags = false can be removed.

Deprecation: #104463 - Fluid standalone overrideArgument 

See forge#104463

Description 

Fluid standalone method \TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper->overrideArgument() has been marked as deprecated.

Impact 

Using overrideArgument() in ViewHelpers logs a deprecation level error message in Fluid standalone v4, and will be removed with Fluid standalone v5. The method continues to work without deprecation level error message in Fluid standalone v2.

With Fluid standalone v2.14, registerArgument() no longer throws an exception if an argument is already registered. This allows to override existing arguments transparently without using overrideArgument().

Affected installations 

Instances with custom ViewHelpers using overrideArgument() are affected.

Migration 

Update typo3fluid/fluid to at least 2.14 and use registerArgument().

Deprecation: #104607 - BackendUserAuthentication:returnWebmounts() 

See forge#104607

Description 

Method \TYPO3\CMS\Core\Authentication\BackendUserAuthentication::returnWebmounts() has been marked as deprecated and will be removed with TYPO3 v14.

Method BackendUserAuthentication::getWebmounts() was introduced as substitution. It returns a unique list of integer uids instead of a list of strings, which is more type safe. Superfluous calls to array_unique() can be removed since the uniqueness is now guaranteed by BackendUserAuthentication::getWebmounts().

Impact 

Calling BackendUserAuthentication::returnWebmounts() will trigger a PHP deprecation warning.

Affected installations 

All installations using BackendUserAuthentication::returnWebmounts() are affected.

Migration 

Existing calls to BackendUserAuthentication::returnWebmounts() should be replaced by BackendUserAuthentication::getWebmounts().

If third party extensions convert the previous result array from an array of strings to an array of integers, this can be skipped. In addition superfluous calls to array_unique() can be removed since the uniqueness is now guaranteed by BackendUserAuthentication::getWebmounts().

Deprecation: #104662 - BackendUtility thumbCode 

See forge#104662

Description 

The method \TYPO3\CMS\Backend\Utility\BackendUtility::thumbCode() has been deprecated since the method is no longer used in TYPO3 anymore. Additionally, due to multiple changes to file processing over the years, e.g. introducing of FAL, the method's signature changed a couple of times leading to a couple of method arguments are being unused, which is quite a bad API.

Impact 

Calling the PHP method BackendUtility::thumbCode() will trigger a PHP deprecation warning.

Affected installations 

TYPO3 installations with custom extensions using this method. The extension scanner will report any usage as strong match.

Migration 

Remove any usage of this method. In case you currently rely on the functionality, you can copy it to your custom extension. However, you might want to consider refactoring the corresponding code places.

The method basically resolved given FileReference objects. In case a file could not be resolved, a special icon has been rendered. Otherwise, the cropping configuration has been applied and the file's process() has been called to get the thumbnail, which has been wrapped in corresponding thumbnail markup. This might has been extended to also open the information modal on click.

This means the relevant parts are:

// Get file references
$fileReferences = BackendUtility:resolveFileReferences($table, $field, $row);

// Check for existence of the file
$fileReference->getOriginalFile()->isMissing()

// Render special icon if missing
$iconFactory
    ->getIcon('mimetypes-other-other', IconSize::MEDIUM, 'overlay-missing')
    ->setTitle(static::getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.file_missing') . ' ' . $fileObject->getName())
    ->render()

// Process file with cropping configuration if not missing
$fileReference->getOriginalFile()->process(
    ProcessedFile::CONTEXT_IMAGEPREVIEW,// ProcessedFile::CONTEXT_IMAGECROPSCALEMASK if cropArea is defiend
    [
        'width' => '...',
        'height' => '...',
        'crop' // If cropArea is defined
    ]
)

// Use cropped file and create <img> tag
<img src="' . $fileReference->getOriginalFile()->process()->getPublicUrl() . '"/>

// Wrap the info popup via <a> around the thumbnail
<a href="#" data-dispatch-action="TYPO3.InfoWindow.showItem" data-dispatch-args-list="_FILE,' . (int)$fileReference->getOriginalFile()->getUid() . '">
Copied!

Example of the HTML markup for a thumbnail:

<div class="preview-thumbnails" style="--preview-thumbnails-size: 64px">
    <div class="preview-thumbnails-element">
        <div class="preview-thumbnails-element-image">
            <img src="' . $fileReference->getOriginalFile()->process()->getPublicUrl() . '" width="64px" height="64px" alt="' . $fileReference->getAlternative() ?: $fileReference->getName()  . '" loading="lazy"/>
        </div>
    </div>
</div>
Copied!

Deprecation: #104684 - Fluid RenderingContext->getRequest() 

See forge#104684

Description 

The following methods have been marked as deprecated in TYPO3 v13 and will be removed with TYPO3 v14:

  • \TYPO3\CMS\Fluid\Core\Rendering\RenderingContext->setRequest()
  • \TYPO3\CMS\Fluid\Core\Rendering\RenderingContext->getRequest()

Impact 

Calling above methods triggers a deprecation level log entry in TYPO3 v13 and will trigger a fatal PHP error with TYPO3 v14.

Affected installations 

RenderingContext->getRequest() is a relatively common call in custom view helpers. Instances with extensions that deliver custom view helpers may be affected. The extension scanner is not configured to find potential places since the method names are common and would lead to too many false positives.

Migration 

Class \TYPO3\CMS\Fluid\Core\Rendering\RenderingContext of the Core extension Fluid extends class \TYPO3Fluid\Fluid\Core\Rendering\RenderingContext of Fluid standalone and adds the methods setRequest() and getRequest(). These methods are however not part of \TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface .

Fluid standalone will not add these methods, since the view of this library should stay free from direct PSR-7 \ServerRequestInterface dependencies. Having those methods in ext:fluid RenderingContext however collides with \RenderingContextInterface , which is type hinted in Fluid view helper method signatures.

Fluid standalone instead added three methods to handle arbitrary additional data in \RenderingContextInterface : setAttribute(), hasAttribute() and getAttribute(). Those should be used instead.

A typical usage in a view helper before:

/** @var \TYPO3Fluid\Fluid\Core\Rendering\RenderingContext $renderingContext */
$renderingContext = $this->renderingContext;
$request = $renderingContext->getRequest();
Copied!

After:

// use Psr\Http\Message\ServerRequestInterface

$request = null;
if ($renderingContext->hasAttribute(ServerRequestInterface::class)) {
    $request = $renderingContext->getAttribute(ServerRequestInterface::class);
}
Copied!

To stay compatible to previous TYPO3 versions while avoiding deprecation notices, the following code can be used:

// use Psr\Http\Message\ServerRequestInterface

if (
    method_exists($renderingContext, 'getAttribute') &&
    method_exists($renderingContext, 'hasAttribute') &&
    $renderingContext->hasAttribute(ServerRequestInterface::class)
) {
    $request = $renderingContext->getAttribute(ServerRequestInterface::class);
} else {
    $request = $renderingContext->getRequest();
}
Copied!

Deprecation: #104764 - Fluid TemplatePaths->fillDefaultsByPackageName 

See forge#104764

Description 

Method \TYPO3\CMS\Fluid\View\TemplatePaths->fillDefaultsByPackageName() has been marked as deprecated in TYPO3 v13 and will be removed in TYPO3 v14.

Fluid template paths should be set directly using the methods setTemplateRootPaths(), setLayoutRootPaths() and setPartialRootPaths(), or - even better - be handled by ViewFactoryInterface, which comes as new feature in TYPO3 v13.

See Feature: #104773 - Generic view factory for more details of the generic view interface.

Impact 

Calling fillDefaultsByPackageName() triggers a deprecation level log level entry in TYPO3 v13 and will be removed in TYPO3 v14.

Affected installations 

The method is relatively rarely used by extensions directly, a usage in Extbase ActionController has been refactored away. The extension scanner will find candidates.

Note class TemplatePaths is marked @internal and should not be used by extensions at all.

Migration 

Use \TYPO3\CMS\Core\View\ViewFactoryInterface to create views with proper template paths instead. The TYPO3 system extensions come with plenty of examples on how to do this.

Deprecation: #104773 - Custom Fluid views and Extbase 

See forge#104773

Description 

These classes have been marked as deprecated in TYPO3 v13 and will be removed in v14:

  • \TYPO3\CMS\Fluid\View\StandaloneView
  • \TYPO3\CMS\Fluid\View\TemplateView
  • \TYPO3\CMS\Fluid\View\AbstractTemplateView
  • \TYPO3\CMS\Extbase\Mvc\View\ViewResolverInterface
  • \TYPO3\CMS\Extbase\Mvc\View\GenericViewResolver

This change is related to the general View refactoring.

Impact 

Using one of the above classes triggers a deprecation level log entry.

Affected installations 

Instances with extensions that create view instances of \StandaloneView or \TemplateView are affected. The extension scanner will find possible candidates.

Migration 

Extensions should no longer directly instantiate own views, but should get \TYPO3\CMS\Core\View\ViewFactoryInterface injected and use create() to retrieve a view.

Within Extbase, ActionController->defaultViewObjectName should only be set to Extbase JsonView if needed, or not set at all. Custom view implementations should implement an own ViewFactoryInterface and configure controllers to inject an instance, or can set $this->defaultViewObjectName = JsonView::class in a custom __construct().

Deprecation: #104773 - ext:backend LoginProviderInterface changes 

See forge#104773

Description 

Method \TYPO3\CMS\Backend\LoginProvider\LoginProviderInterface->render() has been marked as deprecated and is substituted by LoginProviderInterface->modifyView() that will be added to the interface in TYPO3 v14, removing render() from the interface in v14.

Related to this, event \TYPO3\CMS\Backend\LoginProvider\Event\ModifyPageLayoutOnLoginProviderSelectionEvent has been changed to deprecate getController() and getPageRenderer(), while getRequest() has been added. getView() now typically returns an instance of ViewInterface.

This change is related to the general View refactoring.

Impact 

The default LoginProviderInterface implementation is UsernamePasswordLoginProvider provided by ext:core. This consumer has been adapted.

Using LoginProviderInterface->render() in TYPO3 v13 will trigger a deprecation level log entry and will fail in v14.

Affected installations 

Instances with custom login providers that change the TYPO3 backend login field rendering may be affected. The extension scanner is not configured to find usages, since method name render() is too common. A deprecation level log message is triggered upon use of the old method.

Migration 

Consumers of LoginProviderInterface should implement modifyView() instead, the transition should be smooth. Consumers that need the PageRenderer for JavaScript magic, should use dependency injection to receive an instance.

The default implementation in UsernamePasswordLoginProvider is a good example. Extensions that need to configure additional template, layout or partial lookup paths can extend them:

if ($view instanceof FluidViewAdapter) {
    $templatePaths = $view->getRenderingContext()->getTemplatePaths();
    $templateRootPaths = $templatePaths->getTemplateRootPaths();
    $templateRootPaths[] = 'EXT:my_extension/Resources/Private/Templates';
    $templatePaths->setTemplateRootPaths($templateRootPaths);
}
Copied!

Consumers of ModifyPageLayoutOnLoginProviderSelectionEvent should use the request instead, and/or should get an instance of PageRenderer injected as well.

Deprecation: #104778 - Instantiation of IconRegistry in ext_localconf.php 

See forge#104778

Description 

Since TYPO3 v11 it is possible to automatically register own icons via Configuration/Icons.php. Prior to this, extension authors used to register icons manually via instantiating the php:\TYPO3\CMS\Core\Imaging\IconRegistry in their ext_localconf.php files. This method has now been deprecated. It is recommended to switch to the newer method introduced with forge#94692.

Impact 

Instantiating IconRegistry inside ext_localconf.php files will trigger a deprecation-level log entry.

Affected installations 

All installations, which instantiate IconRegistry before the \TYPO3\CMS\Core\Core\Event\BootCompletedEvent . This includes ext_localconf.php files as well as TCA/Overrides.

Migration 

The most common use-cases can be accomplished via the Configuration/Icons.php file.

Before:

EXT:example/ext_localconf.php
<?php

$iconRegistry = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(
  \TYPO3\CMS\Core\Imaging\IconRegistry::class,
);
$iconRegistry->registerIcon(
    'example',
    \TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class,
    [
        'source' => 'EXT:example/Resources/Public/Icons/example.svg'
    ],
);
Copied!

After:

EXT:example/Configuration/Icons.php
<?php

return [
    'example' => [
        'provider' => \TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class,
        'source' => 'EXT:example/Resources/Public/Icons/example.svg',
    ],
];
Copied!

For more complex tasks, it is recommended to register an event listener for the BootCompletedEvent . At this stage the system is fully booted and you have a completely configured IconRegistry at hand.

In case the registry was used in TCA/Overrides files to retrieve icon identifiers, then this should be replaced completely with static identifiers. The reason behind this is, that the registry isn't even fully usable at this stage. TCA isn't fully built yet and icons can still be registered at a later point.

Deprecation: #104789 - Fluid variables true, false, null 

See forge#104789

Description 

Fluid standalone will add proper language syntax for booleans and null with Fluid v4, which will be used in TYPO3 v13. Thus, user-defined variables named true, false and null are no longer allowed.

Impact 

Assigning variables with name true, false or null will throw an exception in Fluid v4. In preparation of this change, Fluid v2.15 logs a deprecation level error message if any of these variable names are used.

Affected installations 

Instances with Fluid templates using true, false or null as user-defined variable names. This should rarely happen, as it would involve using $view->assign('true', $someVar).

Migration 

Template code using these variables should be adjusted to use different variable names. In Fluid v4, the variables will contain their matching PHP counterparts.

Deprecation: #104789 - renderStatic() for Fluid ViewHelpers 

See forge#104789

Description 

The usage of renderStatic() for Fluid ViewHelpers has been deprecated. Also, Fluid standalone traits \TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithContentArgumentAndRenderStatic and \TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithRenderStatic have been marked as deprecated.

Impact 

Using one of the mentioned traits or renderStatic() in ViewHelpers logs a deprecation level error message in Fluid standalone v4. renderStatic() will no longer be called in Fluid standalone v5. renderStatic() and both traits continue to work without deprecation level error message in Fluid standalone v2.

Affected installations 

Instances with custom ViewHelpers using any variant of renderStatic() are affected.

Migration 

ViewHelpers should always use render() as their primary rendering method.

ViewHelpers using just renderStatic() without any trait or with the trait \CompileWithRenderStatic can be migrated by converting the static rendering method to a non-static method:

Before:

class MyViewHelper extends AbstractViewHelper
{
    use CompileWithRenderStatic;

    public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext): string
    {
        return $renderChildrenClosure();
    }
}
Copied!

After:

class MyViewHelper extends AbstractViewHelper
{
    public function render(): string
    {
        return $this->renderChildren();
    }
}
Copied!

ViewHelpers using \TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithContentArgumentAndRenderStatic can use the new contentArgumentName feature added with Fluid v2.15:

Before:

class MyViewHelper extends AbstractViewHelper
{
    use CompileWithContentArgumentAndRenderStatic;

    public function initializeArguments(): void
    {
        $this->registerArgument('value', 'string', 'a value');
    }

    public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext): string
    {
        return $renderChildrenClosure();
    }

    public function resolveContentArgumentName(): string
    {
        return 'value';
    }
}
Copied!

After:

class MyViewHelper extends AbstractViewHelper
{
    public function initializeArguments(): void
    {
        $this->registerArgument('value', 'string', 'a value');
    }

    public function render(): string
    {
        return $this->renderChildren();
    }

    public function getContentArgumentName(): string
    {
        return 'value';
    }
}
Copied!

Here is a basic recipe to perform this migration, preferably utilizing statical code analysis/replacement tools on your *ViewHelper.php files:

  • Find definitions of renderStatic
  • Rename method to render(), remove the arguments, remove static declaration
  • Within that method:

    • Replace $arguments with $this->arguments
    • Replace $renderingContext with $this->renderingContext
    • Replace $renderChildrenClosure() with $this->renderChildren()
    • Replace remaining $renderChildrenClosure usages with proper closure handling, like $this->renderChildren(...).
  • Replace resolveContentArgumentName( with getContentArgumentName(
  • Remove the mentioned definitions:

    • use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
    • use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithRenderStatic;
    • use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithContentArgumentAndRenderStatic;
    • use CompileWithRenderStatic; (class trait)
    • use CompileWithContentArgumentAndRenderStatic; (class trait)
  • (Optionally remove custom phpdoc annotations to the renderStatic parameters)
  • If you previously called ViewHelper's renderStatic methods in other places, you may utilize something like:

    $this->renderingContext->getViewHelperInvoker()->invoke(
        MyViewHelper::class,
        $arguments,
        $this->renderingContext,
        $this->renderChildren(...),
    );
    Copied!

Important: #101535 - Unused DB fields from tt_content removed 

See forge#101535

Description 

The database table tt_content contains all necessary fields for rendering content elements.

Back with TYPO3 v4.7, a major feature to render certain Content Types in a more accessible way, funded by the German Government (BLE_) with the "Konjunkturpaket II" was merged into CSS Styled Content.

In this procedure, certain Content Types received new fields and rendering definitions, which were stored in the database fields accessibility_title, accessibility_bypass and accessibility_bypass_text.

When CSS Styled Content was removed in favor of Fluid Styled Content in TYPO3 v8, the DB fields continued to exist in TYPO3 Core, so a migration from CSS Styled Content was possible.

However, the DB fields are not evaluated anymore since then, and are removed, along with their TCA definition in tt_content.

If these fields are still relevant for a custom legacy installation, these DB fields need to be re-created via TCA for further use in a third-party extension.

Important: #104126 - Drop "typo3conf" directory from system status check and backend locking 

See forge#104126

Description 

The directory typo3conf is no longer needed in Composer Mode. Checking for the existence of this directory is no longer performed in the Environment and Install Tool.

Previously it contained:

  • extensions (which are now Composer packages stored in vendor/),
  • the configuration files (which are now part of the config/ tree)
  • language labels and some artifact states (now part of var/)
  • a "backend lock" file (LOCK_BACKEND)

The location to this file can be adjusted via the new configuration setting $GLOBALS['TYPO3_CONF_VARS']['BE']['lockBackendFile'] . See Feature: #104126 - Add configuration setting to define backend-locking file for details on this setting and location.

By default, LOCK_BACKEND is now located here:

  • var/lock/ for Composer Mode
  • config/ for Legacy Mode

13.2 Changes 

Table of contents

Breaking Changes 

None since TYPO3 v13.0 release.

Features 

Deprecation 

Important 

Feature: #91783 - Allow system maintainer to mute disable_functions error 

See forge#91783

Description 

Adds a configuration option to adapt the environment check in the Install Tool for a list of sanctioned disable_functions.

With the new configuration option $GLOBALS['TYPO3_CONF_VARS']['SYS']['allowedPhpDisableFunctions'] , a system maintainer can add native PHP function names to this list, which are then reported as environment warnings instead of errors.

Configuration example in additional.php:

$GLOBALS['TYPO3_CONF_VARS']['SYS']['allowedPhpDisableFunctions']
  = ['set_time_limit', 'set_file_buffer'];
Copied!

You can also define this in your settings.php file manually or via Admin Tools > Settings > Configure options.

Impact 

Native php function names can be added as an array of function names, which will not trigger an error but only a warning, if they can also be found in the php.ini setting disable_functions.

Feature: #92009 - Provide backend modules in LiveSearch 

See forge#92009

Description 

The backend LiveSearch is now capable of listing backend modules, a user has access to, offering the possibility of alternative navigation to different parts of the backend.

Impact 

Backend users now have another possibility to quickly access a backend module.

Feature: #99203 - Streamline FE/versionNumberInFilename to 'EXT:' resources 

See forge#99203

Description 

Local resources are currently not "cache-busted", for example, have no version in URL. TypoScript has no possibility to add the cache buster. When replacing them a new filename must be used (which feels little hacky).

getText "asset" to cache-bust assets in TypoScript 

EXT:my_extension/Configuration/TypoScript/setup.typoscript
page.20 = TEXT
page.20 {
    value = { asset : EXT:core/Resources/Public/Icons/Extension.svg }
    insertData = 1
}
Copied!
Result
typo3/sysext/core/Resources/Public/Icons/Extension.svg?1709051481
Copied!

Cache-busted assets with the <f:uri.resource> ViewHelper 

EXT:my_extension/Resources/Private/Template/MyTemplate.html
<f:uri.resource
    path="EXT:core/Resources/Public/Icons/Extension.svg"
    useCacheBusting="true"
/>
Copied!
Comparison
Before: typo3/sysext/core/Resources/Public/Icons/Extension.svg
Now: typo3/sysext/core/Resources/Public/Icons/Extension.svg?1709051481
Copied!

The ViewHelper argument useCacheBusting is enabled by default.

Depending on $GLOBALS['TYPO3_CONF_VARS']['FE']['versionNumberInFilename'] the cache buster is applied as query string or embedded in the filename.

Impact 

Local resources now can have a cache buster to easily replace them without changing the filename.

Feature: #102155 - User TSconfig option for default resources ViewMode 

See forge#102155

Description 

The listing of resources in the TYPO3 backend, e.g. in the File > Filelist module or the FileBrowser can be switched between list and tiles. TYPO3 serves tiles by default.

A new User TSconfig option options.defaultResourcesViewMode has been introduced, which allows the initial display mode to be defined. Valid values are therefore list and tiles, e.g.:

options.defaultResourcesViewMode = list
Copied!

Impact 

Integrators can now define the default display mode for resources via User TSconfig.

Feature: #102326 - Allow custom translations for Extbase validators 

See forge#102326

Description 

All validation messages from Extbase validators can now be overwritten using validator options. It is possible to provide either a translation key or a custom message as string.

Extbase validators providing only one validation message can be overwritten by a translation key or message using the validator option message. Validators providing multiple validation messages (e.g. Boolean, NotEmpty or NumberRange) use different validator options keys. In general, translation keys or messages for validators are registered in the validator property translationOptions.

Example with translations 

use TYPO3\CMS\Extbase\Annotation as Extbase;

#[Extbase\Validate([
    'validator' => 'NotEmpty',
    'options' => [
        'nullMessage' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:validation.myProperty.notNull',
        'emptyMessage' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:validation.myProperty.notEmpty',
    ],
])]
protected string $myProperty = '';
Copied!

In this example, translation option keys for the NotEmptyValidator are overwritten for the property $myProperty. The locallang.xlf translation file from the extension my_extension will be used to lookup translations for the newly provided translation key options.

Example with a custom string 

use TYPO3\CMS\Extbase\Annotation as Extbase;

#[Extbase\Validate([
    'validator' => 'Float',
    'options' => [
        'message' => 'A custom, non translatable message',
    ],
])]
protected float $myProperty = 0.0;
Copied!

In this example, translation option keys for the FloatValidator are overwritten for the property $myProperty. The message string is shown if validation fails.

Impact 

The new validator translation option keys allow developers to define unique validation messages for TYPO3 Extbase validators on validator usage basis. This may result in a better user experience, since validation messages now can refer to the current usage scope (e.g. "The field 'Title' is required" instead of "The given subject was empty.").

Feature: #102337 - PSR-14 event for modifying record list export data 

See forge#102337

Description 

A new PSR-14 event \TYPO3\CMS\Backend\RecordList\Event\BeforeRecordDownloadIsExecutedEvent has been introduced to modify the result of a download / export initiated in the Web > List module.

This replaces the $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList']['customizeCsvHeader'] and $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList']['customizeCsvRow'] , hooks, which have been deprecated.

The event allows body and header sections of the data dump to be modified, so that they can e.g. be used to redact specific data for GDPR compliance, transform / translate specific data, trigger creation of archives or web hooks, log export access and more.

The event offers the following methods:

  • getHeaderRow(): Return the current header row of the dataset.
  • setHeaderRow(): Sets the modified header row of the dataset.
  • getRecords(): Returns the current body rows of the dataset.
  • setRecords(): Sets the modified body rows of the dataset.
  • getRequest(): Returns the PSR request context.
  • getTable(): Returns the name of the database table of the dataset.
  • getFormat(): Returns the format of the download action (CSV/JSON).
  • getFilename(): Returns the name of the download filename (for browser output).
  • getId(): Returns the page UID of the download origin.
  • getModTSconfig(): Returns the active module TSconfig of the download origin.
  • getColumnsToRender(): Returns the list of header columns for the triggered download.
  • isHideTranslations(): Returns whether translations are hidden or not.

Example 

The corresponding event listener class:

<?php

declare(strict_types=1);

namespace Vendor\MyPackage\Core\EventListener;

use TYPO3\CMS\Backend\RecordList\Event\BeforeRecordDownloadIsExecutedEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(identifier: 'my-package/record-list-download-data')]
final readonly class DataListener
{
    public function __invoke(BeforeRecordDownloadIsExecutedEvent $event): void
    {
        // List of redactable fields.
        $gdprFields = ['title', 'author'];

        $headerRow = $event->getHeaderRow();
        $records = $event->getRecords();

        // Iterate header to mark redacted fields...
        foreach ($headerRow as $headerRowKey => $headerRowValue) {
            if (in_array($headerRowKey, $gdprFields, true)) {
                $headerRow[$headerRowKey] .= ' (REDACTED)';
            }
        }

        // Redact actual content...
        foreach ($records as $uid => $record) {
            foreach ($gdprFields as $gdprField) {
                if (isset($record[$gdprField])) {
                    $records[$uid][$gdprField] = '(REDACTED)';
                }
            }
        }

        $event->setHeaderRow($headerRow);
        $event->setRecords($records);
    }
}
Copied!

Migration 

The functionality of both hooks customizeCsvHeader and customizeCsvRow are now handled by the new PSR-14 event.

Migrating customizeCsvHeader 

The prior hook parameter/variable fields is now available via $event->getColumnsToRender(). The actual record data (previously $this->recordList, submitted to the hook as its object reference) is accessible via $event->getHeaderRow().

Migrating customizeCsvRow 

The prior hook parameters/variables have the following substitutes:

  • databaseRow is now available via $event->getRecords() (see note below).
  • tableName is now available via $event->getTable().
  • pageId is now available via $event->getId().

The actual record data (previously $this->recordList, submitted to the hook as its object reference) is accessible via $event->getRecords().

Please note that the hook was previously executed once per row retrieved from the database. The PSR-14 event however - due to performance reasons - is only executed for the full record list after database retrieval, thus allowing post-processing on the whole dataset.

Impact 

Using the PSR-14 event BeforeRecordDownloadIsExecutedEvent it is now possible to modify all of the data available when downloading / exporting a list of records via the Web > List module.

Feature: #102337 - PSR-14 event for modifying record list download presets 

See forge#102337

Description 

A new PSR-14 event \TYPO3\CMS\Backend\RecordList\Event\BeforeRecordDownloadPresetsAreDisplayedEvent has been introduced to manipulate the list of available download presets in the Web > List module.

See Feature: #102337 - Presets for download of record lists for a detailed description of how to utilize presets when downloading a set of records from the backend in CSV or JSON format.

The event class offers the following methods:

  • getPresets(): Returns a list of presets set via TSconfig
  • setPresets(): Sets a modified list of presets.
  • getDatabaseTable(): Returns the database table name that a preset applies to.
  • getRequest(): Returns the PSR Request object for the context of the request.
  • getId(): Returns the page ID of the originating page.

Note that the event is dispatched for one specific database table. If an event listener is created to attach presets to different tables, the listener method must check for the table name, as shown in the example below.

If no download presets exist for a given table, the PSR-14 event can still be used to modify and add presets to it via the setPresets() method.

The array passed from getPresets() to setPresets() can contain an array collection of \TYPO3\CMS\Backend\RecordList\DownloadPreset objects with the array key using the preset label. The existing presets can be retrieved with these getters:

  • $preset->getLabel(): Name of the preset (can utilize LLL translations), optional.
  • $preset->getColumns(): Array of database table column names.
  • $preset->getIdentifier(): Identifier of the preset (manually set or calculated based on label and columns)

The event listener can also remove array indexes or columns of existing array entries by passing a newly constructed DownloadPreset object with the changed label and columns constructor properties.

Example 

The corresponding event listener class:

<?php

declare(strict_types=1);

namespace Vendor\MyPackage\RecordList\EventListener;

use TYPO3\CMS\Backend\RecordList\Event\BeforeRecordDownloadPresetsAreDisplayedEvent;
use TYPO3\CMS\Backend\RecordList\DownloadPreset;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(identifier: 'my-package/modify-record-list-preset')]
final readonly class PresetListener
{
    public function __invoke(BeforeRecordDownloadPresetsAreDisplayedEvent $event): void
    {
        $presets = $event->getPresets();

        switch ($event->getDatabaseTable()) {
            case 'be_users':
                $presets[] = new DownloadPreset('PSR-14 preset', ['uid','email']);
                break;

            case 'pages':
                $presets[] = new DownloadPreset('PSR-14 preset', ['title']);
                $presets[] = new DownloadPreset('Another PSR-14 preset', ['title', 'doktype']);
                break;

            case 'tx_myvendor_myextension':
                $presets[] = new DownloadPreset('PSR-14 preset', ['uid', 'something']);
                break;
        }

        $presets[] = new DownloadPreset('Available everywhere, simple UID list', ['uid']);

        $presets['some-identifier'] = new DownloadPreset('Overwrite preset', ['uid, pid'], 'some-identifier');

        $event->setPresets($presets);
    }
}
Copied!

Impact 

Using the PSR-14 event BeforeRecordDownloadPresetsAreDisplayedEvent it is now possible to modify the presets of each table for downloading / exporting a list of such records via the Web > List module.

Feature: #102337 - Presets for download of record lists 

See forge#102337

Description 

In the Web > List backend module, the data of records for each database table (including pages and content records) can be downloaded.

This export takes the currently selected list of columns into consideration and alternatively allows all columns to be selected.

A new feature has been introduced adding the ability to pick the exported data columns from a list of configurable presets.

Those presets can be configured via page TSconfig, and can also be overridden via user TSconfig (for example, to make certain presets only available to specific users).

EXT:my_extension/Configuration/page.tsconfig
mod.web_list.downloadPresets {
    pages {
        10 {
            label = Quick overview
            columns = uid, title, crdate, slug
        }

        20 {
            identifier = LLL:EXT:myext/Resources/Private/Language/locallang.xlf:preset2.label
            label = UID and titles only
            columns = uid, title
        }
    }
}
Copied!

Each entry of mod.web_list.downloadPresets defines the table name on the first level (in this case pages), followed by any number of presets.

Each preset contains a label (the displayed name of the preset, which can be a locallang key), a comma-separated list of each column that should be included in the export as columns and optionally an identifier. If identifier is not provided, the identifier is generated as a hash of the label and columns.

This can be manipulated with user TSConfig by adding the page. prefix. User TSConfig is loaded after page TSConfig, so you can overwrite existing keys or replace the whole list of keys:

EXT:my_extension/Configuration/user.tsconfig
page.mod.web_list.downloadPresets {
    pages {
        10 {
            label = Quick overview (customized)
            columns = uid, title, crdate, slug
        }

        30 {
            label = Short with URL
            columns = uid, title, slug
        }
    }
}
Copied!

Since any table can be configured for a preset, any extension can deliver a defined set of presets through the EXT:my_extension/Configuration/page.tsconfig file and their table name(s).

Additionally, the list of presets can be manipulated via the new BeforeRecordDownloadPresetsAreDisplayedEvent.

Impact 

Editors can now export data with specific presets as required as identified by the website maintainer or extension developer(s).

It is no longer required to pick specific columns to export over and over again, and the list of presets is controlled by the website maintainer.

Two new PSR-14 Events have been added to allow further manipulation:

Feature: #102869 - List workspaces in LiveSearch 

See forge#102869

Description 

The backend LiveSearch is now able to list workspaces a backend user has access to, offering the possibility to switch to a workspace quickly outside the Workspaces module.

Impact 

Backend users now have another, quickly accessible way to access a workspace. With proper permissions, a backend user may also switch to the edit interface of a workspace to configure its settings.

Feature: #102951 - Provide PSR-7 request in Extbase validators 

See forge#102951

Description 

Extbase abstractValidator now provides a getter and a setter for the PSR-7 Request object. Validators extending AbstractValidator will include the PSR-7 request object, if the validator has been instantiated by Extbase ValidationResolver.

Impact 

Extension developers can now create custom validators which consume data from the PSR-7 request object (e.g. request attribute frontend.user).

Feature: #103019 - ModifyRedirectUrlValidationResultEvent PSR-14 event 

See forge#103019

Description 

This feature introduces the new PSR-14 event ModifyRedirectUrlValidationResultEvent in the felogin extension to provide developers the possibility and flexibility to implement custom validation for the redirect URL. This may be useful if TYPO3 frontend login acts as an SSO system or if users should be redirected to an external URL after login.

Example 

<?php

namespace Vendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\FrontendLogin\Event\ModifyRedirectUrlValidationResultEvent;

class ValidateRedirectUrl
{
    #[AsEventListener('validate-custom-redirect-url')]
    public function __invoke(ModifyRedirectUrlValidationResultEvent $event): void
    {
        $parsedUrl = parse_url($event->getRedirectUrl());
        if ($parsedUrl['host'] === 'trusted-host-for-redirect.tld') {
            $event->setValidationResult(true);
        }
    }
}
Copied!

Impact 

Developers now have the possibility to modify the validation results for the redirect URL, allowing redirects to URLs not matching existing validation constraints.

Feature: #103783 - RecordTransformation Data Processor 

See forge#103783

Description 

A new TypoScript data processor for FLUIDTEMPLATE and PAGEVIEW has been added.

The record-transformation Data Processor can typically be used in conjunction with the DatabaseQuery Data Processor. The DatabaseQuery Data Processor typically fetches records from the database, and the record-transformation will take the result and transform the objects into Record objects, which contain relevant data from the TCA table, which has been configured in the TCA columns fields for this record.

This is especially useful for TCA tables, which contain "types" (such as pages or tt_content database tables), where only relevant fields are added to the record object. In addition, special fields from "enableColumns" or deleted fields, as well as language and version information are extracted so they can be dealt with in a unified way.

The "type" property contains the database table name and the actual type based on the record, such as "tt_content.textmedia" for Content Elements.

Impact 

Example of the data processor being used in conjunction with DatabaseQuery processor.

page = PAGE
page {
  10 = PAGEVIEW
  10 {
    paths.10 = EXT:my_extension/Resources/Private/Templates/
    dataProcessing {
      10 = database-query
      10 {
        as = mainContent
        table = tt_content
        select.where = colPos=0
        dataProcessing.10 = record-transformation
      }
    }
  }
}
Copied!

Transform the current data array of FLUIDTEMPLATE to a Record object. This can be used for Content Elements of Fluid Styled Content or custom ones. In this example the FSC element "Text" has its data transformed for easier and enhanced usage.

tt_content.text {
  templateName = Text
  dataProcessing {
    10 = record-transformation
    10 {
      as = data
    }
  }
}
Copied!

Usage in Fluid templates 

The f:debug output of the Record object is misleading for integrators, as most properties are accessed differently than would be expected. The debug view is a better organized overview of all the available information. E.g. the property properties lists all relevant fields for the current Content Type.

We are dealing with an object here. You can, however, access your record properties as you are used to with {record.title} or {record.uid}. In addition, you gain special, context-aware properties like the language {record.languageId} or workspace {data.versionInfo.workspaceId}.

Overview of all possibilities:

<!-- Any property, which is available in the Record (like normal) -->
{record.title}
{record.uid}
{record.pid}

<!-- Language related properties -->
{record.languageId}
{record.languageInfo.translationParent}
{record.languageInfo.translationSource}

<!-- The overlaid uid -->
{record.overlaidUid}

<!-- Types are a combination of the table name and the Content Type name. -->
<!-- Example for table "tt_content" and CType "textpic": -->

<!-- "tt_content" (this is basically the table name) -->
{record.mainType}

<!-- "textpic" (this is the CType) -->
{record.recordType}

<!-- "tt_content.textpic" (Combination of mainType and record type, separated by a dot) -->
{record.fullType}

<!-- System related properties -->
{data.systemProperties.isDeleted}
{data.systemProperties.isDisabled}
{data.systemProperties.isLockedForEditing}
{data.systemProperties.createdAt}
{data.systemProperties.lastUpdatedAt}
{data.systemProperties.publishAt}
{data.systemProperties.publishUntil}
{data.systemProperties.userGroupRestriction}
{data.systemProperties.sorting}
{data.systemProperties.description}

<!-- Computed properties depending on the request context -->
{data.computedProperties.versionedUid}
{data.computedProperties.localizedUid}
{data.computedProperties.requestedOverlayLanguageId}
{data.computedProperties.translationSource} <!-- Only for pages, contains the Page model -->

<!-- Workspace related properties -->
{data.versionInfo.workspaceId}
{data.versionInfo.liveId}
{data.versionInfo.state.name}
{data.versionInfo.state.value}
{data.versionInfo.stageId}
Copied!

Available options 

The variable that contains the record(s) from a previous data processor,
or from a FLUIDTEMPLATE view. Default is :typoscript:`data`.
variableName = items

# the name of the database table of the records. Leave empty to auto-resolve
# the table from context.
table = tt_content

# the target variable where the resolved record objects are contained
# if empty, "record" or "records" (if multiple records are given) is used.
as = myRecords
Copied!

Feature: #103894 - Additional properties for columns in Page Layouts 

See forge#103894

Description 

Backend Layouts were introduced in TYPO3 v6 in order to customize the view of the Page module in TYPO3 backend for pages, but has since grown, also in frontend rendering, to select e.g. Fluid template files via TypoScript for a page, commonly used via data:pagelayout.

In order to use a single source for backend and frontend representation, the definition of a "Backend Layout" or "Page Layout" is expanded to also include more information for a specific content area. The Content Area was previously defined via "name" (for the label in the Page module) and "colPos", the numeric database field in which content is grouped in.

A definition can now optionally also contain a "slideMode" property and an "identifier" property next to each colPos, in order to simplify frontend rendering.

Whereas "identifier" is a speaking representation for the colPos, such as "main", "sidebar" or "footerArea", the "slideMode" can be set to one of the three options:

  • slideMode = slide - if no content is found, check the parent pages for more content
  • slideMode = collect - use all content from this page, and the parent pages as one collection
  • slideMode = collectReverse- same as "collect" but in the opposite order

With this information added, a new DataProcessor page-content ( \TYPO3\CMS\Frontend\DataProcessing\PageContentFetchingProcessor ) is introduced for the frontend rendering, which fetches all content for a page and respecting the settings from the page layout.

The new data processor allows to manipulate the fetched page content via the PSR-14 AfterContentHasBeenFetchedEvent.

Impact 

Enriching the backend layout information for each colPos enables a TYPO3 integrator to write less TypoScript in order to render content on a page.

The DataProcessor fetches all content elements from all defined columns with an included "identifier" in the selected backend layout and makes the resolved record objects available in the Fluid template via {content."myIdentifier".records}.

Example of an enriched backend layout definition:

mod.web_layout.BackendLayouts {
  default {
    title = Default
    config {
      backend_layout {
        colCount = 1
        rowCount = 1
        rows {
          1 {
            columns {
              1 {
                name = Main Content Area
                colPos = 0
                identifier = main
                slideMode = slide
              }
            }
          }
        }
      }
    }
  }
}
Copied!

Example of the frontend output:

page = PAGE
page.10 = PAGEVIEW
page.10.paths.10 = EXT:my_site_package/Tests/Resources/Private/Templates/
page.10.dataProcessing.10 = page-content
page.10.dataProcessing.10.as = myContent
Copied!
<main>
    <f:for each="{myContent.main.records}" as="record">
        <f:cObject typoscriptObjectPath="{record.mainType}" table="{record.mainType}" data="{record}"/>
    </f:for>
</main>
Copied!

The f:cObject ViewHelper above uses the rendering definition of the tt_content table {record.mainType} to render the Content Element from the list. The attribute data expects the raw database record, which is retrieved from {record}.

Feature: #104002 - Schema API 

See forge#104002

Description 

A new Schema API is introduced to access information about TCA structures in a unified way.

The main goal of this architecture is to reduce direct access to $GLOBALS['TCA'] after the Bootstrap process is completed.

The Schema API implements the following design goals:

  1. An object-oriented approach to access common TCA information such as if a database table is localizable or workspace-aware, if it has a "deleted" field ("soft-delete"), or other common functionality such as "enableFields" / "enablecolumns", which can be accessed via "Capabilities" within a Schema.
  2. A unified way to access information which "types" a TCA table has available, such as "tt_content", where the "CType" field is the divisor for types, thus, allowing a Schema to have sub-schemata for a TCA Table.

    The API in turn then handles which fields are available for a specific "CType". An example is "tt_content" with type "textpic": The sub-schema "tt_content.textpic" only contains the fields that are registered of that "CType", such as "bodytext", which then knows it is a Rich Text Field (the default column does not have this information), or "image" (a file relation field), but the sub-schema does not contain fields that are irrelevant for this type, such as "assets" (also a file relation field).

  3. An abstracted approach to available TCA field types such as "input" or "select", which also takes information into account, if a select field is a selection of a static list (such as "pages.layout") or if it contains a relation to another schema or field (based on "foreign_table"). Previously, this was evaluated in many places in TYPO3 Core, and can now be reduced.

    Thus, Schema API can now be utilized to determine the RelationshipType of a relational field type in a unified way without having to deal with deeply nested arrays.

  4. Information about relations to other database tables or fields. This is especially useful when dealing with Inline elements or category selection fields.

    Schema API can find out, which fields of other schemata are pointing to one-self. Schema API differentiates between an "Active Relation" and a "Passive Relation". An Active Relation is the information that a field such as "pages.media" (a field of type "file") contains a reference to the "sys_file_reference.uid_foreign" field. Active Relations in consequence are connected to a specific field (of type RelationalFieldTypeInterface).

    In turn, a "Passive Relation" is the information what other schemata/fields are pointing to a specific table or field.

    A common example of a "Passive Relation" is "sys_workspace_stage": The information stored in $GLOBALS[TCA][sys_workspace_stage] does not contain the information that this table is actually used as a reference from the database field sys_workspace.custom_stages, the sys_workspace_stage Schema now contains this information directly via TcaSchema->getPassiveRelations(). This is possible as TcaSchemaFactory is evaluating all TCA information and holistically as a graph. Passive Relations are currently only connected to a Schema, and Active Relations to a Field or a Schema.

    As the Schema API fetches information solely based on the TCA, an Active Relation only points to possible references, however, the actual reference (does a record really have a connection to another database table) would require an actual Record instance (a database row) to evaluate this information.

    Relations do not know about the "Type" or "Quantity" (many-to-many etc) as this information is already stored in the Field information. For this reason, the "Relations" currently only contain a flat information structure of the table (and possibly a field) pointing TO another schema name (Active Relation) or FROM another schema name / field (Passive Relation).

    Schema API also parses all available FlexForm data structures in order to resolve relations. As a result, a field of type FlexFormField contains a list of possible "FlexFormSchema" instances, which resolve all fields, sheets and section containers within each data structure.

  5. Once built, the Schema can never be changed. Whereas the TCA could be overridden at runtime, all TCA is now evaluated once and then cached. This is a consequence of working with an object-oriented approach.

    If the TCA is changed after the Bootstrap process is completed, the Schema needs to be rebuilt manually, which TYPO3 Core currently does, for example, in some Functional Testing Scenarios.

    All key objects (Schema, FieldType, Capabilities) are treated as immutable DTOs and never contain cross-references to their parent objects (Sub schemata do not know information about their parent schema, a field does not know which schema it belongs to), so the only entry point is always the TcaSchemaFactory object.

    This design allows the API to be fully cacheable at PHP level as a nested tree.

  6. Low-level, not full-fledged but serves as a basis.

    Several API decisions were made in order to let Schema API keep only its original purpose, but can be encapsulated further in other APIs:

    • Schema API is not available during Bootstrap as it needs TCA to be available and fully finished.
    • Schema API does not contain all available TCA properties for each field type. An example is "renderType" for select fields. This information is not relevant when querying records in the frontend, and mainly relevant for FormEngine - it is not generic enough to justify a getter method.
    • Extensibility: custom field types are currently not available until TYPO3 Core as fully migrated to Schema API.
    • User Permissions: Evaluating if a user has access to "tt_content.bodytext" requires information about the currently logged in user, thus it is not part of the Schema API. A "Permission API" should evaluate this information in the future.
    • Available options for a field. As an example, a common scenario is to find out which possible options are available for "pages.backend_layout". In TYPO3 Core an itemsProcFunc is connected to that field in TCA. Whether there is an itemsProcFunc is stored, but Schema API is not designed to actually execute the itemsProcFunc as it is dependent on various factors evaluated during runtime, such as the page it resides on, user permissions or PageTsConfig overrides.

    Schema API is currently marked as internal, as it might be changed during TYPO3 v13 development, because more parts of TYPO3 will be migrated towards Schema API.

    DataHandler and the Record Factory already utilize Schema API in order to reduce direct access to $GLOBALS[TCA].

    In the future Schema API might be used to evaluate information for Site Configurations, like TCA and FlexForms.

Impact 

Reading and writing $GLOBALS[TCA] within Configuration/TCA/* and via TCA Overrides is untouched, as the API is meant for reading the information there in a unified way.

Usage 

The API can now be used to find out information about TCA fields.

public function __construct(
    protected readonly PageRepository $pageRepository,
    protected readonly TcaSchemaFactory $tcaSchemaFactory
) {}

public function myMethod(string $tableName): void
{
    if (!$this->tcaSchemaFactory->has($tableName)) {
        // this table is not managed via TYPO3's TCA API
        return;
    }
    $schema = $this->tcaSchemaFactory->get($tableName);

    // Find out if a table is localizable
    if ($schema->isLocalizable()) {
        // do something
    }

    // Find all registered types
    $types = $schema->getSubSchemata();

}
Copied!

Using the API improves handling for parts such as evaluating columnsOverrides, foreign field structures, FlexForm Schema parsing, and evaluating type fields for database fields.

Feature: #104020 - ViewHelper to check feature flags 

See forge#104020

Description 

The <f:feature> ViewHelper allows integrators to check for feature flags from within Fluid templates. The ViewHelper follows the same rules as the underlying TYPO3 api, which means that undefined flags will be treated as false.

Examples 

Basic usage 

<f:feature name="myFeatureFlag">
   This is being shown if the flag is enabled
</f:feature>
Copied!

feature / then / else 

<f:feature name="myFeatureFlag">
   <f:then>
      Flag is enabled
   </f:then>
   <f:else>
      Flag is undefined or not enabled
   </f:else>
</f:feature>

Copied!

Impact 

Feature flags can now be checked from within Fluid templates.

Feature: #104067 - Sorting of forms in the form module 

See forge#104067

Description 

The listing of existing forms in the Form backend module has been extended to provide sorting functionality.

Impact 

It's now possible to sort the existing forms in the Form backend module.

Feature: #104069 - Improved backend notifications display and handling 

See forge#104069

Description 

The notifications shown on the lower right now have a "Clear all" button to allow the user to clear all notifications with a single click. This button is only displayed when two or more notifications are on screen.

In case the height of the notification container exceeds the viewport, a scroll bar will allow the user to navigate through the notifications.

Impact 

Handling of multiple notifications has been improved by allowing to scroll and clear all notifications at once.

Feature: #104085 - Edit specific columns of multiple records in List module 

See forge#104085

Description 

Using the "Show columns" button on a record table in the Web > List backend module allows to select the columns to be displayed for the corresponding table listing.

When selecting multiple records, it has already been possible to edit all those records at once, using the "Edit" button in the table header.

Now, a new button "Edit columns" has been introduced, which additionally allows to access the editing form for the selected records with just the columns of the current selection (based on "Show columns"). This improves the usability when doing mass editing of specific columns.

Impact 

It's now possible to edit the columns of multiple records in the Web > List backend module, using the new "Edit columns" button.

Feature: #104095 - Edit specific columns of multiple files in Filelist module 

See forge#104095

Description 

Using the "Show columns" action in the File > Filelist backend module allows to select the columns to be displayed file and folder listing.

When selecting multiple files, it has already been possible to edit the metadata of all those records at once, using the "Edit Metadata" button above the listing.

Now, a new button "Edit selected columns" has been introduced, which additionally allows to access the editing form for the selected files with just the columns of the current selection (based on "Show columns"). This improves the usability when doing mass editing of specific columns.

Impact 

It's now possible to edit selected columns of multiple file metadata in the File > Filelist backend module, using the new "Edit selected columns" button.

Feature: #104114 - Command to generate Fluid schema files 

See forge#104114

Description 

With Fluid Standalone 2.12, a new implementation of the XSD schema generator has been introduced, which was previously a separate composer package. These XSD files allow IDEs to provide autocompletion for ViewHelper arguments in Fluid templates, provided that they are included in the template by using the xmlns syntax:

<html
    xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
    xmlns:my="http://typo3.org/ns/Vendor/MyPackage/ViewHelpers"
    data-namespace-typo3-fluid="true"
>
Copied!

A new CLI command has been defined to apply Fluid's new schema generator to TYPO3's Fluid integration. New Fluid APIs are used to find all ViewHelpers that exist in the current project (based on the composer autoloader). Then, TYPO3's configuration is checked for any merged Fluid namespaces (like f:, which consists of both Fluid Standalone and EXT:fluid ViewHelpers which in some cases override each other).

After that consolidation, *.xsd files are created in var/transient/ using another API from Fluid Standalone. These files which will automatically get picked up by supporting IDEs (like PhpStorm) to provide autocompletion in template files.

Impact 

To get autocompletion for all available ViewHelpers in supporting IDEs, the following CLI command can be executed in local development environments:

vendor/bin/typo3 fluid:schema:generate
Copied!

Feature: #104220 - Make parseFunc allowTags and denyTags optional 

See forge#104220

Description 

Defining the TypoScript properties allowTags or denyTags for the HTML processing via stdWrap.parseFunc is now optional.

Besides that, it is now possible to use allowTags = *.

Impact 

By omitting allowTags or