Commit ebf0f1a7 authored by Markus Hoelzle's avatar Markus Hoelzle Committed by Benni Mack
Browse files

[!!!][FEATURE] Move extension configuration to install tool

With #82254 LocalConfiguration serialized array EXT/extConf has been
changed to a not serialized array in EXTENSIONS. This patch follows
up on this task an finishes various tasks:

* An install tool silent upgrader upmerges given EXT/extConf settings
  to EXTENSIONS array. The resulting EXTENSIONS array does not contain
  dots for sub paths in its array key anymore and is accessible with a
  new ExtensionConfiguration->get() API to fetch values and whole
  extension config.
* A new API is introduced to get() and set() extension specific
  configuration, is documented and used throughout the core to not
  unserialize old EXT/extConf anymore. Setting values updates legacy
  EXT/extConf to new values including compatible 'dot' ending on
  nested array configurations.
* If extensions come with new configuration items in ext_conf_template.txt
  a silent upgrader of the install tool synchronizes these to the
  EXTENSIONS and old extConf array. Extension authors can rely on that
  and always fetch new keys from the new ExtensionConfiguration->get()
  API right away. The synchronization is also triggered when new
  extensions are loaded or extensions are updated via the extension
  manager.
* Core usages are adapted to the new API.
* Next to the main get() / set() API, the extension configuration
  form is extracted from the extension manager and put into the install
  tool as a new card in "Settings". The code below is streamlined
  and encapsulated with just a couple of public methods in class
  'ExtensionConfigurationService' as internal class for use in install
  tool and extension manager.

Resolves: #82368
Related: #82254
Releases: master
Change-Id: I88568fa355f8f6fd5acc9850dcdd718fdd9a1b2e
Reviewed-on: https://review.typo3.org/54034


Tested-by: default avatarTYPO3com <no-reply@typo3.com>
Reviewed-by: default avatarDaniel Gorges <daniel.gorges@b13.de>
Tested-by: default avatarDaniel Gorges <daniel.gorges@b13.de>
Reviewed-by: Andreas Fernandez's avatarAndreas Fernandez <typo3@scripting-base.de>
Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
parent dc3770a3
......@@ -20,6 +20,8 @@ $grid-float-breakpoint: $screen-md-min;
@import "libs/chosen";
@import "libs/gridder";
@import "component/card";
@import "component/colorpicker";
@import "typo3/main_form";
//
// Include elements
......
......@@ -21,6 +21,7 @@ use TYPO3\CMS\Backend\Module\ModuleLoader;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Toolbar\ToolbarItemInterface;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
......@@ -280,7 +281,7 @@ class BackendController
$view = $this->getFluidTemplateObject($this->partialPath . 'Backend/Topbar.html');
// Extension Configuration to find the TYPO3 logo in the left corner
$extConf = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['backend'], ['allowed_classes' => false]);
$extConf = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('backend');
$logoPath = '';
if (!empty($extConf['backendLogo'])) {
$customBackendLogo = GeneralUtility::getFileAbsFileName($extConf['backendLogo']);
......
......@@ -21,6 +21,7 @@ use TYPO3\CMS\Backend\LoginProvider\LoginProviderInterface;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\FormProtection\BackendFormProtection;
use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
......@@ -165,7 +166,7 @@ class LoginController
$this->checkRedirect();
// Extension Configuration
$extConf = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['backend'], ['allowed_classes' => false]);
$extConf = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('backend');
// Background Image
if (!empty($extConf['loginBackgroundImage'])) {
......
......@@ -17,6 +17,7 @@ namespace TYPO3\CMS\Backend\Template;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Page\PageRenderer;
......@@ -932,10 +933,9 @@ function jumpToUrl(URL) {
*/
protected function getBackendFavicon()
{
$extConf = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['backend'], ['allowed_classes' => false]);
if (!empty($extConf['backendFavicon'])) {
$path = $this->getUriForFileName($extConf['backendFavicon']);
$backendFavicon = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('backend', 'backendFavicon');
if (!empty($backendFavicon)) {
$path = $this->getUriForFileName($backendFavicon);
} else {
$path = ExtensionManagementUtility::extPath('backend') . 'Resources/Public/Icons/favicon.ico';
}
......
<?php
namespace TYPO3\CMS\Core\Configuration\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!
*/
use TYPO3\CMS\Core\Exception;
/**
* An exception thrown if ExtensionConfiguration->get() is called for
* an extension that has no configuration.
*
* @internal
*/
class ExtensionConfigurationExtensionNotConfiguredException extends Exception
{
}
<?php
namespace TYPO3\CMS\Core\Configuration\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!
*/
use TYPO3\CMS\Core\Exception;
/**
* An exception thrown if ExtensionConfiguration->get() is called with
* a path that does not exist within the extension configuration.
*
* @internal
*/
class ExtensionConfigurationPathDoesNotExistException extends Exception
{
}
<?php
declare(strict_types=1);
namespace TYPO3\CMS\Core\Configuration;
/*
* 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!
*/
use TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationExtensionNotConfiguredException;
use TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationPathDoesNotExistException;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* API to get() and set() instance specific extension configuration options.
*
* Extension authors are encouraged to use this API - it is currently a simple
* wrapper to access TYPO3_CONF_VARS['EXTENSIONS'] but could later become something
* different in case core decides to store extension configuration elsewhere.
*
* Extension authors must not access TYPO3_CONF_VARS['EXTENSIONS'] on their own.
*
* Extension configurations are often 'feature flags' currently defined by
* ext_conf_template.txt files. The core (more specifically the install tool)
* takes care default values and overridden values are properly prepared upon
* loading or updating an extension.
*/
class ExtensionConfiguration
{
/**
* Get a single configuration value, a sub array or the whole configuration.
*
* Examples:
* // Simple and typical usage: Get a single config value, or an array if the key is a "TypoScript"
* // a-like sub-path in ext_conf_template.txt "foo.bar = defaultValue"
* ->get('myExtension', 'aConfigKey');
*
* // Get all current configuration values, always an array
* ->get('myExtension');
*
* // Get a nested config value if the path is a "TypoScript" a-like sub-path
* // in ext_conf_template.txt "FE.forceSalted = defaultValue"
* ->get('myExtension', 'FE/forceSalted')
*
* Notes:
* - Return values are NOT type safe: A boolean false could be returned as string 0.
* Cast accordingly.
* - This API throws exceptions if the path does not exist or the extension
* configuration is not available. The install tool takes care any new
* ext_conf_template.txt values are available TYPO3_CONF_VARS['EXTENSIONS'],
* a thrown exception indicates a programming error on developer side
* and should not be caught.
* - It is not checked if the extension in question is loaded at all,
* it's just checked the extension configuration path exists.
* - Extensions should typically not get configuration of a different extension.
*
* @param string $extension Extension name
* @param string $path Configuration path - eg. "featureCategory/coolThingIsEnabled"
* @return mixed The value. Can be a sub array or a single value.
* @throws ExtensionConfigurationExtensionNotConfiguredException If ext configuration does no exist
* @throws ExtensionConfigurationPathDoesNotExistException If a requested extension path does not exist
* @api
*/
public function get(string $extension, string $path = '')
{
if (!isset($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension]) || !is_array($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension])) {
throw new ExtensionConfigurationExtensionNotConfiguredException(
'No extension configuration for extension ' . $extension . ' found. Either this extension'
. ' has no extension configuration or the configuration is not up to date. Execute the'
. ' install tool to update configuration.',
1509654728
);
}
if (empty($path)) {
return $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'][$extension];
}
if (!ArrayUtility::isValidPath($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'], $extension . '/' . $path)) {
throw new ExtensionConfigurationPathDoesNotExistException(
'Path ' . $path . ' does not exist in extension configuration',
1509977699
);
}
return ArrayUtility::getValueByPath($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'], $extension . '/' . $path);
}
/**
* Store a new or overwrite an existing configuration value.
*
* This is typically used by core internal low level tasks like the install
* tool but may become handy if an extension needs to update extension configuration
* on the fly for whatever reason.
*
* Examples:
* // Enable a single feature
* ->set('myExtension', 'myFeature', true)
*
* // Set a full extension configuration ($value could be a nested array, too)
* ->set('myExtension', '', ['aFeature' => 'true', 'aCustomClass' => 'css-foo'])
*
* // Set a full sub path
* ->set('myExtension', 'myFeatureCategory', ['aFeature' => 'true', 'aCustomClass' => 'css-foo'])
*
* // Unset a whole extension configuration
* ->set('myExtension')
*
* // Unset a single value or sub path
* ->set('myExtension', 'myFeature')
*
* Notes:
* - $path is NOT validated. It is up to an ext author to also define them in
* ext_conf_template.txt to have an interface in install tool reflecting these settings
* - If $path is currently an array, $value overrides the whole thing. Merging existing values
* is up to the extension author
* - Values are not type safe, if the install tool wrote them,
* boolean true could become string 1 on ->get()
* - It is not possible to store 'null' as value, giving $value=null
* or no value at all will unset the path
* - Setting a value and calling ->get() afterwards will still return the old (!) value, the
* new value is only available in ->get() with next request. This is to have consistent
* values if the setting is possibly overwritten in AdditionalConfiguration again, which
* this API does not know and is only evaluated early during bootstrap.
* - Warning on AdditionalConfiguration.php: If this file overwrites settings, it spoils the
* ->set() call and values may not finally end up as expected. Avoid using AdditionalConfiguration.php
* in general ...
*
* @param string $extension Extension name
* @param string $path Configuration path to set - eg. "featureCategory/coolThingIsEnabled"
* @param null $value The value. If null, unset the path
* @api
*/
public function set(string $extension, string $path = '', $value = null)
{
if (empty($extension)) {
throw new \RuntimeException('extension name must not be empty', 1509715852);
}
$configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
if ($path === '' && $value === null) {
// Remove whole extension config
$configurationManager->removeLocalConfigurationKeysByPath([ 'EXTENSIONS/' . $extension ]);
} elseif ($path !== '' && $value === null) {
// Remove a single value or sub path
$configurationManager->removeLocalConfigurationKeysByPath([ 'EXTENSIONS/' . $extension . '/' . $path]);
} elseif ($path === '' && $value !== null) {
// Set full extension config
$configurationManager->setLocalConfigurationValueByPath('EXTENSIONS/' . $extension, $value);
} else {
// Set single path
$configurationManager->setLocalConfigurationValueByPath('EXTENSIONS/' . $extension . '/' . $path, $value);
}
// After TYPO3_CONF_VARS['EXTENSIONS'] has been written, update legacy layer TYPO3_CONF_VARS['EXTENSIONS']['extConf']
// @deprecated since TYPO3 v9, will be removed in v10 with removal of old serialized 'extConf' layer
$extensionsConfigs = $configurationManager->getConfigurationValueByPath('EXTENSIONS');
foreach ($extensionsConfigs as $extensionName => $extensionConfig) {
$extensionConfig = $this->addDotsToArrayKeysRecursiveForLegacyExtConf($extensionConfig);
$configurationManager->setLocalConfigurationValueByPath('EXT/extConf/' . $extensionName, serialize($extensionConfig));
}
}
/**
* The old EXT/extConf layer had '.' (dots) at the end of all nested array keys. This is created here
* to keep EXT/extConf format compatible with old not yet adapted extensions.
* Most prominent usage is ext:saltedpasswords which uses sub keys like FE.forceSalted and BE.forceSalted,
* but extensions may rely on ending dots if using legacy unserialize() on their extensions, too.
*
* A EXTENSIONS array like:
* TYPO3_CONF_VARS['EXTENSIONS']['someExtension'] => [
* 'someKey' => [
* 'someSubKey' => [
* 'someSubSubKey' => 'someValue',
* ],
* ],
* ]
* becomes (serialized) in old EXT/extConf (mind the dots and end of array keys for sub arrays):
* TYPO3_CONF_VARS['EXTENSIONS']['someExtension'] => [
* 'someKey.' => [
* 'someSubKey.' => [
* 'someSubSubKey' => 'someValue',
* ],
* ],
* ]
*
* @param array $extensionConfig
* @return array
* @deprecated since TYPO3 v9, will be removed in v10 with removal of old serialized 'extConf' layer
*/
private function addDotsToArrayKeysRecursiveForLegacyExtConf(array $extensionConfig)
{
$newArray = [];
foreach ($extensionConfig as $key => $value) {
if (is_array($value)) {
$newArray[$key . '.'] = $this->addDotsToArrayKeysRecursiveForLegacyExtConf($value);
} else {
$newArray[$key] = $value;
}
}
return $newArray;
}
}
......@@ -805,25 +805,26 @@ return [
'allowGlobalInstall' => false,
'allowLocalInstall' => true,
'excludeForPackaging' => '(?:\\..*(?!htaccess)|.*~|.*\\.swp|.*\\.bak|\\.sass-cache|node_modules|bower_components)',
'extConf' => [
'saltedpasswords' => serialize([
'BE.' => [
'saltedPWHashingMethod' => \TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt::class,
'forceSalted' => 0,
'onlyAuthService' => 0,
'updatePasswd' => 1,
],
'FE.' => [
'enabled' => 0,
'saltedPWHashingMethod' => \TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt::class,
'forceSalted' => 0,
'onlyAuthService' => 0,
'updatePasswd' => 1,
],
]),
],
'runtimeActivatedPackages' => [],
],
// Custom options shipped by extensions
'EXTENSIONS' => [
'saltedpasswords' => [
'BE' => [
'saltedPWHashingMethod' => \TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt::class,
'forceSalted' => 0,
'onlyAuthService' => 0,
'updatePasswd' => 1,
],
'FE' => [
'enabled' => 0,
'saltedPWHashingMethod' => \TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt::class,
'forceSalted' => 0,
'onlyAuthService' => 0,
'updatePasswd' => 1,
],
],
],
'BE' => [
// Backend Configuration.
'languageDebug' => false,
......
......@@ -18,24 +18,45 @@ return [
],
],
],
'EXT' => [
'extConf' => [
'rsaauth' => 'a:1:{s:18:"temporaryDirectory";s:0:"";}',
'saltedpasswords' => serialize([
'BE.' => [
'saltedPWHashingMethod' => \TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt::class,
'forceSalted' => 0,
'onlyAuthService' => 0,
'updatePasswd' => 1,
],
'FE.' => [
'enabled' => 1,
'saltedPWHashingMethod' => \TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt::class,
'forceSalted' => 0,
'onlyAuthService' => 0,
'updatePasswd' => 1,
],
]),
'EXTENSIONS' => [
'backend' => [
'backendFavicon' => '',
'backendLogo' => '',
'loginBackgroundImage' => '',
'loginFootnote' => '',
'loginHighlightColor' => '',
'loginLogo' => '',
],
'extensionmanager' => [
'automaticInstallation' => 1,
'offlineMode' => 0,
],
'rsaauth' => [
'temporaryDirectory' => '',
],
'saltedpasswords' => [
'BE' => [
'saltedPWHashingMethod' => \TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt::class,
'forceSalted' => 0,
'onlyAuthService' => 0,
'updatePasswd' => 1,
],
'FE' => [
'enabled' => 1,
'saltedPWHashingMethod' => \TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt::class,
'forceSalted' => 0,
'onlyAuthService' => 0,
'updatePasswd' => 1,
],
'checkConfigurationBE' => '0',
'checkConfigurationBE2' => '0',
'checkConfigurationFE' => '0',
'checkConfigurationFE2' => '0',
],
'scheduler' => [
'enableBELog' => 1,
'maxLifetime' => 1440,
'showSampleTasks' => 1,
],
],
'FE' => [
......
.. include:: ../../Includes.txt
====================================================================
Breaking: #82368 - Signal 'afterExtensionConfigurationWrite' removed
====================================================================
See :issue:`82368`
Description
===========
The extension manager no longer emits signal 'afterExtensionConfigurationWrite'.
The code has been moved to the install tool which does not load signal / slot
information at this point.
Impact
======
Slots of this signal are no longer executed.
Affected Installations
======================
Extensions that use signal 'afterExtensionConfigurationWrite'. This is a rather seldom
used signal, relevant mostly only for distributions.
Migration
=========
In many cases it should be possible to use signal 'hasInstalledExtensions' instead
which is fired after an extension has been installed.
.. index:: Backend, LocalConfiguration, PHP-API, NotScanned
\ No newline at end of file
......@@ -9,7 +9,9 @@ See :issue:`82254`
Description
===========
The extension configuration stored in $GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf'] has been deprecated and replaced by a plain array in $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'].
The extension configuration stored in $GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf'] has been
deprecated and replaced by a plain array in $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']. A new
API has been introduced to retrieve extension configuration.
Affected Installations
......@@ -21,6 +23,21 @@ All extensions manually getting settings and unserializing them from $GLOBALS['T
Migration
=========
Switch to the use of $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'] instead and remove all unserialize calls.
Use a new API to retrieve extension configuration, examples:
.. code-block:: php
// Retrieve a single key
$backendFavicon = (bool)GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('backend', 'backendFavicon');
// Retrieve whole configuration
$backendConfiguration = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('backend');
// Fully qualified class names for usage in ext_localconf.php / ext_tables.php
$backendConfiguration = (bool)\TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(
\TYPO3\CMS\Core\Configuration\ExtensionConfiguration::class
)->get('backend');
.. index:: LocalConfiguration, PHP-API, FullyScanned
......@@ -9,14 +9,30 @@ See :issue:`82254`
Description
===========
There is no reason to save the extension configuration as serialized values instead of an plain array. Arrays are easier to handle and there are already parts of the core using arrays (for example the avatar provider registration).
There is no reason to save the extension configuration as serialized values instead of
an plain array. Arrays are easier to handle and there are already parts of the core
using arrays (for example the avatar provider registration).
Therefore the API has been changed to store the extension configuration as an array in $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']. The configuration is merged on save with the default configuration and the full configuration is written to LocalConfiguration.
Therefore the API has been changed to store the extension configuration as an array
in $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']. The configuration is merged on save
with the default configuration and the full configuration is written to LocalConfiguration.
Impact
======
Extension configuration can now be accessed as array directly, without calling unserialize(). The old and new API will co-exist in version 9.
Extension configuration can now be accessed as array directly, without
calling unserialize(). The old and new API will co-exist in version 9.
Use a new API to retrieve extension configuration, examples:
.. code-block:: php
// Retrieve a single key
$backendFavicon = (bool)GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('backend', 'backendFavicon');
// Retrieve whole configuration
$backendConfiguration = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('backend');
.. index:: LocalConfiguration, PHP-API
<?php
declare(strict_types=1);
namespace TYPO3\CMS\Core\Tests\Unit\Configuration;
/*
* 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!
*/
use Prophecy\Argument;
use TYPO3\CMS\Core\Configuration\ConfigurationManager;
use TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationExtensionNotConfiguredException;
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
/**
* Test case
*/
class ExtensionConfigurationTest extends UnitTestCase