...
 
Commits (17)
......@@ -21,6 +21,10 @@ ignoreFiles+="sysext/form/Classes/Mvc/Property/Exception/TypeConverterException.
ignoreFiles+="sysext/core/Classes/Database/Driver/PDOStatement.php"
ignoreFiles+="sysext/core/Classes/Database/Driver/PDOConnection.php"
# both ActionController and AbstractController throw the same exceptions
# until AbstractController is removed
ignoreFiles+="sysext/extbase/Classes/Mvc/Controller/AbstractController.php"
foundNewFile=0
oldFilename=""
firstLineOfMatch=""
......
......@@ -17,19 +17,23 @@ enum Selectors {
toggleSelector = '.t3js-form-field-inputlink-explanation-toggle',
inputFieldSelector = '.t3js-form-field-inputlink-input',
explanationSelector = '.t3js-form-field-inputlink-explanation',
iconSelector = '.t3js-form-field-inputlink-icon',
}
class InputLinkElement {
private element: HTMLSelectElement = null;
private container: HTMLElement = null;
private toggleSelector: HTMLButtonElement = null;
private explanationField: HTMLInputElement = null;
private icon: HTMLSpanElement = null;
constructor(elementId: string) {
$((): void => {
this.element = <HTMLSelectElement>document.querySelector('#' + elementId);
this.container = <HTMLElement>this.element.closest('.t3js-form-field-inputlink');
this.toggleSelector = <HTMLButtonElement>this.container.querySelector(Selectors.toggleSelector);
this.explanationField = <HTMLInputElement>this.container.querySelector(Selectors.explanationSelector);
this.icon = <HTMLSpanElement>this.container.querySelector(Selectors.iconSelector);
this.toggleVisibility(this.explanationField.value === '');
this.registerEventHandler();
});
......@@ -48,7 +52,7 @@ class InputLinkElement {
}
private registerEventHandler(): void {
this.container.querySelector(Selectors.toggleSelector).addEventListener('click', (e: Event): void => {
this.toggleSelector.addEventListener('click', (e: Event): void => {
e.preventDefault();
const explanationShown = !this.explanationField.classList.contains('hidden');
......@@ -60,8 +64,19 @@ class InputLinkElement {
if (explanationShown) {
this.toggleVisibility(explanationShown);
}
this.disableToggle();
this.clearIcon();
});
}
private disableToggle(): void {
this.toggleSelector.classList.add('disabled');
this.toggleSelector.setAttribute('disabled', 'disabled');
}
private clearIcon(): void {
this.icon.innerHTML = '';
}
}
export = InputLinkElement;
......@@ -22,6 +22,7 @@ use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\Routing\RouteNotFoundException;
use TYPO3\CMS\Core\Utility\CommandUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
......@@ -169,8 +170,15 @@ class SystemInformationToolbarItem implements ToolbarItemInterface
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
$view = $this->getFluidTemplateObject('SystemInformationDropDown.html');
try {
$environmentToolUrl = (string)$uriBuilder->buildUriFromRoute('tools_toolsenvironment');
} catch (RouteNotFoundException $e) {
$environmentToolUrl = '';
}
$view->assignMultiple([
'environmentToolUrl' => (string)$uriBuilder->buildUriFromRoute('tools_toolsenvironment'),
'environmentToolUrl' => $environmentToolUrl,
'messages' => $this->systemMessages,
'count' => $this->totalCount > $this->maximumCountInBadge ? $this->maximumCountInBadge . '+' : $this->totalCount,
'severityBadgeClass' => $this->severityBadgeClass,
......
......@@ -221,7 +221,7 @@ class InputLinkElement extends AbstractFormElement
$expansionHtml[] = '<div class="form-wizards-wrap">';
$expansionHtml[] = '<div class="form-wizards-element">';
$expansionHtml[] = '<div class="input-group t3js-form-field-inputlink">';
$expansionHtml[] = '<span class="input-group-addon">' . $linkExplanation['icon'] . '</span>';
$expansionHtml[] = '<span class="t3js-form-field-inputlink-icon input-group-addon">' . $linkExplanation['icon'] . '</span>';
$expansionHtml[] = '<input class="form-control form-field-inputlink-explanation t3js-form-field-inputlink-explanation" data-toggle="tooltip" data-title="' . $explanation . '" value="' . $explanation . '" readonly>';
$expansionHtml[] = '<input type="text"' . GeneralUtility::implodeAttributes($attributes, true) . ' />';
$expansionHtml[] = '<span class="input-group-btn">';
......
......@@ -5,11 +5,13 @@
<h3 class="dropdown-headline">
<f:translate key="systemmessage.header"/>
</h3>
<p class="dropdown-text typo3-module-menu-item submodule mod-tools_toolsenvironment" data-modulename="tools_toolsenvironment">
<f:format.raw>
<f:translate key="systemmessage.intro" arguments="{0: '{environmentToolUrl}'}"/>
</f:format.raw>
</p>
<f:if condition="{environmentToolUrl}">
<p class="dropdown-text typo3-module-menu-item submodule mod-tools_toolsenvironment" data-modulename="tools_toolsenvironment">
<f:format.raw>
<f:translate key="systemmessage.intro" arguments="{0: environmentToolUrl}"/>
</f:format.raw>
</p>
</f:if>
<f:if condition="{systemInformation}">
<hr>
......
......@@ -10,4 +10,4 @@
*
* The TYPO3 project - inspiring people to share!
*/
define(["require","exports","jquery"],function(e,t,i){"use strict";var n;!function(e){e.toggleSelector=".t3js-form-field-inputlink-explanation-toggle",e.inputFieldSelector=".t3js-form-field-inputlink-input",e.explanationSelector=".t3js-form-field-inputlink-explanation"}(n||(n={}));return class{constructor(e){this.element=null,this.container=null,this.explanationField=null,i(()=>{this.element=document.querySelector("#"+e),this.container=this.element.closest(".t3js-form-field-inputlink"),this.explanationField=this.container.querySelector(n.explanationSelector),this.toggleVisibility(""===this.explanationField.value),this.registerEventHandler()})}toggleVisibility(e){this.explanationField.classList.toggle("hidden",e),this.element.classList.toggle("hidden",!e);const t=this.container.querySelector(".form-control-clearable button.close");null!==t&&t.classList.toggle("hidden",!e)}registerEventHandler(){this.container.querySelector(n.toggleSelector).addEventListener("click",e=>{e.preventDefault();const t=!this.explanationField.classList.contains("hidden");this.toggleVisibility(t)}),this.container.querySelector(n.inputFieldSelector).addEventListener("change",()=>{const e=!this.explanationField.classList.contains("hidden");e&&this.toggleVisibility(e)})}}});
\ No newline at end of file
define(["require","exports","jquery"],function(e,t,i){"use strict";var l;!function(e){e.toggleSelector=".t3js-form-field-inputlink-explanation-toggle",e.inputFieldSelector=".t3js-form-field-inputlink-input",e.explanationSelector=".t3js-form-field-inputlink-explanation",e.iconSelector=".t3js-form-field-inputlink-icon"}(l||(l={}));return class{constructor(e){this.element=null,this.container=null,this.toggleSelector=null,this.explanationField=null,this.icon=null,i(()=>{this.element=document.querySelector("#"+e),this.container=this.element.closest(".t3js-form-field-inputlink"),this.toggleSelector=this.container.querySelector(l.toggleSelector),this.explanationField=this.container.querySelector(l.explanationSelector),this.icon=this.container.querySelector(l.iconSelector),this.toggleVisibility(""===this.explanationField.value),this.registerEventHandler()})}toggleVisibility(e){this.explanationField.classList.toggle("hidden",e),this.element.classList.toggle("hidden",!e);const t=this.container.querySelector(".form-control-clearable button.close");null!==t&&t.classList.toggle("hidden",!e)}registerEventHandler(){this.toggleSelector.addEventListener("click",e=>{e.preventDefault();const t=!this.explanationField.classList.contains("hidden");this.toggleVisibility(t)}),this.container.querySelector(l.inputFieldSelector).addEventListener("change",()=>{const e=!this.explanationField.classList.contains("hidden");e&&this.toggleVisibility(e),this.disableToggle(),this.clearIcon()})}disableToggle(){this.toggleSelector.classList.add("disabled"),this.toggleSelector.setAttribute("disabled","disabled")}clearIcon(){this.icon.innerHTML=""}}});
\ No newline at end of file
......@@ -17,6 +17,9 @@
"typo3/cms-core": "10.2.*@dev",
"typo3/cms-recordlist": "10.2.*@dev"
},
"suggest": {
"typo3/cms-install": "To generate url to install tool in environment toolbar"
},
"conflict": {
"typo3/cms": "*"
},
......
......@@ -57,10 +57,12 @@ class Richtext
$pageTs = $this->getPageTsConfiguration($table, $field, $pid, $recordType);
// determine which preset to use
$usePreset = $pageTs['preset'] ?? $tcaFieldConf['richtextConfiguration'] ?? 'default';
$pageTs['preset'] = $pageTs['fieldSpecificPreset'] ?? $tcaFieldConf['richtextConfiguration'] ?? $pageTs['generalPreset'] ?? 'default';
unset($pageTs['fieldSpecificPreset']);
unset($pageTs['generalPreset']);
// load configuration from preset
$configuration = $this->loadConfigurationFromPreset($usePreset);
$configuration = $this->loadConfigurationFromPreset($pageTs['preset']);
// overlay preset configuration with pageTs
ArrayUtility::mergeRecursiveWithOverrule(
......@@ -170,6 +172,9 @@ class Richtext
// Load PageTSconfig configuration
$fullPageTsConfig = $this->getRtePageTsConfigOfPid($pid);
$defaultPageTsConfigOverrides = $fullPageTsConfig['default.'] ?? null;
$defaultPageTsConfigOverrides['generalPreset'] = $fullPageTsConfig['default.']['preset'] ?? null;
$fieldSpecificPageTsConfigOverrides = $fullPageTsConfig['config.'][$table . '.'][$field . '.'] ?? null;
unset($fullPageTsConfig['default.'], $fullPageTsConfig['config.']);
......@@ -181,6 +186,9 @@ class Richtext
ArrayUtility::mergeRecursiveWithOverrule($rtePageTsConfiguration, $defaultPageTsConfigOverrides);
}
$rtePageTsConfiguration['fieldSpecificPreset'] = $fieldSpecificPageTsConfigOverrides['types.'][$recordType . '.']['preset'] ??
$fieldSpecificPageTsConfigOverrides['preset'] ?? null;
// Then overload with RTE.config.tt_content.bodytext
if (is_array($fieldSpecificPageTsConfigOverrides)) {
$fieldSpecificPageTsConfigOverridesWithoutType = $fieldSpecificPageTsConfigOverrides;
......@@ -200,6 +208,8 @@ class Richtext
}
}
unset($rtePageTsConfiguration['preset']);
return $rtePageTsConfiguration;
}
}
......@@ -338,6 +338,11 @@ class RteHtmlParser extends HtmlParser implements LoggerAwareInterface
foreach ($blockSplit as $k => $v) {
if ($k % 2) {
list($tagAttributes) = $this->get_tag_attributes($this->getFirstTag($v), true);
// Anchors would not have an href attribute
if (!isset($tagAttributes['href'])) {
continue;
}
$linkService = GeneralUtility::makeInstance(LinkService::class);
$linkInformation = $linkService->resolve($tagAttributes['href'] ?? '');
......
......@@ -1018,8 +1018,9 @@ class PageRenderer implements SingletonInterface
* @param string $integrity Subresource Integrity (SRI)
* @param bool $defer Flag if property 'defer="defer"' should be added to JavaScript tags
* @param string $crossorigin CORS settings attribute
* @param bool $nomodule Flag if property 'nomodule="nomodule"' should be added to JavaScript tags
*/
public function addJsLibrary($name, $file, $type = '', $compress = false, $forceOnTop = false, $allWrap = '', $excludeFromConcatenation = false, $splitChar = '|', $async = false, $integrity = '', $defer = false, $crossorigin = '')
public function addJsLibrary($name, $file, $type = 'text/javascript', $compress = false, $forceOnTop = false, $allWrap = '', $excludeFromConcatenation = false, $splitChar = '|', $async = false, $integrity = '', $defer = false, $crossorigin = '', $nomodule = false)
{
if (!in_array(strtolower($name), $this->jsLibs)) {
$this->jsLibs[strtolower($name)] = [
......@@ -1035,6 +1036,7 @@ class PageRenderer implements SingletonInterface
'integrity' => $integrity,
'defer' => $defer,
'crossorigin' => $crossorigin,
'nomodule' => $nomodule,
];
}
}
......@@ -1054,8 +1056,9 @@ class PageRenderer implements SingletonInterface
* @param string $integrity Subresource Integrity (SRI)
* @param bool $defer Flag if property 'defer="defer"' should be added to JavaScript tags
* @param string $crossorigin CORS settings attribute
* @param bool $nomodule Flag if property 'nomodule="nomodule"' should be added to JavaScript tags
*/
public function addJsFooterLibrary($name, $file, $type = '', $compress = false, $forceOnTop = false, $allWrap = '', $excludeFromConcatenation = false, $splitChar = '|', $async = false, $integrity = '', $defer = false, $crossorigin = '')
public function addJsFooterLibrary($name, $file, $type = 'text/javascript', $compress = false, $forceOnTop = false, $allWrap = '', $excludeFromConcatenation = false, $splitChar = '|', $async = false, $integrity = '', $defer = false, $crossorigin = '', $nomodule = false)
{
$name .= '_jsFooterLibrary';
if (!in_array(strtolower($name), $this->jsLibs)) {
......@@ -1072,6 +1075,7 @@ class PageRenderer implements SingletonInterface
'integrity' => $integrity,
'defer' => $defer,
'crossorigin' => $crossorigin,
'nomodule' => $nomodule,
];
}
}
......@@ -1090,8 +1094,9 @@ class PageRenderer implements SingletonInterface
* @param string $integrity Subresource Integrity (SRI)
* @param bool $defer Flag if property 'defer="defer"' should be added to JavaScript tags
* @param string $crossorigin CORS settings attribute
* @param bool $nomodule Flag if property 'nomodule="nomodule"' should be added to JavaScript tags
*/
public function addJsFile($file, $type = '', $compress = true, $forceOnTop = false, $allWrap = '', $excludeFromConcatenation = false, $splitChar = '|', $async = false, $integrity = '', $defer = false, $crossorigin = '')
public function addJsFile($file, $type = 'text/javascript', $compress = true, $forceOnTop = false, $allWrap = '', $excludeFromConcatenation = false, $splitChar = '|', $async = false, $integrity = '', $defer = false, $crossorigin = '', $nomodule = false)
{
if (!isset($this->jsFiles[$file])) {
$this->jsFiles[$file] = [
......@@ -1107,6 +1112,7 @@ class PageRenderer implements SingletonInterface
'integrity' => $integrity,
'defer' => $defer,
'crossorigin' => $crossorigin,
'nomodule' => $nomodule,
];
}
}
......@@ -1125,8 +1131,9 @@ class PageRenderer implements SingletonInterface
* @param string $integrity Subresource Integrity (SRI)
* @param bool $defer Flag if property 'defer="defer"' should be added to JavaScript tags
* @param string $crossorigin CORS settings attribute
* @param bool $nomodule Flag if property 'nomodule="nomodule"' should be added to JavaScript tags
*/
public function addJsFooterFile($file, $type = '', $compress = true, $forceOnTop = false, $allWrap = '', $excludeFromConcatenation = false, $splitChar = '|', $async = false, $integrity = '', $defer = false, $crossorigin = '')
public function addJsFooterFile($file, $type = 'text/javascript', $compress = true, $forceOnTop = false, $allWrap = '', $excludeFromConcatenation = false, $splitChar = '|', $async = false, $integrity = '', $defer = false, $crossorigin = '', $nomodule = false)
{
if (!isset($this->jsFiles[$file])) {
$this->jsFiles[$file] = [
......@@ -1142,6 +1149,7 @@ class PageRenderer implements SingletonInterface
'integrity' => $integrity,
'defer' => $defer,
'crossorigin' => $crossorigin,
'nomodule' => $nomodule,
];
}
}
......@@ -2116,9 +2124,10 @@ class PageRenderer implements SingletonInterface
$properties['file'] = $this->getStreamlinedFileName($properties['file']);
$async = $properties['async'] ? ' async="async"' : '';
$defer = $properties['defer'] ? ' defer="defer"' : '';
$nomodule = $properties['nomodule'] ? ' nomodule="nomodule"' : '';
$integrity = $properties['integrity'] ? ' integrity="' . htmlspecialchars($properties['integrity']) . '"' : '';
$crossorigin = $properties['crossorigin'] ? ' crossorigin="' . htmlspecialchars($properties['crossorigin']) . '"' : '';
$tag = '<script src="' . htmlspecialchars($properties['file']) . '" type="' . htmlspecialchars($properties['type']) . '"' . $async . $defer . $integrity . $crossorigin . '></script>';
$tag = '<script src="' . htmlspecialchars($properties['file']) . '" type="' . htmlspecialchars($properties['type']) . '"' . $async . $defer . $integrity . $crossorigin . $nomodule . '></script>';
if ($properties['allWrap']) {
$wrapArr = explode($properties['splitChar'] ?: '|', $properties['allWrap'], 2);
$tag = $wrapArr[0] . $tag . $wrapArr[1];
......@@ -2161,9 +2170,10 @@ class PageRenderer implements SingletonInterface
$type = $properties['type'] ? ' type="' . htmlspecialchars($properties['type']) . '"' : '';
$async = $properties['async'] ? ' async="async"' : '';
$defer = $properties['defer'] ? ' defer="defer"' : '';
$nomodule = $properties['nomodule'] ? ' nomodule="nomodule"' : '';
$integrity = $properties['integrity'] ? ' integrity="' . htmlspecialchars($properties['integrity']) . '"' : '';
$crossorigin = $properties['crossorigin'] ? ' crossorigin="' . htmlspecialchars($properties['crossorigin']) . '"' : '';
$tag = '<script src="' . htmlspecialchars($file) . '"' . $type . $async . $defer . $integrity . $crossorigin . '></script>';
$tag = '<script src="' . htmlspecialchars($file) . '"' . $type . $async . $defer . $integrity . $crossorigin . $nomodule . '></script>';
if ($properties['allWrap']) {
$wrapArr = explode($properties['splitChar'] ?: '|', $properties['allWrap'], 2);
$tag = $wrapArr[0] . $tag . $wrapArr[1];
......
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Core\Resource\Exception;
/*
* 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!
*/
/**
* An exception when something is wrong with the Hash
* Is thrown for example when the driver returns an unexpected (non-string) hash value
*/
class InvalidHashException extends \TYPO3\CMS\Core\Resource\Exception
{
}
......@@ -14,8 +14,11 @@ namespace TYPO3\CMS\Core\Resource\Index;
* The TYPO3 project - inspiring people to share!
*/
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use TYPO3\CMS\Core\Resource\Exception\IllegalFileExtensionException;
use TYPO3\CMS\Core\Resource\Exception\InsufficientFileAccessPermissionsException;
use TYPO3\CMS\Core\Resource\Exception\InvalidHashException;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Resource\ResourceStorage;
......@@ -24,10 +27,12 @@ use TYPO3\CMS\Core\Type\File\ImageInfo;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* The New FAL Indexer
* The FAL Indexer
*/
class Indexer
class Indexer implements LoggerAwareInterface
{
use LoggerAwareTrait;
/**
* @var array
*/
......@@ -228,34 +233,43 @@ class Indexer
protected function processChangedAndNewFiles()
{
foreach ($this->filesToUpdate as $identifier => $data) {
if ($data == null) {
// search for files with same content hash in indexed storage
$fileHash = $this->storage->hashFileByIdentifier($identifier, 'sha1');
$files = $this->getFileIndexRepository()->findByContentHash($fileHash);
$fileObject = null;
if (!empty($files)) {
foreach ($files as $fileIndexEntry) {
// check if file is missing then we assume it's moved/renamed
if (!$this->storage->hasFile($fileIndexEntry['identifier'])) {
$fileObject = $this->getResourceFactory()->getFileObject($fileIndexEntry['uid'], $fileIndexEntry);
$fileObject->updateProperties([
'identifier' => $identifier
]);
$this->updateIndexEntry($fileObject);
$this->identifiedFileUids[] = $fileObject->getUid();
break;
try {
if ($data === null) {
// search for files with same content hash in indexed storage
$fileHash = $this->storage->hashFileByIdentifier($identifier, 'sha1');
$files = $this->getFileIndexRepository()->findByContentHash($fileHash);
$fileObject = null;
if (!empty($files)) {
foreach ($files as $fileIndexEntry) {
// check if file is missing then we assume it's moved/renamed
if (!$this->storage->hasFile($fileIndexEntry['identifier'])) {
$fileObject = $this->getResourceFactory()->getFileObject(
$fileIndexEntry['uid'],
$fileIndexEntry
);
$fileObject->updateProperties(
[
'identifier' => $identifier,
]
);
$this->updateIndexEntry($fileObject);
$this->identifiedFileUids[] = $fileObject->getUid();
break;
}
}
}
// create new index when no missing file with same content hash is found
if ($fileObject === null) {
$fileObject = $this->createIndexEntry($identifier);
$this->identifiedFileUids[] = $fileObject->getUid();
}
} else {
// update existing file
$fileObject = $this->getResourceFactory()->getFileObject($data['uid'], $data);
$this->updateIndexEntry($fileObject);
}
// create new index when no missing file with same content hash is found
if ($fileObject === null) {
$fileObject = $this->createIndexEntry($identifier);
$this->identifiedFileUids[] = $fileObject->getUid();
}
} else {
// update existing file
$fileObject = $this->getResourceFactory()->getFileObject($data['uid'], $data);
$this->updateIndexEntry($fileObject);
} catch (InvalidHashException $e) {
$this->logger->error('Unable to create hash for file ' . $identifier);
}
}
}
......@@ -294,8 +308,9 @@ class Indexer
*
* @param string $identifier
* @return array
* @throws \TYPO3\CMS\Core\Resource\Exception\InvalidHashException
*/
protected function gatherFileInformationArray($identifier)
protected function gatherFileInformationArray($identifier): array
{
$fileInfo = $this->storage->getFileInfoByIdentifier($identifier);
$fileInfo = $this->transformFromDriverFileInfoArrayToFileObjectFormat($fileInfo);
......
......@@ -160,7 +160,7 @@ class ResourceCompressor
$filesToInclude = [];
foreach ($jsFiles as $key => $fileOptions) {
// invalid section found or no concatenation allowed, so continue
if (empty($fileOptions['section']) || !empty($fileOptions['excludeFromConcatenation'])) {
if (empty($fileOptions['section']) || !empty($fileOptions['excludeFromConcatenation']) || !empty($fileOptions['nomodule'])) {
continue;
}
if (!isset($filesToInclude[$fileOptions['section']])) {
......
......@@ -23,6 +23,7 @@ use TYPO3\CMS\Core\Log\LogManager;
use TYPO3\CMS\Core\Registry;
use TYPO3\CMS\Core\Resource\Driver\StreamableDriverInterface;
use TYPO3\CMS\Core\Resource\Exception\ExistingTargetFileNameException;
use TYPO3\CMS\Core\Resource\Exception\InvalidHashException;
use TYPO3\CMS\Core\Resource\Exception\InvalidTargetFolderException;
use TYPO3\CMS\Core\Resource\Index\FileIndexRepository;
use TYPO3\CMS\Core\Resource\OnlineMedia\Helpers\OnlineMediaHelperRegistry;
......@@ -1275,6 +1276,7 @@ class ResourceStorage implements ResourceStorageInterface
*
* @param FileInterface $fileObject
* @param string $hash
* @throws \TYPO3\CMS\Core\Resource\Exception\InvalidHashException
* @return string
*/
public function hashFile(FileInterface $fileObject, $hash)
......@@ -1287,12 +1289,16 @@ class ResourceStorage implements ResourceStorageInterface
*
* @param string $fileIdentifier
* @param string $hash
*
* @throws \TYPO3\CMS\Core\Resource\Exception\InvalidHashException
* @return string
*/
public function hashFileByIdentifier($fileIdentifier, $hash)
{
return $this->driver->hash($fileIdentifier, $hash);
$hash = $this->driver->hash($fileIdentifier, $hash);
if (!is_string($hash) || $hash === '') {
throw new InvalidHashException('Hash has to be non-empty string.', 1551950301);
}
return $hash;
}
/**
......
......@@ -17,7 +17,7 @@ namespace TYPO3\CMS\Core\Routing\Aspect;
*/
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
use TYPO3\CMS\Core\Site\SiteLanguageAwareTrait;
use TYPO3\CMS\Core\Site\SiteLanguageAwareInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
......@@ -98,8 +98,8 @@ class AspectFactory
*/
protected function enrich(AspectInterface $aspect, SiteLanguage $language): AspectInterface
{
if (in_array(SiteLanguageAwareTrait::class, class_uses($aspect), true)) {
/** @var $aspect SiteLanguageAwareTrait */
// the check for the trait can be removed at any time after TYPO3 v11
if ($aspect instanceof SiteLanguageAwareInterface || in_array(SiteLanguageAwareTrait::class, class_uses($aspect), true)) {
$aspect->setSiteLanguage($language);
}
return $aspect;
......
......@@ -16,6 +16,7 @@ namespace TYPO3\CMS\Core\Routing\Aspect;
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Site\SiteLanguageAwareInterface;
use TYPO3\CMS\Core\Site\SiteLanguageAwareTrait;
/**
......@@ -44,7 +45,7 @@ use TYPO3\CMS\Core\Site\SiteLanguageAwareTrait;
* - locale: 'de_.*'
* value: 'übersicht'
*/
class LocaleModifier implements ModifiableAspectInterface
class LocaleModifier implements ModifiableAspectInterface, SiteLanguageAwareInterface
{
use SiteLanguageAwareTrait;
......
......@@ -21,6 +21,7 @@ use TYPO3\CMS\Core\Context\LanguageAspectFactory;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
use TYPO3\CMS\Core\Site\SiteLanguageAwareInterface;
use TYPO3\CMS\Core\Site\SiteLanguageAwareTrait;
use TYPO3\CMS\Core\Utility\GeneralUtility;
......@@ -43,7 +44,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
* routeFieldName: 'path_segment'
* routeValuePrefix: '/'
*/
class PersistedAliasMapper implements PersistedMappableAspectInterface, StaticMappableAspectInterface
class PersistedAliasMapper implements PersistedMappableAspectInterface, StaticMappableAspectInterface, SiteLanguageAwareInterface
{
use SiteLanguageAwareTrait;
......
......@@ -21,6 +21,7 @@ use TYPO3\CMS\Core\Context\LanguageAspectFactory;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
use TYPO3\CMS\Core\Site\SiteLanguageAwareInterface;
use TYPO3\CMS\Core\Site\SiteLanguageAwareTrait;
use TYPO3\CMS\Core\Utility\GeneralUtility;
......@@ -46,7 +47,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
*
* @internal might change its options in the future, be aware that there might be modifications.
*/
class PersistedPatternMapper implements PersistedMappableAspectInterface, StaticMappableAspectInterface
class PersistedPatternMapper implements PersistedMappableAspectInterface, StaticMappableAspectInterface, SiteLanguageAwareInterface
{
use SiteLanguageAwareTrait;
......
......@@ -16,6 +16,7 @@ namespace TYPO3\CMS\Core\Routing\Aspect;
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Site\SiteLanguageAwareInterface;
use TYPO3\CMS\Core\Site\SiteLanguageAwareTrait;
/**
......@@ -37,7 +38,7 @@ use TYPO3\CMS\Core\Site\SiteLanguageAwareTrait;
* 2k18: '2018'
* next: '2019'
*/
class StaticValueMapper implements StaticMappableAspectInterface, \Countable
class StaticValueMapper implements StaticMappableAspectInterface, SiteLanguageAwareInterface, \Countable
{
use SiteLanguageAwareTrait;
......
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Core\Site;
/*
* 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!
*/
/**
* Interface for SiteLanguageAware features of TYPO3
*/
interface SiteLanguageAwareInterface
{
/**
* @param Entity\SiteLanguage $siteLanguage
*/
public function setSiteLanguage(Entity\SiteLanguage $siteLanguage);
/**
* @return Entity\SiteLanguage
*/
public function getSiteLanguage(): Entity\SiteLanguage;
}
......@@ -1226,7 +1226,7 @@ class GeneralUtility
if ($val !== '=') {
if ($valuemode) {
if ($name) {
$attributes[$name] = $val;
$attributes[$name] = htmlspecialchars_decode($val);
$name = '';
}
} else {
......
......@@ -76,7 +76,6 @@ return [
'cols' => 48,
'rows' => 5,
'enableRichtext' => true,
'richtextConfiguration' => 'default',
],
]
],
......
.. include:: ../../Includes.txt
====================================================================================
Deprecation: #89554 - Deprecate \TYPO3\CMS\Extbase\Mvc\Controller\AbstractController
====================================================================================
See :issue:`89554`
Description
===========
The class :php:`\TYPO3\CMS\Extbase\Mvc\Controller\AbstractController` has been marked as deprecated.
The :php:`AbstractController` is an internal class which never really had any functionality besides
providing some basic methods for the :php:`\TYPO3\CMS\Extbase\Mvc\Controller\ActionController`. Therefore
and in order to streamline the codebase of extbase, the :php:`AbstractController` will be removed
with TYPO3 11.0.
Impact
======
As all functionality of the :php:`AbstractController` has been moved to the :php:`ActionController` there is no impact
for extbase extensions that used and extended the :php:`ActionController`.
Affected Installations
======================
Installations that extended the :php:`AbstractController` directly.
Migration
=========
Extend the :php:`ActionController`.
.. index:: PHP-API, PartiallyScanned, ext:extbase
.. include:: ../../Includes.txt
===============================================
Feature: #84990 - Mark broken file links in RTE
===============================================
See :issue:`84990`
Description
===========
Links to files that were detected as broken by the system extension
`linkvalidator` are now marked accordingly in the RTE via
:php:`TYPO3\CMS\Core\Html\Event\BrokenLinkAnalysisEvent`.
Those links are now marked with extra markup (yellow background with
red border) in RTE.
The procedure for marking the broken links in the RTE is as follow:
#. RTE content is fetched from the database. Before it is displayed in
the edit form, RTE transformations are performed.
#. The transformation function parses the text and detects links.
#. For each link, a new PSR-14 event is dispatched.
#. If a listener is attached, it may set the link as broken and will set
the link as "checked".
#. If a link is detected as broken, RTE will mark it as broken.
The implementation for checking file links is supplied by the system
extension `linkvalidator`.
Other extensions can use the event to override the default behaviour.
Impact
======
The behaviour stays the same as before unless the system extension `linkvalidator`
is installed.
If `linkvalidator` is installed and regularly checks for broken file links, those
links will be marked in the RTE.
If `linkvalidator` is used, it is recommended to use the scheduler to regularly
check for broken links.
.. index:: RTE, ext:linkvalidator
.. include:: ../../Includes.txt
====================================================================
Feature: #86759 - Support nomodule attribute for JavaScript includes
====================================================================
See :issue:`86759`
Description
===========
When including JavaScript files in TypoScript, the HTML5 attribute :html:`nomodule` is now
supported.
See https://html.spec.whatwg.org/multipage/scripting.html#attr-script-nomodule
.. code-block:: typoscript
page.includeJSFooter.file = path/to/file.js
page.includeJSFooter.file.nomodule = 1
.. index:: TypoScript
\ No newline at end of file
.. include:: ../../Includes.txt
========================================================================================
Feature: #87380 - Introduce SiteLanguageAwareInterface to denote site language awareness
========================================================================================
See :issue:`87380`
Description
===========
A `SiteLanguageAwareInterface` with the methods `setSiteLanguage(Entity\SiteLanguage $siteLanguage)`
and `getSiteLanguage()` has been introduced. The interface can be used to denote a class as aware of
the site language.
Impact
======
Routing aspects respecting the site language are now using the `SiteLanguageAwareInterface` in addition
to the `SiteLanguageAwareTrait`. The `AspectFactory` check has been adjusted to check for the interface
_or_ the trait. If you are currently using the trait, you should implement the interface as well.
.. index:: PHP-API, ext:core
.. include:: ../../Includes.txt
==============================================================
Feature: #87798 - Provide a way to sort form lists in ext:form
==============================================================
See :issue:`87798`
Description
===========
Forms in ext:form were previously not sorted in any manner,
but just outputted in the order they were read from the filesystem's directories.
Forms can now be sorted by multiple keys in either ascending or descending order.
Two new settings were introduced: ``sortByKeys`` and ``sortAscending``.
Here is an example configuration,
that will sort forms by their name first and by their file uid second:
.. code-block:: yaml
TYPO3:
CMS:
Form:
persistenceManager:
sortByKeys: ['name', 'fileUid']
sortAscending: true
Valid keys, by which the forms can be sorted, are:
``name``
The forms name.
``identifier``
The filename.
``fileUid``
The files uid.
``persistenceIdentifier``
The files location.
Example: ``1:/form_definitions/contact.form.yaml``
``readOnly``
Is the form readonly?
``removable``
Is the form removable?
``location``
Either `storage` or `extension`
``invalid``
Does the form have an error?
Impact
======
Forms will now initially be sorted by their name first and their file uid second in an ascending order.
This affects both the form list shown in the form module as well as the ordering of the available select options when creating a new form content element.
To change the sorting, you can override the configuration via YAML as described by the example above.
.. index:: Backend, ext:form
.. include:: ../../Includes.txt
==============================================================
Important: #88655 - Changed loading order of RTE Configuration
==============================================================
See :issue:`88655`
Description
===========
The order in which RTE Configuration is loaded has been changed.
The new order is:
1. preset defined for a specific field via PageTS
2. richtextConfiguration defined for a specific field via TCA
3. general preset defined via PageTS
4. default
This results in a change if you were used to using `RTE.default.preset` to overwrite _all_ RTE
configuration presets - as those with specific configuration in TCA now use their specific settings
instead of falling back to the default. Please make sure, that this new behavior is fitting for your
use cases.
If you are an extension author and you want your RTE fields to use the systems default configuration
(the one configured for the complete web site) please do not set a specific preset for your fields.
If you as an extension author want to provide a specific preset - for example because you are
providing a custom parseFunc - set the property `richtextConfiguration` in TCA.
If an extension provides a custom preset for a specific field and you as an integrator want to
override that configuration (for example to use "your" default), set it specifically for that field
in TSConfig or overwrite the TCA configuration.
For example:
If the blog extension configures `'richtextConfiguration' => 'blog'` for the tag description and
you want the tag description to use the default preset, set
`RTE.config.tx_blog_domain_model_tag.content.types.text.preset = default`.
.. index:: RTE, TCA, TSConfig, ext:core
\ No newline at end of file
<?php
namespace TYPO3\CMS\Core\Tests\Functional\Page;
/*
......@@ -68,7 +69,16 @@ class PageRendererTest extends \TYPO3\TestingFramework\Core\Functional\Functiona
$headerData = $expectedHeaderData = '<tag method="private" name="test" />';
$subject->addHeaderData($headerData);
$subject->addJsLibrary('test', 'fileadmin/test.js', 'text/javascript', false, false, 'wrapBeforeXwrapAfter', false, 'X');
$subject->addJsLibrary(
'test',
'fileadmin/test.js',
'text/javascript',
false,
false,
'wrapBeforeXwrapAfter',
false,
'X'
);
$expectedJsLibraryRegExp = '#wrapBefore<script src="fileadmin/test\\.(js|\\d+\\.js|js\\?\\d+)" type="text/javascript"></script>wrapAfter#';
$subject->addJsFile('fileadmin/test.js', 'text/javascript', false, false, 'wrapBeforeXwrapAfter', false, 'X');
......@@ -133,10 +143,27 @@ class PageRendererTest extends \TYPO3\TestingFramework\Core\Functional\Functiona
$subject->addFooterData($footerData);
$expectedJsFooterLibraryRegExp = '#wrapBefore<script src="fileadmin/test\\.(js|\\d+\\.js|js\\?\\d+)" type="text/javascript"></script>wrapAfter#';
$subject->addJsFooterLibrary('test', 'fileadmin/test.js', 'text/javascript', false, false, 'wrapBeforeXwrapAfter', false, 'X');
$subject->addJsFooterLibrary(
'test',
'fileadmin/test.js',
'text/javascript',
false,
false,
'wrapBeforeXwrapAfter',
false,
'X'
);
$expectedJsFooterRegExp = '#wrapBefore<script src="fileadmin/test\\.(js|\\d+\\.js|js\\?\\d+)" type="text/javascript"></script>wrapAfter#';
$subject->addJsFooterFile('fileadmin/test.js', 'text/javascript', false, false, 'wrapBeforeXwrapAfter', false, 'X');
$subject->addJsFooterFile(
'fileadmin/test.js',
'text/javascript',
false,
false,
'wrapBeforeXwrapAfter',
false,
'X'
);
$jsFooterInlineCode = $expectedJsFooterInlineCodeString = 'var x = "' . $this->getUniqueId('jsFooterInline-') . '"';
$subject->addJsFooterInlineCode($this->getUniqueId(), $jsFooterInlineCode);
......@@ -178,4 +205,87 @@ class PageRendererTest extends \TYPO3\TestingFramework\Core\Functional\Functiona
self::assertStringContainsString($expectedLanguageLabel2, $renderedString);
self::assertStringContainsString($expectedInlineSettingsReturnValue, $renderedString);
}
/**
* @test
*/
public function pageRendererRendersNomoduleJavascript()
{
$subject = new PageRenderer();
$subject->setCharSet('utf-8');
$subject->setLanguage('default');
$subject->addJsFooterLibrary(
'test',
'fileadmin/test.js',
'text/javascript',
false,
false,
'',
false,
'|',
false,
'',
false,
'',
true
);
$expectedJsFooterLibrary = '<script src="fileadmin/test.js" type="text/javascript" nomodule="nomodule"></script>';
$subject->addJsLibrary(
'test2',
'fileadmin/test2.js',
'text/javascript',
false,
false,
'',
false,
'|',
false,
'',
false,
'',
true
);
$expectedJsLibrary = '<script src="fileadmin/test2.js" type="text/javascript" nomodule="nomodule"></script>';
$subject->addJsFile(
'fileadmin/test3.js',
'text/javascript',
false,
false,
'',
false,
'|',
false,
'',
false,
'',
true
);
$expectedJsFile = '<script src="fileadmin/test3.js" type="text/javascript" nomodule="nomodule"></script>';
$subject->addJsFooterFile(
'fileadmin/test4.js',
'text/javascript',
false,
false,
'',
false,
'|',
false,
'',
false,
'',
true
);
$expectedJsFooter = '<script src="fileadmin/test4.js" type="text/javascript" nomodule="nomodule"></script>';
$renderedString = $subject->render();
self::assertStringContainsString($expectedJsFooterLibrary, $renderedString);
self::assertStringContainsString($expectedJsLibrary, $renderedString);
self::assertStringContainsString($expectedJsFile, $renderedString);
self::assertStringContainsString($expectedJsFooter, $renderedString);
}
}
......@@ -631,4 +631,82 @@ class RteHtmlParserTest extends UnitTestCase
$thisConfig = ['proc.' => $this->procOptions];
self::assertEquals($expectedResult, $subject->RTE_transform($subject->RTE_transform($content, [], 'db', $thisConfig), [], 'rte', $thisConfig));
}
/**
* Data provider for anchorCorrectlyTransformedOnWayToDatabase
*/
public static function anchorCorrectlyTransformedOnWayToDatabaseProvider()
{
return [
[
'<p><a name="some_anchor"></a></p>' . CRLF . '<h3>Some headline here</h3>',
'<p><a name="some_anchor"></a></p>' . CRLF . '<h3>Some headline here</h3>'
],
[
'<p><a id="some_anchor"></a></p>' . CRLF . '<h3>Some headline here</h3>',
'<p><a id="some_anchor"></a></p>' . CRLF . '<h3>Some headline here</h3>'
],
[
'<p><a name="some_anchor" id="some_anchor"></a></p>' . CRLF . '<h3>Some headline here</h3>',
'<p><a name="some_anchor" id="some_anchor"></a></p>' . CRLF . '<h3>Some headline here</h3>'
],
[
'<p><a id="some_anchor">Some text inside the anchor</a></p>',
'<p><a id="some_anchor">Some text inside the anchor</a></p>'
]
];
}
/**
* @test
* @dataProvider anchorCorrectlyTransformedOnWayToDatabaseProvider
* @param $content
* @param $expectedResult
*/
public function anchorCorrectlyTransformedOnWayToDatabase($content, $expectedResult)
{
$eventDispatcher = $this->createMock(EventDispatcherInterface::class);
$subject = new RteHtmlParser($eventDispatcher);
$thisConfig = ['proc.' => $this->procOptions];
self::assertEquals($expectedResult, $subject->RTE_transform($content, [], 'db', $thisConfig));
}
/**
* Data provider for anchorCorrectlyTransformedOnWayToDatabaseAndBackToRTE
*/
public static function anchorCorrectlyTransformedOnWayToDatabaseAndBackToRTEProvider()
{
return [
[
'<p><a name="some_anchor"></a></p>' . CRLF . '<h3>Some headline here</h3>',
'<p><a name="some_anchor"></a></p>' . CRLF . '<h3>Some headline here</h3>'
],
[
'<p><a id="some_anchor"></a></p>' . CRLF . '<h3>Some headline here</h3>',
'<p><a id="some_anchor"></a></p>' . CRLF . '<h3>Some headline here</h3>'
],
[
'<p><a name="some_anchor" id="some_anchor"></a></p>' . CRLF . '<h3>Some headline here</h3>',
'<p><a name="some_anchor" id="some_anchor"></a></p>' . CRLF . '<h3>Some headline here</h3>'
],
[
'<p><a id="some_anchor">Some text inside the anchor</a></p>',
'<p><a id="some_anchor">Some text inside the anchor</a></p>'
]
];
}
/**
* @test
* @dataProvider anchorCorrectlyTransformedOnWayToDatabaseAndBackToRTEProvider
* @param $content
* @param $expectedResult
*/
public function anchorCorrectlyTransformedOnWayToDatabaseAndBackToRTE($content, $expectedResult)
{
$eventDispatcher = $this->createMock(EventDispatcherInterface::class);
$subject = new RteHtmlParser($eventDispatcher);
$thisConfig = ['proc.' => $this->procOptions];
self::assertEquals($expectedResult, $subject->RTE_transform($subject->RTE_transform($content, [], 'db', $thisConfig), [], 'rte', $thisConfig));
}
}
......@@ -641,4 +641,25 @@ class ResourceCompressorTest extends BaseTestCase
$relativeToRootPath = $subject->_call('getFilenameFromMainDir', $filename);
self::assertSame($expected, $relativeToRootPath);
}
/**
* @test
*/
public function nomoduleJavascriptIsNotConcatenated(): void
{
$fileName = 'fooFile.js';
$concatenatedFileName = 'merged_' . $fileName;
$testFileFixture = [
$fileName => [
'file' => $fileName,
'nomodule' => true,
'section' => 'top',
]
];
$result = $this->subject->concatenateJsFiles($testFileFixture);
self::assertArrayNotHasKey($concatenatedFileName, $result);
self::assertTrue($result[$fileName]['nomodule']);
}
}
......@@ -10,6 +10,7 @@ return function (ContainerConfigurator $containerConfigurator, ContainerBuilder
$container->registerForAutoconfiguration(Mvc\RequestHandlerInterface::class)->addTag('extbase.request_handler');
$container->registerForAutoconfiguration(Mvc\Controller\ControllerInterface::class)->addTag('extbase.controller');
$container->registerForAutoconfiguration(Mvc\Controller\AbstractController::class)->addTag('extbase.prototype_controller');
$container->registerForAutoconfiguration(Mvc\Controller\ActionController::class)->addTag('extbase.action_controller');
$container->registerForAutoconfiguration(Mvc\View\ViewInterface::class)->addTag('extbase.view');
$container->addCompilerPass(new class implements CompilerPassInterface {
......@@ -24,6 +25,9 @@ return function (ContainerConfigurator $containerConfigurator, ContainerBuilder
foreach ($container->findTaggedServiceIds('extbase.prototype_controller') as $id => $tags) {
$container->findDefinition($id)->setShared(false);
}
foreach ($container->findTaggedServiceIds('extbase.action_controller') as $id => $tags) {
$container->findDefinition($id)->setShared(false);
}
foreach ($container->findTaggedServiceIds('extbase.view') as $id => $tags) {
$container->findDefinition($id)->setShared(false)->setPublic(true);
}
......
<?php
namespace TYPO3\CMS\Extbase\Tests\Unit\Mvc\Controller;
namespace TYPO3\CMS\Extbase\Tests\UnitDeprecated\Mvc\Controller;
/*
* This file is part of the TYPO3 CMS project.
......@@ -91,7 +91,7 @@ class AbstractControllerTest extends UnitTestCase
->getMock();
$controllerContext->expects(self::once())->method('getFlashMessageQueue')->willReturn($flashMessageQueue);
$controller = $this->getMockForAbstractClass(
$controller = $this->getAccessibleMockForAbstractClass(
\TYPO3\CMS\Extbase\Mvc\Controller\AbstractController::class,
[],
'',
......@@ -100,7 +100,7 @@ class AbstractControllerTest extends UnitTestCase
true,
['dummy']
);
$this->inject($controller, 'controllerContext', $controllerContext);
$controller->_set('controllerContext', $controllerContext);
$controller->addFlashMessage($messageBody, $messageTitle, $severity, $storeInSession);
}
......
......@@ -102,7 +102,7 @@ class EmailViewHelperTest extends ViewHelperBaseTestcase
'Plain email with spam protection' => [
'some@email.tld',
1,
'<a href="javascript:linkTo_UnCryptMailto(\'nbjmup+tpnfAfnbjm\/ume\');">some(at)email.tld</a>',
'<a href="javascript:linkTo_UnCryptMailto(%27nbjmup%2BtpnfAfnbjm%5C%2Fume%27);">some(at)email.tld</a>',
],
'Plain email with ascii spam protection' => [
'some@email.tld',
......@@ -117,7 +117,7 @@ class EmailViewHelperTest extends ViewHelperBaseTestcase
'Susceptible email with spam protection' => [
'"><script>alert(\'email\')</script>',
1,
'<a href="javascript:linkTo_UnCryptMailto(\'nbjmup+\u0022\u003E\u003Ctdsjqu\u003Ebmfsu(\u0027fnbjm\u0027)\u003C0tdsjqu\u003E\');">&quot;&gt;&lt;script&gt;alert(\'email\')&lt;/script&gt;</a>',
'<a href="javascript:linkTo_UnCryptMailto(%27nbjmup%2B%5Cu0022%5Cu003E%5Cu003Ctdsjqu%5Cu003Ebmfsu%28%5Cu0027fnbjm%5Cu0027%29%5Cu003C0tdsjqu%5Cu003E%27);">&quot;&gt;&lt;script&gt;alert(\'email\')&lt;/script&gt;</a>',
],
'Susceptible email with ascii spam protection' => [
'"><script>alert(\'email\')</script>',
......
......@@ -280,12 +280,13 @@ abstract class AbstractFinisher implements FinisherInterface
// resolve (recursively) all array items
if (is_array($needle)) {
return array_map(
function ($item) use ($formRuntime) {
return $this->substituteRuntimeReferences($item, $formRuntime);
},
$needle
);
$substitutedNeedle = [];
foreach ($needle as $key => $item) {
$key = $this->substituteRuntimeReferences($key, $formRuntime);
$item = $this->substituteRuntimeReferences($item, $formRuntime);
$substitutedNeedle[$key] = $item;
}
return $substitutedNeedle;
}
// substitute one(!) variable in string which either could result
......
......@@ -331,7 +331,7 @@ class FormPersistenceManager implements FormPersistenceManagerInterface
}
}
return $forms;
return $this->sortForms($forms);
}
/**
......@@ -771,4 +771,27 @@ class FormPersistenceManager implements FormPersistenceManagerInterface
{
return isset($data['identifier'], $data['type']) && !empty($data['identifier']) && trim($data['type']) === 'Form';
}
/**
* @param array $forms
* @return array
*/
protected function sortForms(array $forms): array
{
$keys = $this->formSettings['persistenceManager']['sortByKeys'] ?? ['name', 'fileUid'];
$ascending = $this->formSettings['persistenceManager']['sortAscending'] ?? true;
usort($forms, function (array $a, array $b) use ($keys) {
foreach ($keys as $key) {
if (isset($a[$key]) && isset($b[$key])) {
$diff = strcasecmp((string)$a[$key], (string)$b[$key]);
if ($diff) {
return $diff;
}
}
}
});
return ($ascending) ? $forms : array_reverse($forms);
}
}
......@@ -6,6 +6,8 @@ TYPO3:
10: 1:/form_definitions/
allowSaveToExtensionPaths: false
allowDeleteFromExtensionPaths: false
sortByKeys: ['name', 'fileUid']
sortAscending: true
#allowedExtensionPaths:
#10: EXT:example/Resources/Private/Forms/
......
......@@ -15,6 +15,8 @@ Full default configuration
10: '1:/form_definitions/'
allowSaveToExtensionPaths: false
allowDeleteFromExtensionPaths: false
sortByKeys: ['name', 'fileUid']
sortAscending: true
prototypes:
standard:
formElementsDefinition:
......
......@@ -113,6 +113,94 @@ allowDeleteFromExtensionPaths
Set this to ``true`` if you want to allow backend users to **delete** forms stored within your own extension.
.. _typo3.cms.form.persistencemanager.sortByKeys:
sortByKeys
-----------------------------
:aspect:`Option path`
TYPO3.CMS.Form.persistenceManager.sortByKeys
:aspect:`Data type`
array
:aspect:`Needed by`
Backend (form manager)
:aspect:`Mandatory`