.. _profile-editing: =============== Profile editing =============== The :guilabel:`Profile editing` content element first renders all profiles assigned to the authenticated frontend user. Its :guilabel:`Edit` action opens the selected profile in Profile editing; :guilabel:`View` opens the public ``academic_persons`` Detail plugin on the page configured through ``plugin.tx_academicpersons.detailPid``. This is the same target setting used by the Academic Persons list views. The shipped :file:`Resources/Private/Templates/Profile/Index.html` editor template contains four independently persisted areas: * profile fields using the generic JSON update endpoint, * the synchronization checkbox using its own JSON endpoint, * the expanding profile-image editor using dedicated upload and delete endpoints, and * the structured document sections — contracts, their contacts and the profile-information collections — each with their own form, create, update, delete and sort endpoints. Fluid renders the markup of that view, and five custom elements own its behavior. The entry point is maintained as TypeScript in :file:`Resources/Private/TypeScript/frontend/profile.ts`, from which the frontend build generates :file:`Resources/Public/JavaScript/frontend/profile.js`; it defines those elements and does nothing else. Every editor on the page is an ```` element that starts itself when the browser upgrades it, so a second plugin on the page — or one that is loaded into the page later — needs no start-up scan and cannot be started twice. Typed feature modules below :file:`Resources/Private/TypeScript/frontend/profile/` separately own common requests and status output, field editing, documents, rich text, synchronization, image editing and sticky positioning; the five elements below :file:`…/profile/elements/` drive them. All of them are plain custom elements that depend on no user interface library, and none of them renders markup: each controls what Fluid rendered. No framework is bundled with the extension, and none is loaded from anywhere else either. All changes are saved through AJAX without reloading the page. Editable fields are discovered across the complete component root, even when the responsive page layout places them in separate ``data-pe-fields-form`` elements. Every element renders into the light DOM — none of them has a shadow root — so a project's stylesheet reaches every control they render, exactly as it reaches the Fluid-rendered ones. The inline collapse targets, the two status regions and the client-side templates live in the same component scope. Assigned profile overview ========================= ``ProfileController::listAction()`` is the default action. It uses the authenticated frontend-user relation and passes ``{profileListItems}`` to :file:`Resources/Private/Templates/Profile/List.html`. Every item contains the Profile object and the title of its ``sys_language_uid`` from the current site configuration. The profile column contains a square thumbnail or the shipped placeholder followed by the complete name. :guilabel:`Edit` passes the explicit ``profileUid`` to ``indexAction()``; the action resolves that UID again through the authenticated user's assigned profiles before rendering anything. An unassigned or manipulated UID receives the ``403`` access denied response of the site, the same one an unauthenticated visitor gets — raised as a propagated response rather than returned by the action, because on TYPO3 v13 the status code of an Extbase plugin response never reaches the frontend response. The :guilabel:`View` URI uses extension ``academicpersons``, controller ``Profile``, action ``detail`` and plugin ``Detail``. Its target page comes from ``plugin.tx_academicpersons.detailPid`` and must contain the ``academicpersons_detail`` content element. The ProfileEditing TypoScript copies the Academic Persons constant into ``plugin.tx_academicpersonsedit.settings``; there is no separate editor-specific detail PID. Importing :file:`EXT:academic_persons/Configuration/Routes/Detail.yaml` is optional and turns the query-string link into a speaking URI. View data ========= The controller assigns the following variables to the Fluid template: .. list-table:: :header-rows: 1 * - Variable - Description * - ``{profile}`` - Explicitly selected profile after its assignment to the authenticated frontend user has been verified. * - ``{profileListItems}`` - Assigned Profiles and their readable site-language labels. This variable belongs to the default list action and list template. * - ``{profileSections}`` - Ordered Fluid view models generated from :yaml:`profile`. Every section contains regular fields and inserted composite special items. * - ``{specialFields}`` - Typed :yaml:`special` components, including composed title, image and synchronization metadata. * - ``{profileFieldOptions}`` - Options for every configured :yaml:`renderType: select` field. The option source remains the matching Profile TCA field. * - ``{documentSections}`` - Ordered structured-section view models derived from :yaml:`documentSections`, including mapping, read-only and validation metadata plus the typed records. * - ``{imageAllowedMimeTypes}`` - Comma-separated MIME types accepted by the image input. The server validates the configured values independently. * - ``{editorLanguage}`` - The language code of the current site language, lower-cased and without its region. CKEditor is initialized with it; an empty string when the request carries no site language. * - ``{data}`` and ``{record}`` - Current content element data and its record object. View structure ============== The template is intentionally a composition root. The main partial groups are: .. list-table:: :header-rows: 1 * - Partial group - Responsibility * - ``Image/Card.html`` and ``Image/Editor.html`` - :guilabel:`Profile image` heading above the sticky page preview, animated full-width editor, file selection, crop preview and image actions. * - ``Profile/Personal.html``, ``Profile/About.html`` and ``Profile/Fields.html`` - Personal-data and about-section boundaries plus ordered iteration of settings-driven profile fields. * - ``Field/Editable.html``, ``Field/Control.html``, ``Field/Select.html``, ``Field/Checkbox.html`` and ``Field/Group.html`` - ``renderType``-driven controls, previews, grouped values and persistence actions. * - ``Documents/Sections.html``, ``Documents/Contract.html``, ``Documents/ProfileInformation.html`` and their ``*Row.html`` - Structured document sections, their rows, the row actions and the hidden row prototype that is cloned after a create. * - ``Documents/Editor.html`` - The panel of the document editor, as a prototype the element clones; see :ref:`profile-editing-document-editor`. * - ``Header.html`` and ``StatusToast.html`` - Complete profile-name heading with synchronization/edit-all controls, and scoped status output. The status output is two live regions, one assertive and one polite, because a region's politeness cannot be changed reliably once it is in the accessibility tree. The personal form renders its own :guilabel:`Personal data` heading. ``ButtonTemplates.html`` remains a compatibility fallback for existing template overrides; the shipped read view does not use its button-shaped value controls. Layout and responsive behavior ============================== The view uses Bootstrap 5 grid, spacing, typography, background, positioning and form utilities; the Fluid templates contain no inline style declarations. :file:`Resources/Public/Css/frontend/profile-editing.css` is generated from :file:`Resources/Private/Scss/frontend/profile-editing.scss` by the repository build and holds only what Bootstrap cannot express: the ``display: block`` the custom elements need, a ``[hidden]`` rule that outranks Bootstrap's display utilities, three corrections to a surrounding theme (a ``.section`` overflow, one frame spacing variable, the stacking of the sticky card), the focus ring of the controls and buttons, the drag states of a sortable list and the enter/leave classes of the two editor transitions. All Bootstrap button controls of the shipped editor carry ``rounded-0`` so their corners remain square. The focus ring is the one appearance the view takes away from the surrounding theme rather than correcting. Bootstrap draws it with ``box-shadow``, several themes layer further opaque rings behind that one, and a translucent accent ring over an opaque black one reads as a hard dark rectangle around the focused control. A shadow is also the wrong mechanism twice over: it is painted outside the border box, so the ``overflow: hidden`` of the two collapse panels cut the ring off the fields and buttons inside them, and forced-colours mode drops shadows altogether while the rules that draw them also set ``outline: 0``, which leaves the appearance in that mode to the browser rather than to the site. The view therefore draws its own ring as a real ``outline`` on ``:focus-visible`` — the pseudo-class the theme's own rules use, so the rule sits exactly where the defect is — sunk inside the border box with a negative ``outline-offset`` so that no ancestor can clip it, coloured with ``currentcolor`` so that it contrasts with whatever the control it sits on is filled with, and it rewrites ``box-shadow`` in the same rule so no layered ring and no inset shadow survives underneath. It applies to every ``input``, ``select``, ``textarea`` and ``button`` the view renders — the five controls of :file:`Partials/Profile/Field/Control.html`, the image upload of :file:`Partials/Profile/Image/Editor.html`, the synchronisation switch of :file:`Partials/Profile/Header.html` and the buttons of every action group — because two focus appearances in one form would be worse than the one being replaced. ``:focus-visible`` is not a synonym for keyboard focus. Measured in Chrome, a pointer click on a checkbox or on the synchronisation switch does not match it and those two keep Bootstrap's soft glow, while a pointer click on a text input or a select does match it and those take the inset ring. Most of what the view renders is in the second group, so the change is visible to a visitor using a mouse and not only to one using the keyboard. The colour is ``currentcolor``, which is the colour a control draws its own text in — but not the colour of a checkbox tick or a switch knob, which Bootstrap paints as a background image with a hardcoded ``#ffffff``. On those two the ring takes the inherited body colour, ``#212121`` on the ``#577760`` of a checked control with the shipped theme, and measures 3.23:1: enough for WCAG 2.1 SC 1.4.11 and no more, so a site with a darker ``$primary`` has to check that pair. Plain links are not covered, no theme rule takes their focus ring away. CKEditor 5 is covered by halves: its editable region is a ``div``, so it is outside the rule and keeps the library's own focus styling, while the buttons of its toolbar are ``button`` elements below the plugin root and take this ring rather than the library's. That is the same answer as everywhere else - one focus appearance for everything the visitor can focus. Above the grid, the complete profile name and the synchronization/edit-all controls share one responsive header row. The controls wrap below the name on narrow viewports. On ``lg`` and larger viewports the first content row uses a ``4 / 8`` column split. The profile image block has ``sticky-top`` so the image and its edit action stay visible while the profile data scrolls. Below ``lg`` both columns stack in document order. The about section follows the complete first row and therefore never overlaps the sticky column. At runtime ``initializeStickyImageOffset()`` reads the visible outer height of ``#page-header.navbar-fixed-top`` through ``getBoundingClientRect().height``, adds a 10-pixel visual gap and assigns the result to the ``top`` property of ``data-pe-sticky-image``. The measurement itself is ``observePageHeaderOffset()`` of ``academic_persons``, which the public detail view uses for its own sticky navigation — one implementation, two callers. A ``ResizeObserver`` watches the header's ``border-box`` and keeps the offset synchronized whenever the navbar changes height, including height or padding changes caused by a scroll-dependent header state. Environments without it use the window ``resize`` event as a fallback. If the matching fixed page header is absent, Bootstrap's regular ``sticky-top`` value remains in control. The two columns live in their own ``align-items-stretch`` row. The image column inherits the stretched cross-axis size, giving the sticky image a containing block as tall as the adjacent profile data. The full-width about section keeps its own ``col-12`` in a separate sibling row below it. The complete profile name is the page's ``h1`` above both columns. Both Fluid and JavaScript use the ordered ``fields`` list from :yaml:`special.title`. Fluid renders the initial name; ``data-pe-profile-name-field-ids`` lets JavaScript recompose the same name after a successful update without reloading the page. Profile values are rendered as readable text rows with alternating ``bg-body-tertiary`` surfaces. The only read-mode action is a borderless pencil button with an accessible label. Name components and the URL/title pair of each link share one preview row and open as one editing group. Only one field or group is ever open: a second pencil, and :guilabel:`Edit all`, discard the one that is open before they open theirs (:ref:`profile-editing-full-form`). The special name editor retains the established responsive grid (academic title / first name at ``4 / 8`` and middle / last name at ``6 / 6``) without putting layout metadata into YAML. Settings-driven controls ======================== ``ProfileSectionProvider`` converts the typed :yaml:`profile` and :yaml:`special` settings into an ordered Fluid view model. Section placement itself remains explicit in :file:`Templates/Profile/Index.html`: the template decides where ``profileSections.information`` and ``profileSections.aboutme`` appear. It does not enumerate their individual fields. ``Profile/Fields.html`` chooses the field control solely from ``renderType``. Option values are not duplicated in YAML: for a select field, ``ProfileFieldOptionsService`` reads the corresponding Profile TCA items. Preview behavior likewise follows the rendered control type. Removing a special component removes its controls; marking the image or synchronization special ``readonly`` or ``disabled`` also blocks the matching write endpoint, not just its Fluid control. A translated profile initially uses TYPO3's ``parent`` localization state for the image and follows changes to the default-language image. The first upload or deletion in that frontend language changes the image field to ``custom``; the translated profile then owns its FAL reference and can select, replace or delete its image independently. The image controls therefore remain available while editing a translated profile; the endpoint still verifies that the requested profile is assigned to the authenticated frontend user. The shared :file:`Resources/Private/Partials/Profile/Field/Editable.html` partial composes three focused, reusable partials: * :file:`Field/Preview.html` renders the text preview and pencil trigger — the trigger that opens this field and closes whichever other one is open, * :file:`Field/Control.html` renders either ``f:form.textfield`` or ``f:form.textarea``, including the CKEditor hook, and * :file:`Field/Actions.html` renders delete, cancel and save. :file:`Field/Group.html` composes related textfields below one preview. Its ``data-pe-field-ids`` value defines which fields open, cancel and save together; ``data-pe-display-field-ids`` and ``data-pe-display-mode`` control whether the preview joins values (the name) or uses the first non-empty value (link title falling back to its URL). Helptext buttons are edit controls: field and group previews do not render them. They appear after the corresponding field editor is opened. Document helptexts follow the same rule and are present in add/edit forms, but not in a document editor opened in view mode. ``validation.inputType`` supplies the concrete HTML input type. Checkbox controls save immediately on change. Select controls use the same clear, undo and save actions as text fields. The synchronization switch in :file:`Header.html` is persisted through its own endpoint. .. list-table:: :header-rows: 1 * - Requirement - Implementation * - Telephone number - Textfield with ``inputType: 'tel'`` or a validation input type of ``tel``. * - Website address - Textfield with ``inputType: 'url'`` or a validation input type of ``url``. * - Free text input - Default textfield. * - Select - :file:`Field/Select.html`; options come from the configured field's TCA items and changes save immediately. * - Checkbox - :file:`Field/Checkbox.html` for direct Profile flags. The synchronization special uses its own form and endpoint. * - Multiline text - Field partial with ``textarea: true``. Passing ``richText: true`` additionally turns the textarea into the TYPO3 CKEditor 5 when the field is opened. .. _profile-editing-full-form: Two editing modes ================= The profile fields have two mutually exclusive editing modes, and the controls say which one is active. **Single field.** The pencil beside a value opens that field — or, for a group, that group — and the three buttons of :file:`Field/Actions.html` act on it: clear empties the control and keeps it open, undo restores the saved value and closes it, save posts what changed. This is the mode described everywhere else on this page, and the editors of the document and contract panels behave the same way. Only one editor is open at a time — one field or group, the whole form, one document row or one contact of a contract. Opening another one, and entering full form editing, closes the one that is open first. An editor that still holds the values it was opened with closes silently, exactly as its own :guilabel:`Undo` or :guilabel:`Cancel` would close it. An editor with changes does not: the view asks, in a dialog of its own rather than a browser prompt, whether to :guilabel:`Save and continue`, to :guilabel:`Discard changes` or to :guilabel:`Keep editing`. Saving stores exactly what the open editor's own save would store and then opens the other editor; a save the server refuses keeps the visitor in the refused editor with its messages, and opens nothing. Discarding puts the editor back to the stored values and announces it in the polite live region: *Unsaved changes were discarded.* Keeping — and :kbd:`Escape` in the dialog — opens nothing at all. Pressing the pencil of the field or group that is already open changes nothing and keeps what has been typed. The dialog is the ``unsaved-changes`` template of :file:`Partials/Profile/UnsavedChanges.html`, a ```` element the view clones and shows modally; its labels are the extension's own and are overridable with the partial. A site that drops the template leaves the visitor in the editor that is open: nothing is thrown away because a template is missing. **Full form.** :guilabel:`Edit all` opens every editable field of the profile at once. While it is open, every per-field and per-group button group is hidden — including the undo beside an autosaving checkbox — and one bar governs the whole form. :file:`Field/FormActions.html` renders it at the end of every ``data-pe-fields-form``, and the shipped template has two of those (personal data and about me); each bar acts on all of them. .. list-table:: :header-rows: 1 * - Control - Behavior * - :guilabel:`Apply` - Posts every changed field in one request to the generic field update endpoint below. On success the stored values are written back into the controls, the previews and the name heading, the form closes and the focus returns to :guilabel:`Edit all`. * - :guilabel:`Undo` - Restores every field to the value that is stored and keeps the form open. Announced in the polite live region. * - :guilabel:`Discard` - Restores every field and closes the form. Pressing :guilabel:`Edit all` again does the same. ``updateAction()`` validates the complete field map before it persists anything, so an apply either stores all of it or none of it. A refusal reverts nothing: the entered values stay where they were entered, each refused field is marked and described by its own message, the caret goes to the first of them, and the refusal is announced once rather than once per field. While an apply is on its way to the server, :guilabel:`Undo`, :guilabel:`Discard`, :guilabel:`Edit all` and :kbd:`Escape` do nothing. The request cannot be taken back, and reverting under it would leave the stored profile and the editor's own baseline disagreeing without anything on screen saying so. The same holds for a save beside a *single* field: because opening an editor now discards one, a pencil, :guilabel:`Edit all` and the :guilabel:`Delete content` and :guilabel:`Undo` beside a field or group do nothing at all while such a save is on its way — not even open the editor that was asked for. All of them work again as soon as the answer has been written back, and while they do not, they say so in the polite live region: *Please wait until the change has been saved.* A request that the server accepts and never answers therefore leaves the editors of that profile refusing until the page is reloaded. The frontend deliberately sets no timeout on its requests, so nothing here decides on the visitor's behalf that a slow save has failed. A pencil pressed while :guilabel:`Edit all` is open does nothing at all, and says nothing: every field is already open, and the form's own bar is the way out of the mode. A checkbox that saves on change does not save while the form is open — it is applied with everything else. Without that it would reach the database while the visitor is still deciding and :guilabel:`Discard` could not take it back. The synchronization switch of :file:`Header.html` is outside the field forms and keeps saving immediately. Keyboard and assistive technology --------------------------------- * Opening the form puts the caret in the first editable field. * The bar stands after the fields in document order, so tabbing on from the last field reaches :guilabel:`Apply`, :guilabel:`Undo`, :guilabel:`Discard`. * :kbd:`Escape` discards the form, unless the caret is inside a CKEditor instance, where the key closes the editor's own balloon first. * :kbd:`Ctrl` + :kbd:`Enter` applies it, and so does :kbd:`Enter` in a text field: the form is submitted and the submission is turned into an apply rather than into a page load. * Both keys are handled by the field form itself, so a document, contract or image editor that is open at the same time keeps its own handling of them. * The bar is a ``role="group"`` with its own accessible name. :guilabel:`Edit all` carries ``aria-pressed`` and names the forms it controls in ``aria-controls``. Results are announced in the two live regions the editor already has. The restored notice and a success are polite; a failure and a refusal of the form are assertive, because the caret has just been moved to the first refused field and a polite message queued behind that is routinely dropped. A refusal beside a *single* field stays polite — it stands next to the control the visitor is already in. What an override has to keep ---------------------------- :file:`Profile/Fields.html` renders :file:`Profile/Field/FormActions` at its end; an override of it that drops the line leaves the profile with no way to apply the form. An override of the bar itself keeps the ``data-pe-form-actions`` element, its ``data-pe-form-reverted-message`` and the three buttons marked ``data-pe-form-apply``, ``data-pe-form-undo`` and ``data-pe-form-discard``. Everything else about it — the tags, the classes, the labels, the order — is Fluid, and none of it is spelled in JavaScript. The optional ``sectionLabel`` argument names the button group after the section it stands in, so a page with two field forms does not offer two groups of the same name. Generic field update ==================== The URL is generated by ``f:uri.action`` for ``updateAction()`` with page type :php:`1733735` (see :ref:`profile-editing-page-type`). Requests must use ``POST``, ``Content-Type: application/json`` and the request header ``X-Requested-With: XMLHttpRequest`` (see :ref:`profile-editing-request-header`). Only values changed since the last successful save are sent. An empty string clears a property when its section-local validation permits an empty value; omitted properties remain unchanged. .. code-block:: json :caption: Partial profile update { "profile": 123, "data": { "gender": "female", "website": "" } } Unknown profile properties, configured read-only/disabled fields and select values not configured in the matching TCA field are rejected. The direct academic/honorific ``profile.title`` field is an ordinary configured Profile property; the composed display name is :yaml:`special.title`. ``skipSync`` is a direct special property. A configured ``combinedLink`` additionally enables its matching ``*Title`` companion. All other writable Profile properties come from :yaml:`profile`. Extbase validation errors are returned in an ``errors`` object keyed by property path and are mapped back to the corresponding form controls by the frontend module. ``ProfileFieldOptionsService`` supplies both the presentation options and the strict allow-list validation for every configured select. Thus another select does not require a new Fluid partial, validator service or JavaScript branch. Rich-text content fields ======================== The shipped configuration marks five profile properties with :yaml:`renderType: ckeditor`. Four are displayed with the ``information`` section, while ``miscellaneous`` belongs to :yaml:`aboutme`: * ``coreCompetences``, * ``teachingArea``, * ``supervisedDoctoralThesis``, * ``supervisedThesis``, and * ``miscellaneous``. The editor is TYPO3's shipped CKEditor 5 from the ``typo3/cms-rte-ckeditor`` package. The ``profile/rich-text.js`` module imports its CKEditor modules through TYPO3's JavaScript import map; it does not load an editor from a CDN. An editor instance is created lazily when a rich-text field is opened. If initialization fails, the original textarea remains available and the component reports an error. The toolbar intentionally exposes only undo, redo, bold, italic, bulleted lists, numbered lists and links. Editor changes are mirrored into the underlying textarea, so required-field validation, changed-value detection and the existing JSON request remain the single persistence path. CKEditor's initial HTML normalization becomes the local comparison baseline; merely opening an editor therefore does not submit or rewrite legacy content. Profile fields may configure a positive ``characterLimit`` next to ``renderType: ckeditor``. Fluid then adds the limit metadata and an accessible live counter to that field. The shared rich-text module counts normalized visible text, keeps the last accepted value when an addition would exceed the limit and still allows older over-limit content to be shortened. The Extbase validator checks the submitted sanitized value again before the partial update is persisted. The shipped ``miscellaneous`` field uses a limit of 1000. Outside edit mode, each rich-text field renders its formatted content directly and provides a borderless pencil action on the right. Empty values show a localized placeholder. The preview is initially rendered through TYPO3's HTML formatting pipeline and is replaced after a successful save with the sanitized markup returned by the server. The frontend applies the same strict tag, attribute and URI-scheme allowlist without assigning markup through ``innerHTML``. Each open text or rich-text field has three explicit actions. For ``renderType: ckeditor`` only, the action group sits in the label row with Bootstrap's ``ms-auto`` utility, leaving the editor itself full width. Other field types retain the action group beside their control. Selects and checkboxes save immediately when their value changes. * :guilabel:`Delete` (``data-pe-dismiss``) clears the current browser-side draft. The editor stays open and no request is sent. * :guilabel:`Cancel` (``data-pe-cancel``) restores the last successfully persisted value and closes only that field. No request is sent. * :guilabel:`Save` (``data-pe-save``) sends that field through the JSON AJAX endpoint. It closes the field only after a successful response or when there is no changed value to persist. The action group uses Bootstrap utility classes to remain content-sized and align itself to the end of the editor row instead of stretching to the CKEditor height. No additional stylesheet or inline style is required. The :guilabel:`Edit all` toggle beside the page heading opens both regular fields and grouped rows, receives Bootstrap's ``active`` state and changes its label to :guilabel:`Close all`. Activating it again collapses every editor without saving or discarding browser-side drafts. There is no global footer action area; save and undo remain explicit per-field actions. The pencil is rendered through TYPO3's ``core:icon`` ViewHelper. Template overrides may replace the icon but must retain the button's edit hook, ``data-pe-for`` target and accessible label. The profile value itself must not be placed back inside the button. Server-side sanitization ------------------------ Client-side editor configuration is not treated as a security boundary. ``ProfileUpdateValidationService`` derives writable properties from the configured profile sections. It passes every configured :yaml:`renderType: ckeditor` property through ``ProfileRichTextSanitizer`` before validation and persistence. The sanitizer uses TYPO3's allow-list based HTML sanitizer and permits only: * the tags ``p``, ``br``, ``strong``, ``em``, ``ul``, ``ol``, ``li`` and ``a``; * the ``href`` attribute on ``a``; and * local links and the URI schemes ``http``, ``https``, ``mailto`` and ``tel``. Scripts, event-handler attributes, style attributes, images, unknown tags and unsafe URI schemes are removed. The successful JSON response contains the normalized, sanitized values. The frontend replaces its local editor and preview state with exactly those returned values rather than trusting the submitted markup. Each AJAX request is validated as a partial profile submission. The validator selects only submitted or explicitly overridden DTO properties from their configured profile sections. A ``required`` rule on an omitted sibling field therefore does not block a field update or the dedicated ``skipSync`` request. Submitted inline overrides are validated after normalization and sanitization. The extension requires TYPO3 13.4 or TYPO3 14.3. These constraints include the HTML-sanitizer fixes published with TYPO3-CORE-SA-2026-006. Projects must still keep TYPO3 security updates current. Editable structured profile sections ==================================== The profile editing view renders the structured records directly below the :guilabel:`About me` field. ``ProfileDocumentSectionProvider`` supplies one ordered view model instead of duplicating relation mapping in Fluid. It reads all sections, including ``contracts``, from the shared settings graph. That graph is built from the active packages' :file:`EXT:academic_persons/Configuration/AcademicPersons/Settings.yaml` files, so configured order is also presentation order. The same graph supplies the public profile, frontend validation and backend TCA metadata. For every document section the provider reuses the configured ``identifier``, ``fieldName``, record ``type``, LLL ``label``, ``readonly`` state, ``rowFields``, ``actions`` and the section-local validations. The heading is translated directly from that label. A newly configured type consequently does not require another section registry in ``academic_persons_edit``. The generic localized empty state is used until an identifier-specific message is added. ``contracts`` contains ``FGTCLB\AcademicPersons\Domain\Model\Contract`` objects. Every other collection contains ``FGTCLB\AcademicPersons\Domain\Model\ProfileInformation`` objects. Contract rows show the configured values, which are start date and position in the shipped settings. Profile-information rows render only the configured values in their declared order. The aliases ``from``, ``to`` and ``description`` map to ``yearStart``, ``yearEnd`` and ``bodytext``. All sections remain visible when empty and display a localized empty state. The three year properties ``year``, ``yearStart`` and ``yearEnd`` are four digit integers stored in nullable :sql:`int` columns. Their add/edit control is an ```` carrying ``min="0"``, ``max="9999"`` and ``step="1"`` - the bounds of the TCA ``range`` of the same columns, which the endpoint enforces again on every submission, so a client that ignores them is refused rather than clamped. Nothing about a year is formatted and nothing follows a locale. The three controls each use ``col-12 col-md-3`` and share one responsive row on medium and larger viewports. Their HTML and server-side required states come from the same validation set: only a field with the additional ``required`` flag must be filled. In the shipped settings this applies to ``year`` but not to ``from`` or ``to``. .. _profile-editing-contract-dates: The two Contract dates ``validFrom`` and ``validTo`` are the only date fields of the editor, and their control is a plain ```` showing ``d.m.Y`` behind the hint ``dd.mm.yyyy`` - the control the previous editor had. It is **not** an ````: the native control follows the locale of the browser rather than the one of the site, it cannot be styled with the rest of the editor, and its calendar is not the date picker this editor is meant to get. **A date picker is deliberately not shipped yet.** It is a feature of its own, it will not be the browser's, and until it exists the date is typed. The endpoint accepts two formats for such a field. ``d.m.Y`` is what the control shows and submits. ``Y-m-d`` is what the endpoint answered and accepted while the control was a native one and stays accepted, so a client written against that shape keeps working. Both are read strictly - the parsed date is formatted back and has to match what came in, so ``32.01.2026`` is refused rather than read as the first of February - and anything that is neither format is refused with :guilabel:`The value must be a valid date.` The read view is unaffected: a stored date is rendered as the ``MEDIUMDATE`` of the site language, the way the public profile renders it. .. _profile-editing-document-editor: How the document editor is rendered ----------------------------------- The editor of a document row is rendered in the browser, and cannot be rendered anywhere else: its fields, their labels, their select options and their display values all come from the ``documentForm`` response, which the permission-checked endpoint decides per section, per record and per mode. :file:`Partials/Profile/Documents/Editor.html` therefore renders the *shape* of that editor, as a ``