Skip to content

Commit

Permalink
[BUGFIX] Disable language (de)activation with write-protected configu…
Browse files Browse the repository at this point in the history
…ration

The language management module writes into system/settings.php to
either enable or disable a language. With the changes made with #98957,
the language management should also inform about a write-protected
configuration file and remove any options that conflict with this
state.

This patch removes any option to either enable or disable a language and
moves the Fluid partial to a more generic location aimed for multiple
usages across several modules.

Resolves: #99008
Related: #98957
Releases: main, 12.4
Change-Id: Iae8c75e0727011289c421a66266989f44be76932
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/80829
Tested-by: Benjamin Franzke <ben@bnf.dev>
Reviewed-by: Benjamin Franzke <ben@bnf.dev>
Tested-by: core-ci <typo3@b13.com>
  • Loading branch information
andreaskienast authored and bnf committed Sep 12, 2023
1 parent 3732db0 commit c3ebac6
Show file tree
Hide file tree
Showing 12 changed files with 148 additions and 109 deletions.
Expand Up @@ -328,41 +328,49 @@ class LanguagePacks extends AbstractInteractableModule {
const activateIcon = this.findInModal(this.selectorActivateLanguageIcon).html();
const deactivateIcon = this.findInModal(this.selectorDeactivateLanguageIcon).html();
const updateIcon = this.findInModal(this.selectorLanguageUpdateIcon).html();
const configurationIsWritable = this.getModuleContent().data('configuration-is-writable');
const $markupContainer = $('<div>');

const $tbody = $('<tbody>');
data.languages.forEach((language: any): void => {
const availableMatrixActions = [];
const active = language.active;
if (configurationIsWritable) {
availableMatrixActions.push($('<a>', {
'class': 'btn btn-default t3js-languagePacks-deactivateLanguage',
'data-iso': language.iso,
'title': 'Deactivate',
}).append(deactivateIcon));
}
availableMatrixActions.push($('<a>', {
'class': 'btn btn-default t3js-languagePacks-update',
'data-iso': language.iso,
'title': 'Download language packs',
}).append(updateIcon));
const $tr = $('<tr>');
if (active) {
$tbody.append(
$tr.append(
$('<td>').text(' ' + language.name).prepend(
$('<div />', { class: 'btn-group' }).append(
$('<a>', {
'class': 'btn btn-default t3js-languagePacks-deactivateLanguage',
'data-iso': language.iso,
'title': 'Deactivate',
}).append(deactivateIcon),
$('<a>', {
'class': 'btn btn-default t3js-languagePacks-update',
'data-iso': language.iso,
'title': 'Download language packs',
}).append(updateIcon),
availableMatrixActions
),
),
),
);
} else {
if (configurationIsWritable) {
availableMatrixActions.push($('<a>', {
'class': 'btn btn-default t3js-languagePacks-activateLanguage',
'data-iso': language.iso,
'title': 'Activate',
}).append(activateIcon));
}
$tbody.append(
$tr.addClass('t3-languagePacks-inactive t3js-languagePacks-inactive').css({ 'display': 'none' }).append(
$('<td>').text(' ' + language.name).prepend(
$('<div />', { class: 'btn-group' }).append(
$('<a>', {
'class': 'btn btn-default t3js-languagePacks-activateLanguage',
'data-iso': language.iso,
'title': 'Activate',
}).append(activateIcon),
availableMatrixActions
),
),
),
Expand All @@ -376,24 +384,32 @@ class LanguagePacks extends AbstractInteractableModule {
);
$tbody.append($tr);
});
const globalActions = [];
if (configurationIsWritable) {
globalActions.push($('<button>', {
'class': 'btn btn-default t3js-languagePacks-addLanguage-toggle',
'type': 'button'
}).append(
$('<span>').append(activateIcon),
' Add language',
));
}
globalActions.push($('<button>', {
'class': 'btn btn-default disabled update-all t3js-languagePacks-update',
'type': 'button',
'disabled': 'disabled'
}).append(
$('<span>').append(updateIcon),
' Update all',
));
$markupContainer.append(
$('<h3>').text('Active languages'),
$('<table>', { 'class': 'table table-striped table-bordered' }).append(
$('<thead>').append(
$('<tr>').append(
$('<th>').append(
$('<div />', { class: 'btn-group' }).append(
$('<button>', {
'class': 'btn btn-default t3js-languagePacks-addLanguage-toggle',
'type': 'button'
}).append(
$('<span>').append(activateIcon),
' Add language',
),
$('<button>', { 'class': 'btn btn-default disabled update-all t3js-languagePacks-update', 'type': 'button', 'disabled': 'disabled' }).append(
$('<span>').append(updateIcon),
' Update all',
),
globalActions
),
),
$('<th>').text('Locale'),
Expand Down
169 changes: 94 additions & 75 deletions typo3/sysext/install/Classes/Controller/MaintenanceController.php
Expand Up @@ -583,7 +583,9 @@ public function languagePacksGetDataAction(ServerRequestInterface $request): Res
{
$view = $this->initializeView($request);
$formProtection = $this->formProtectionFactory->createFromRequest($request);
$isWritable = $this->configurationManager->canWriteConfiguration();
$view->assignMultiple([
'isWritable' => $isWritable,
'languagePacksActivateLanguageToken' => $formProtection->generateToken('installTool', 'languagePacksActivateLanguage'),
'languagePacksDeactivateLanguageToken' => $formProtection->generateToken('installTool', 'languagePacksDeactivateLanguage'),
'languagePacksUpdatePackToken' => $formProtection->generateToken('installTool', 'languagePacksUpdatePack'),
Expand Down Expand Up @@ -617,40 +619,48 @@ public function languagePacksActivateLanguageAction(ServerRequestInterface $requ
$availableLanguages = $languagePackService->getAvailableLanguages();
$activeLanguages = $languagePackService->getActiveLanguages();
$iso = $request->getParsedBody()['install']['iso'];
$activateArray = [];
foreach ($availableLanguages as $availableIso => $name) {
if ($availableIso === $iso && !in_array($availableIso, $activeLanguages, true)) {
$activateArray[] = $iso;
$dependencies = $this->locales->getLocaleDependencies($availableIso);
if (!empty($dependencies)) {
foreach ($dependencies as $dependency) {
if (!in_array($dependency, $activeLanguages, true)) {
$activateArray[] = $dependency;
if (!$this->configurationManager->canWriteConfiguration()) {
$messageQueue->enqueue(new FlashMessage(
sprintf('The language %s was not activated as the configuration file is not writable.', $availableLanguages[$iso]),
'Language not activated',
ContextualFeedbackSeverity::ERROR
));
} else {
$activateArray = [];
foreach ($availableLanguages as $availableIso => $name) {
if ($availableIso === $iso && !in_array($availableIso, $activeLanguages, true)) {
$activateArray[] = $iso;
$dependencies = $this->locales->getLocaleDependencies($availableIso);
if (!empty($dependencies)) {
foreach ($dependencies as $dependency) {
if (!in_array($dependency, $activeLanguages, true)) {
$activateArray[] = $dependency;
}
}
}
}
}
}
if (!empty($activateArray)) {
$activeLanguages = array_merge($activeLanguages, $activateArray);
sort($activeLanguages);
$this->configurationManager->setLocalConfigurationValueByPath(
'EXTCONF/lang',
['availableLanguages' => $activeLanguages]
);
$activationArray = [];
foreach ($activateArray as $activateIso) {
$activationArray[] = $availableLanguages[$activateIso] . ' (' . $activateIso . ')';
if (!empty($activateArray)) {
$activeLanguages = array_merge($activeLanguages, $activateArray);
sort($activeLanguages);
$this->configurationManager->setLocalConfigurationValueByPath(
'EXTCONF/lang',
['availableLanguages' => $activeLanguages]
);
$activationArray = [];
foreach ($activateArray as $activateIso) {
$activationArray[] = $availableLanguages[$activateIso] . ' (' . $activateIso . ')';
}
$messageQueue->enqueue(
new FlashMessage(
'These languages have been activated: ' . implode(', ', $activationArray)
)
);
} else {
$messageQueue->enqueue(
new FlashMessage('Language with ISO code "' . $iso . '" not found or already active.', '', ContextualFeedbackSeverity::ERROR)
);
}
$messageQueue->enqueue(
new FlashMessage(
'These languages have been activated: ' . implode(', ', $activationArray)
)
);
} else {
$messageQueue->enqueue(
new FlashMessage('Language with ISO code "' . $iso . '" not found or already active.', '', ContextualFeedbackSeverity::ERROR)
);
}
return new JsonResponse([
'success' => true,
Expand All @@ -670,61 +680,70 @@ public function languagePacksDeactivateLanguageAction(ServerRequestInterface $re
$availableLanguages = $languagePackService->getAvailableLanguages();
$activeLanguages = $languagePackService->getActiveLanguages();
$iso = $request->getParsedBody()['install']['iso'];
if (empty($iso)) {
throw new \RuntimeException('No iso code given', 1520109807);
}
$otherActiveLanguageDependencies = [];
foreach ($activeLanguages as $activeLanguage) {
if ($activeLanguage === $iso) {
continue;
}
$dependencies = $this->locales->getLocaleDependencies($activeLanguage);
if (in_array($iso, $dependencies, true)) {
$otherActiveLanguageDependencies[] = $activeLanguage;

if (!$this->configurationManager->canWriteConfiguration()) {
$messageQueue->enqueue(new FlashMessage(
sprintf('The language %s was not deactivated as the configuration file is not writable.', $availableLanguages[$iso]),
'Language not deactivated',
ContextualFeedbackSeverity::ERROR
));
} else {
if (empty($iso)) {
throw new \RuntimeException('No iso code given', 1520109807);
}
}
if (!empty($otherActiveLanguageDependencies)) {
// Error: Must disable dependencies first
$dependentArray = [];
foreach ($otherActiveLanguageDependencies as $dependency) {
$dependentArray[] = $availableLanguages[$dependency] . ' (' . $dependency . ')';
$otherActiveLanguageDependencies = [];
foreach ($activeLanguages as $activeLanguage) {
if ($activeLanguage === $iso) {
continue;
}
$dependencies = $this->locales->getLocaleDependencies($activeLanguage);
if (in_array($iso, $dependencies, true)) {
$otherActiveLanguageDependencies[] = $activeLanguage;
}
}
$messageQueue->enqueue(
new FlashMessage(
'Language "' . $availableLanguages[$iso] . ' (' . $iso . ')" can not be deactivated. These'
. ' other languages depend on it and need to be deactivated before:'
. implode(', ', $dependentArray),
'',
ContextualFeedbackSeverity::ERROR
)
);
} else {
if (in_array($iso, $activeLanguages, true)) {
// Deactivate this language
$newActiveLanguages = [];
foreach ($activeLanguages as $activeLanguage) {
if ($activeLanguage === $iso) {
continue;
}
$newActiveLanguages[] = $activeLanguage;
if (!empty($otherActiveLanguageDependencies)) {
// Error: Must disable dependencies first
$dependentArray = [];
foreach ($otherActiveLanguageDependencies as $dependency) {
$dependentArray[] = $availableLanguages[$dependency] . ' (' . $dependency . ')';
}
$this->configurationManager->setLocalConfigurationValueByPath(
'EXTCONF/lang',
['availableLanguages' => $newActiveLanguages]
);
$messageQueue->enqueue(
new FlashMessage(
'Language "' . $availableLanguages[$iso] . ' (' . $iso . ')" has been deactivated'
)
);
} else {
$messageQueue->enqueue(
new FlashMessage(
'Language "' . $availableLanguages[$iso] . ' (' . $iso . ')" has not been deactivated',
'Language "' . $availableLanguages[$iso] . ' (' . $iso . ')" can not be deactivated. These'
. ' other languages depend on it and need to be deactivated before:'
. implode(', ', $dependentArray),
'',
ContextualFeedbackSeverity::ERROR
)
);
} else {
if (in_array($iso, $activeLanguages, true)) {
// Deactivate this language
$newActiveLanguages = [];
foreach ($activeLanguages as $activeLanguage) {
if ($activeLanguage === $iso) {
continue;
}
$newActiveLanguages[] = $activeLanguage;
}
$this->configurationManager->setLocalConfigurationValueByPath(
'EXTCONF/lang',
['availableLanguages' => $newActiveLanguages]
);
$messageQueue->enqueue(
new FlashMessage(
'Language "' . $availableLanguages[$iso] . ' (' . $iso . ')" has been deactivated'
)
);
} else {
$messageQueue->enqueue(
new FlashMessage(
'Language "' . $availableLanguages[$iso] . ' (' . $iso . ')" has not been deactivated',
'',
ContextualFeedbackSeverity::ERROR
)
);
}
}
}
return new JsonResponse([
Expand Down
Expand Up @@ -12,11 +12,15 @@
data-language-packs-deactivate-language-token="{languagePacksDeactivateLanguageToken}"
data-language-packs-update-pack-token="{languagePacksUpdatePackToken}"
data-language-packs-update-iso-times-token="{languagePacksUpdateIsoTimesToken}"
data-configuration-is-writable="{f:if(condition: isWritable, then: 'true', else: 'false')}"
>
<p>
Gives an overview of available languages excluding the default (English) language.
It allows you to activate and deactivate additional localizations and to update language packs from the official TYPO3 translation server.
</p>

<f:render partial="Generic/ConfigurationNotWritable" arguments="{_all}"/>

<div class="t3js-languagePacks-notifications"></div>
<div class="t3js-languagePacks-output"></div>
<div class="t3js-languagePacks-mainContent"></div>
Expand Down
Expand Up @@ -2,7 +2,7 @@

<h1>Settings</h1>

<f:render partial="Settings/ConfigurationNotWritable" arguments="{_all}"/>
<f:render partial="Generic/ConfigurationNotWritable" arguments="{_all}"/>

<div class="card-container">
<div class="card card-size-fixed-small">
Expand Down
Expand Up @@ -5,7 +5,7 @@
and will overwrite the previously specified password.
</p>

<f:render partial="Settings/ConfigurationNotWritable" arguments="{_all}"/>
<f:render partial="Generic/ConfigurationNotWritable" arguments="{_all}"/>

<div class="t3js-module-content" data-install-tool-token="{changeInstallToolPasswordToken}">
<form action="" id="t3js-changeInstallToolPassword-form" method="post" spellcheck="false">
Expand Down
@@ -1,7 +1,7 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">

<div class="t3js-module-content" data-extension-configuration-write-token="{extensionConfigurationWriteToken}">
<f:render partial="Settings/ConfigurationNotWritable" arguments="{_all}"/>
<f:render partial="Generic/ConfigurationNotWritable" arguments="{_all}"/>

<div class="t3js-extensionConfiguration-output"></div>

Expand Down
Expand Up @@ -6,7 +6,7 @@
can still use the old behaviour.
</p>

<f:render partial="Settings/ConfigurationNotWritable" arguments="{_all}"/>
<f:render partial="Generic/ConfigurationNotWritable" arguments="{_all}"/>

<p>
<strong>Available features:</strong>
Expand Down
Expand Up @@ -6,7 +6,7 @@
The TYPO3 Core does not change the content of this file.
</p>

<f:render partial="Settings/ConfigurationNotWritable" arguments="{_all}"/>
<f:render partial="Generic/ConfigurationNotWritable" arguments="{_all}"/>

<div class="t3js-module-content" data-local-configuration-write-token="{localConfigurationWriteToken}">
<div class="form-group">
Expand Down
Expand Up @@ -10,7 +10,7 @@
Changed values are written to system/settings.php. The optional file system/additional.php is not controlled by the TYPO3’s core and may override certain settings. Administrators must maintain system/additional.php manually and should be used with caution.
</p>

<f:render partial="Settings/ConfigurationNotWritable" arguments="{_all}"/>
<f:render partial="Generic/ConfigurationNotWritable" arguments="{_all}"/>

<div class="t3js-module-content" data-presets-activate-token="{presetsActivateToken}" data-presets-content-token="{presetsGetContentToken}">
<form method="post">
Expand Down
Expand Up @@ -5,7 +5,7 @@
backend administrative users have access to this module when the system is running in development mode.
</p>

<f:render partial="Settings/ConfigurationNotWritable" arguments="{_all}"/>
<f:render partial="Generic/ConfigurationNotWritable" arguments="{_all}"/>

<f:if condition="{systemMaintainerIsDevelopmentContext}">
<div class="typo3-message alert alert-info">
Expand Down

0 comments on commit c3ebac6

Please sign in to comment.