Commit 757f82d1 authored by Oliver Hader's avatar Oliver Hader Committed by Oliver Hader
Browse files

[TASK] Deprecate and replace onchange & onclick attrs in FormEngine

Previously, `fieldChangeFunc` items have been declared as string and
were forwarded as plain inline JavaScript to the client application
using HTML attrs `onchange` and `onclick`.

This change introduces semantic objects for those `fieldChangeFunc`
items that either can be used as structured configuration (JSON) or
still "serialized" to inline JavaScript for legacy applications. New
`OnFieldChangeInterface` provides a hybrid component that is backward
compatible and still supports inline JavaScript as fallback.

Using scalar (string) instructions for `fieldChangeFunc` will
trigger PHP deprecation errors.

Resolves: #91787
Releases: master
Change-Id: I691ea8d12accfcf1568c34e178ce2087fd6ef609
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/71072

Tested-by: Oliver Bartsch's avatarOliver Bartsch <bo@cedev.de>
Tested-by: core-ci's avatarcore-ci <typo3@b13.com>
Tested-by: Jonas Eberle's avatarJonas Eberle <flightvision@googlemail.com>
Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Tested-by: Oliver Hader's avatarOliver Hader <oliver.hader@typo3.org>
Reviewed-by: Oliver Bartsch's avatarOliver Bartsch <bo@cedev.de>
Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Reviewed-by: Oliver Hader's avatarOliver Hader <oliver.hader@typo3.org>
parent 76e6c115
......@@ -17,19 +17,31 @@ import './SelectTree';
import './SelectTreeToolbar';
import 'TYPO3/CMS/Backend/Element/IconElement';
import {TreeNode} from 'TYPO3/CMS/Backend/Tree/TreeNode';
import FormEngine = require('TYPO3/CMS/Backend/FormEngine');
import OnFieldChangeItem = TYPO3.CMS.Backend.OnFieldChangeItem;
export class SelectTreeElement {
private readonly recordField: HTMLInputElement = null;
private readonly tree: SelectTree = null;
constructor(treeWrapperId: string, treeRecordFieldId: string, callback: Function) {
constructor(treeWrapperId: string, treeRecordFieldId: string, callback?: Function, onFieldChangeItems?: OnFieldChangeItem[]) {
if (callback instanceof Function && onFieldChangeItems instanceof Array) {
throw new Error('Cannot assign both `callback` and `onFieldChangeItems`');
}
this.recordField = <HTMLInputElement>document.getElementById(treeRecordFieldId);
const treeWrapper = <HTMLElement>document.getElementById(treeWrapperId);
this.tree = document.createElement('typo3-backend-form-selecttree') as SelectTree;
this.tree.classList.add('svg-tree-wrapper');
this.tree.addEventListener('typo3:svg-tree:nodes-prepared', this.loadDataAfter);
this.tree.addEventListener('typo3:svg-tree:node-selected', this.selectNode);
this.tree.addEventListener('typo3:svg-tree:node-selected', () => { callback(); } );
if (callback instanceof Function) {
// @deprecated
this.tree.addEventListener('typo3:svg-tree:node-selected', () => { callback(); } );
} else if (onFieldChangeItems instanceof Array) {
this.tree.addEventListener('typo3:svg-tree:node-selected', () => { FormEngine.processOnFieldChange(onFieldChangeItems) } );
}
const settings = {
id: treeWrapperId,
......
......@@ -48,6 +48,12 @@ declare namespace TYPO3 {
}
}
// @todo transform to proper interface, once FormEngine.js is migrated to TypeScript
export interface OnFieldChangeItem {
name: string;
data: {[key: string]: string|number|boolean|null}
}
export class FormEngine {
public readonly Validation: FormEngineValidation;
public legacyFieldChangedCb(): void;
......@@ -67,6 +73,7 @@ declare namespace TYPO3 {
public initializeNullNoPlaceholderCheckboxes(): void;
public initializeNullWithPlaceholderCheckboxes(): void;
public requestFormEngineUpdate(askForUpdate: boolean): void
public processOnFieldChange(items: OnFieldChangeItem[]): void
}
export class MultiStepWizard {
......
......@@ -148,10 +148,6 @@ parameters:
path: typo3/sysext/core/Classes/Database/Schema/ConnectionMigrator.php
# Ignored errors for level 4
-
message: "#^Ternary operator condition is always false\\.$#"
count: 1
path: typo3/sysext/backend/Classes/Form/Element/SelectCheckBoxElement.php
-
message: "#^Ternary operator condition is always true\\.$#"
count: 2
......
......@@ -19,6 +19,7 @@ namespace TYPO3\CMS\Backend\Controller;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Form\Behavior\UpdateValueOnFieldChange;
use TYPO3\CMS\Backend\Form\FormDataCompiler;
use TYPO3\CMS\Backend\Form\FormDataGroup\TcaDatabaseRecord;
use TYPO3\CMS\Backend\Form\NodeFactory;
......@@ -123,15 +124,14 @@ class FormFlexAjaxController extends AbstractFormEngineAjaxController
$formData['parameterArray']['itemFormElName'] = 'data[' . $tableName . '][' . $formData['databaseRow']['uid'] . '][' . $fieldName . ']';
// JavaScript code for event handlers:
// @todo: see if we can get rid of this - used in group elements, and also for the "reload" on type field changes
// Client-side behavior for event handlers:
$formData['parameterArray']['fieldChangeFunc'] = [];
$formData['parameterArray']['fieldChangeFunc']['TBE_EDITOR_fieldChanged'] = 'TBE_EDITOR.fieldChanged('
. GeneralUtility::quoteJSvalue($tableName)
. ',' . GeneralUtility::quoteJSvalue($formData['databaseRow']['uid'])
. ',' . GeneralUtility::quoteJSvalue($fieldName)
. ',' . GeneralUtility::quoteJSvalue($formData['parameterArray']['itemFormElName'])
. ');';
$formData['parameterArray']['fieldChangeFunc']['TBE_EDITOR_fieldChanged'] = new UpdateValueOnFieldChange(
$tableName,
$formData['databaseRow']['uid'],
$fieldName,
$formData['parameterArray']['itemFormElName']
);
// @todo: check GroupElement for usage of elementBaseName ... maybe kick that thing?
......
......@@ -59,15 +59,23 @@ class LinkBrowserController extends AbstractLinkBrowserController
$this->parameters['fieldChangeFunc'] = [];
}
unset($this->parameters['fieldChangeFunc']['alert']);
$update = [];
foreach ($this->parameters['fieldChangeFunc'] as $v) {
$update[] = 'FormEngineLinkBrowserAdapter.getParent().' . $v;
}
$inlineJS = implode(LF, $update);
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/FormEngineLinkBrowserAdapter', 'function(FormEngineLinkBrowserAdapter) {
FormEngineLinkBrowserAdapter.updateFunctions = function() {' . $inlineJS . '};
}');
if (($this->parameters['fieldChangeFuncType'] ?? null) === 'items') {
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/FormEngineLinkBrowserAdapter', 'function(FormEngineLinkBrowserAdapter) {
FormEngineLinkBrowserAdapter.onFieldChangeItems = ' . json_encode($this->parameters['fieldChangeFunc'], JSON_HEX_APOS | JSON_HEX_QUOT) . ';
}');
} else {
// @deprecated
$update = [];
foreach ($this->parameters['fieldChangeFunc'] as $v) {
// @todo this is very special and only works when JS code invokes global `window` items
$update[] = 'FormEngineLinkBrowserAdapter.getParent().' . $v;
}
$inlineJS = implode(LF, $update);
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/FormEngineLinkBrowserAdapter', 'function(FormEngineLinkBrowserAdapter) {
FormEngineLinkBrowserAdapter.updateFunctions = function() {' . $inlineJS . '};
}');
}
}
/**
......@@ -109,16 +117,29 @@ class LinkBrowserController extends AbstractLinkBrowserController
if ($handleFlexformSections && preg_match($pattern, $this->parameters['itemName'], $matches)) {
$originalName = $matches[1];
$cleanedName = $matches[2] . $matches[4];
foreach ($fieldChangeFunctions as &$value) {
$value = str_replace($originalName, $cleanedName, $value);
}
unset($value);
$fieldChangeFunctions = $this->strReplaceRecursively(
$originalName,
$cleanedName,
$fieldChangeFunctions
);
}
$result = hash_equals(GeneralUtility::hmac(serialize($fieldChangeFunctions), 'backend-link-browser'), $this->parameters['fieldChangeFuncHash']);
}
return $result;
}
protected function strReplaceRecursively(string $search, string $replace, array $array): array
{
foreach ($array as &$item) {
if (is_array($item)) {
$item = $this->strReplaceRecursively($search, $replace, $item);
} else {
$item = str_replace($search, $replace, $item);
}
}
return $array;
}
/**
* Return the ID of current page
*
......
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Backend\Form\Behavior;
interface OnFieldChangeInterface
{
/**
* Backward compatible fallback, returning deprecated
* JavaScript code for `onclick` element attrs.
*
* @return string
*/
public function __toString(): string;
/**
* @return array{name: string, data?: string}
*/
public function toArray(): array;
}
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Backend\Form\Behavior;
use TYPO3\CMS\Core\Utility\GeneralUtility;
trait OnFieldChangeTrait
{
/**
* @param array<string, string|OnFieldChangeInterface> $items `fieldChangeFunc` items
* @return array<int, array>
*/
protected function getOnFieldChangeItems(array $items): array
{
if (empty($items)) {
return [];
}
return array_map(
static function (OnFieldChangeInterface $item) {
return $item->toArray();
},
// omitting array keys
array_values($items)
);
}
/**
* @param string $event target client event, either `change` or `click`
* @param array<string, string|OnFieldChangeInterface> $items `fieldChangeFunc` items
* @return array<string, string> HTML attrs, not encoded - consumers MUST encode with `htmlspecialchars`
*/
protected function getOnFieldChangeAttrs(string $event, array $items): array
{
if (empty($items)) {
return [];
}
if ($this->validateOnFieldChange($items)) {
$onFieldChangeItems = $this->getOnFieldChangeItems($items);
$attrs = [
'data-formengine-field-change-event' => $event,
'data-formengine-field-change-items' => GeneralUtility::jsonEncodeForHtmlAttribute($onFieldChangeItems, false),
];
} else {
$attrs = [
'on' . $event => implode(';', $items),
];
}
return $attrs;
}
/**
* @param array<string, string|OnFieldChangeInterface> $items `fieldChangeFunc` items
* @param bool $deprecate whether to trigger deprecations
* @return bool whether all items implement `OnFieldChangeInterface`
*/
protected function validateOnFieldChange(array $items, bool $deprecate = true): bool
{
$result = true;
// all items are processed, to log all possible deprecated usages
foreach ($items as $name => $item) {
if ($item instanceof OnFieldChangeInterface) {
continue;
}
$result = false;
if (!$deprecate) {
continue;
}
trigger_error(
sprintf('Using scalar `fieldChangeFunc` for `%s` is deprecated and will be removed in TYPO3 v12.0. Use `OnFieldChangeInterface` instead.', $name),
E_USER_DEPRECATED
);
}
return $result;
}
/**
* Forwards URL query params for `LinkBrowserController`
* @param array<string, string|OnFieldChangeInterface> $items `fieldChangeFunc` items
* @return array<string, string> relevant URL query params for `LinkBrowserController`
*/
protected function forwardOnFieldChangeQueryParams(array $items): array
{
if ($this->validateOnFieldChange($items, false)) {
$type = 'items';
$func = $this->getOnFieldChangeItems($items);
} else {
$type = 'raw';
$func = $items;
}
return [
'fieldChangeFunc' => $func,
'fieldChangeFuncType' => $type,
'fieldChangeFuncHash' => GeneralUtility::hmac(serialize($func), 'backend-link-browser'),
];
}
}
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Backend\Form\Behavior;
/**
* Provides reload behavior in form view,
* in case a particular field has been changed.
*/
class ReloadOnFieldChange implements OnFieldChangeInterface
{
protected bool $confirmation;
public function __construct(bool $confirmation)
{
$this->confirmation = $confirmation;
}
public function __toString(): string
{
return $this->generateInlineJavaScript();
}
public function toArray(): array
{
return [
'name' => 'typo3-backend-form-reload',
'data' => [
'confirmation' => $this->confirmation,
],
];
}
protected function generateInlineJavaScript(): string
{
if ($this->confirmation) {
$alertMsgOnChange = 'Modal.confirm('
. 'TYPO3.lang["FormEngine.refreshRequiredTitle"],'
. ' TYPO3.lang["FormEngine.refreshRequiredContent"]'
. ')'
. '.on('
. '"button.clicked",'
. ' function(e) { if (e.target.name == "ok") { FormEngine.saveDocument(); } Modal.dismiss(); }'
. ');';
} else {
$alertMsgOnChange = 'FormEngine.saveDocument();';
}
return sprintf(
"require(['TYPO3/CMS/Backend/FormEngine', 'TYPO3/CMS/Backend/Modal'], function (FormEngine, Modal) { %s });",
$alertMsgOnChange
);
}
}
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Backend\Form\Behavior;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Updates bitmask values for multi-checkboxes.
*/
class UpdateBitmaskOnFieldChange implements OnFieldChangeInterface
{
protected int $position;
protected int $total;
protected bool $invert;
protected string $elementName;
public function __construct(int $position, int $total, bool $invert, string $elementName)
{
$this->position = $position;
$this->total = $total;
$this->invert = $invert;
$this->elementName = $elementName;
}
public function __toString(): string
{
return $this->generateInlineJavaScript();
}
public function toArray(): array
{
return [
'name' => 'typo3-backend-form-update-bitmask',
'data' => [
'position' => $this->position,
'total' => $this->total,
'invert' => $this->invert,
'elementName' => $this->elementName,
],
];
}
protected function generateInlineJavaScript(): string
{
$mask = 2 ** $this->position;
$unmask = (2 ** $this->total) - $mask - 1;
$elementRef = 'document.editform[' . GeneralUtility::quoteJSvalue($this->elementName) . ']';
return sprintf(
'%s.value = %sthis.checked ? (%s.value|%d) : (%s.value&%d);'
. " %s.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));",
$elementRef,
$this->invert ? '!' : '',
$elementRef,
$mask,
$elementRef,
$unmask,
$elementRef
);
}
}
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Backend\Form\Behavior;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Updates `TBE_EDITOR` value (the default action),
* in case a particular field has been changed.
*/
class UpdateValueOnFieldChange implements OnFieldChangeInterface
{
protected string $tableName;
protected string $identifier;
protected string $fieldName;
protected string $elementName;
public function __construct(string $tableName, string $identifier, string $fieldName, string $elementName)
{
$this->tableName = $tableName;
$this->identifier = $identifier;
$this->fieldName = $fieldName;
$this->elementName = $elementName;
}
public function __toString(): string
{
return $this->generateInlineJavaScript();
}
public function withElementName(string $elementName): self
{
if ($this->elementName === $elementName) {
return $this;
}
$target = clone $this;
$target->elementName = $elementName;
return $target;
}
public function toArray(): array
{
return [
'name' => 'typo3-backend-form-update-value',
'data' => [
'tableName' => $this->tableName,
'identifier' => $this->identifier,
'fieldName' => $this->fieldName,
'elementName' => $this->elementName,
],
];
}
protected function generateInlineJavaScript(): string
{
$args = array_map(
[GeneralUtility::class, 'quoteJSvalue'],
[$this->tableName, $this->identifier, $this->fieldName, $this->elementName]
);
return sprintf('TBE_EDITOR.fieldChanged(%s);', implode(',', $args));
}
}
......@@ -15,6 +15,8 @@
namespace TYPO3\CMS\Backend\Form\Container;
use TYPO3\CMS\Backend\Form\Behavior\ReloadOnFieldChange;
use TYPO3\CMS\Backend\Form\Behavior\UpdateValueOnFieldChange;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Localization\LanguageService;
......@@ -77,6 +79,7 @@ class FlexFormElementContainer extends AbstractContainer
'label' => $languageService->sL(trim($flexFormFieldArray['label'] ?? '')),
'config' => $flexFormFieldArray['config'] ?? [],
'children' => $flexFormFieldArray['children'] ?? [],
// https://docs.typo3.org/m/typo3/reference-tca/master/en-us/Columns/Properties/OnChange.html
'onChange' => $flexFormFieldArray['onChange'] ?? '',
],
'fieldChangeFunc' => $parameterArray['fieldChangeFunc'],
......@@ -87,23 +90,9 @@ class FlexFormElementContainer extends AbstractContainer
$fakeParameterArray['fieldConf']['description'] = $flexFormFieldArray['description'];
}
$alertMsgOnChange = '';
if (isset($fakeParameterArray['fieldConf']['onChange']) && $fakeParameterArray['fieldConf']['onChange'] === 'reload') {
if ($this->getBackendUserAuthentication()->jsConfirmation(JsConfirmation::TYPE_CHANGE)) {
$alertMsgOnChange = 'Modal.confirm('
. 'TYPO3.lang["FormEngine.refreshRequiredTitle"],'
. ' TYPO3.lang["FormEngine.refreshRequiredContent"]'
. ')'
. '.on('
. '"button.clicked",'
. ' function(e) { if (e.target.name == "ok") { FormEngine.saveDocument(); } Modal.dismiss(); }'
. ');';
} else {
$alertMsgOnChange = 'FormEngine.saveDocument();';
}
}
if ($alertMsgOnChange) {
$fakeParameterArray['fieldChangeFunc']['alert'] = 'require([\'TYPO3/CMS/Backend/FormEngine\', \'TYPO3/CMS/Backend/Modal\'], function (FormEngine, Modal) {' . $alertMsgOnChange . '});';
$confirmation = $this->getBackendUserAuthentication()->jsConfirmation(JsConfirmation::TYPE_CHANGE);
$fakeParameterArray['fieldChangeFunc']['alert'] = new ReloadOnFieldChange($confirmation);
}
$originalFieldName = $parameterArray['itemFormElName'];
......@@ -112,8 +101,12 @@ class FlexFormElementContainer extends AbstractContainer
// If calculated itemFormElName is different from originalFieldName
// change the originalFieldName in TBE_EDITOR_fieldChanged. This is
// especially relevant for wizards writing their content back to hidden fields
if (!empty($fakeParameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'])) {
$fakeParameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'] = str_replace($originalFieldName, $fakeParameterArray['itemFormElName'], $fakeParameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged']);