Commit d126080b authored by Andreas Fernandez's avatar Andreas Fernandez Committed by Susanne Moog
Browse files

[BUGFIX] Fix various misbehaviors in "Broken Extension Scanner"

The "Broken Extension Scanner" has some flaws that are fixed with this
patch:

- Only one request is sent to scan all ext_localconf.php /
  ext_tables.php files, each
- ext_tables.php is only scanned if ext_localconf.php was successful,
  since those are dependent
- Protected extensions (mandatory to the system) cannot get uninstalled
- After uninstalling an extension all caches are cleared

Resolves: #89947
Releases: master, 9.5
Change-Id: I63aa7e67df9d061fded42af34c72727db629258a
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/62639


Tested-by: default avatarTYPO3com <noreply@typo3.com>
Tested-by: Anja Leichsenring's avatarAnja Leichsenring <aleichsenring@ab-softlab.de>
Tested-by: Susanne Moog's avatarSusanne Moog <look@susi.dev>
Reviewed-by: Anja Leichsenring's avatarAnja Leichsenring <aleichsenring@ab-softlab.de>
Reviewed-by: Susanne Moog's avatarSusanne Moog <look@susi.dev>
parent ec192807
......@@ -11,15 +11,20 @@
* The TYPO3 project - inspiring people to share!
*/
import {AbstractInteractableModule} from '../AbstractInteractableModule';
import * as $ from 'jquery';
import 'bootstrap';
import Router = require('../../Router');
import ProgressBar = require('../../Renderable/ProgressBar');
import InfoBox = require('../../Renderable/InfoBox');
import Severity = require('../../Renderable/Severity');
import * as $ from 'jquery';
import {AbstractInteractableModule} from '../AbstractInteractableModule';
import Modal = require('TYPO3/CMS/Backend/Modal');
import Notification = require('TYPO3/CMS/Backend/Notification');
import InfoBox = require('../../Renderable/InfoBox');
import ProgressBar = require('../../Renderable/ProgressBar');
import Severity = require('../../Renderable/Severity');
import Router = require('../../Router');
interface BrokenExtension {
name: string;
isProtected: boolean;
}
/**
* Module: TYPO3/CMS/Install/Module/ExtensionCompatTester
......@@ -47,7 +52,6 @@ class ExtensionCompatTester extends AbstractInteractableModule {
this.findInModal(this.selectorCheckTrigger).addClass('disabled').prop('disabled', true);
this.findInModal('.modal-loading').hide();
const modalContent = this.getModalBody();
const modalFooter = this.getModalFooter();
const $outputContainer = this.findInModal(this.selectorOutputContainer);
const message = ProgressBar.render(Severity.loading, 'Loading...', '');
$outputContainer.append(message);
......@@ -62,43 +66,27 @@ class ExtensionCompatTester extends AbstractInteractableModule {
const progressBar = ProgressBar.render(Severity.loading, 'Loading...', '');
$innerOutputContainer.append(progressBar);
if (data.success === true && Array.isArray(data.extensions)) {
const loadExtLocalconf = (): void => {
const promises: Array<any> = [];
data.extensions.forEach((extension: any): void => {
promises.push(this.loadExtLocalconf(extension));
});
return $.when.apply($, promises).done((): void => {
const aMessage = InfoBox.render(Severity.ok, 'ext_localconf.php of all loaded extensions successfully loaded', '');
$innerOutputContainer.append(aMessage);
});
};
const loadExtTables = (): void => {
const promises: Array<any> = [];
data.extensions.forEach((extension: any): void => {
promises.push(this.loadExtTables(extension));
});
return $.when.apply($, promises).done((): void => {
const aMessage = InfoBox.render(Severity.ok, 'ext_tables.php of all loaded extensions successfully loaded', '');
$innerOutputContainer.append(aMessage);
});
};
$.when(loadExtLocalconf(), loadExtTables()).fail((response: any): void => {
const aMessage = InfoBox.render(
Severity.error,
'Loading ' + response.scope + ' of extension "' + response.extension + '" failed',
if (data.success === true) {
this.loadExtLocalconf().done((): void => {
$innerOutputContainer.append(
InfoBox.render(Severity.ok, 'ext_localconf.php of all loaded extensions successfully loaded', ''),
);
$innerOutputContainer.append(aMessage);
modalFooter.find(this.selectorUninstallTrigger)
.text('Unload extension "' + response.extension + '"')
.attr('data-extension', response.extension)
.removeClass('hidden');
}).always((): void => {
$innerOutputContainer.find('.alert-loading').remove();
this.findInModal(this.selectorCheckTrigger).removeClass('disabled').prop('disabled', false);
});
this.loadExtTables().done((): void => {
$innerOutputContainer.append(
InfoBox.render(Severity.ok, 'ext_tables.php of all loaded extensions successfully loaded', ''),
);
}).fail((xhr: JQueryXHR): void => {
this.renderFailureMessages('ext_tables.php', xhr.responseJSON.brokenExtensions, $innerOutputContainer);
}).always((): void => {
this.unlockModal();
})
}).fail((xhr: JQueryXHR): void => {
this.renderFailureMessages('ext_localconf.php', xhr.responseJSON.brokenExtensions, $innerOutputContainer);
$innerOutputContainer.append(
InfoBox.render(Severity.notice, 'Skipped scanning ext_tables.php files due to previous errors', ''),
);
this.unlockModal();
})
} else {
Notification.error('Something went wrong');
}
......@@ -109,9 +97,35 @@ class ExtensionCompatTester extends AbstractInteractableModule {
});
}
private loadExtLocalconf(extension: string): JQueryPromise<{}> {
private unlockModal(): void {
this.findInModal(this.selectorOutputContainer).find('.alert-loading').remove();
this.findInModal(this.selectorCheckTrigger).removeClass('disabled').prop('disabled', false);
}
private renderFailureMessages(scope: string, brokenExtensions: Array<BrokenExtension>, $outputContainer: JQuery): void {
for (let extension of brokenExtensions) {
let uninstallAction;
if (!extension.isProtected) {
uninstallAction = $('<button />', {'class': 'btn btn-danger t3js-extensionCompatTester-uninstall'})
.attr('data-extension', extension.name)
.text('Uninstall extension "' + extension.name + '"');
}
$outputContainer.append(
InfoBox.render(
Severity.error,
'Loading ' + scope + ' of extension "' + extension.name + '" failed',
(extension.isProtected ? 'Extension is mandatory and cannot be uninstalled.' : ''),
),
uninstallAction,
);
}
this.unlockModal();
}
private loadExtLocalconf(): JQueryPromise<JQueryXHR> {
const executeToken = this.getModuleContent().data('extension-compat-tester-load-ext_localconf-token');
const $ajax = $.ajax({
return $.ajax({
url: Router.getUrl(),
method: 'POST',
cache: false,
......@@ -119,22 +133,14 @@ class ExtensionCompatTester extends AbstractInteractableModule {
'install': {
'action': 'extensionCompatTesterLoadExtLocalconf',
'token': executeToken,
'extension': extension,
},
},
});
return $ajax.promise().then(null, (): any => {
throw {
scope: 'ext_localconf.php',
extension: extension,
};
});
}
private loadExtTables(extension: string): JQueryPromise<{}> {
private loadExtTables(): JQueryPromise<JQueryXHR> {
const executeToken = this.getModuleContent().data('extension-compat-tester-load-ext_tables-token');
const $ajax = $.ajax({
return $.ajax({
url: Router.getUrl(),
method: 'POST',
cache: false,
......@@ -142,17 +148,9 @@ class ExtensionCompatTester extends AbstractInteractableModule {
'install': {
'action': 'extensionCompatTesterLoadExtTables',
'token': executeToken,
'extension': extension,
},
},
});
return $ajax.promise().then(null, (): any => {
throw {
scope: 'ext_tables.php',
extension: extension,
};
});
}
/**
......
......@@ -30,9 +30,10 @@ use TYPO3\CMS\Core\Http\JsonResponse;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
use TYPO3\CMS\Core\Migrations\TcaMigration;
use TYPO3\CMS\Core\Package\Package;
use TYPO3\CMS\Core\Package\PackageInterface;
use TYPO3\CMS\Core\Package\PackageManager;
use TYPO3\CMS\Core\Registry;
use TYPO3\CMS\Core\Service\OpcodeCacheService;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Install\ExtensionScanner\Php\CodeStatistics;
......@@ -57,6 +58,7 @@ use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\PropertyExistsStaticMatcher;
use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\PropertyProtectedMatcher;
use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\PropertyPublicMatcher;
use TYPO3\CMS\Install\ExtensionScanner\Php\MatcherFactory;
use TYPO3\CMS\Install\Service\ClearCacheService;
use TYPO3\CMS\Install\Service\CoreUpdateService;
use TYPO3\CMS\Install\Service\CoreVersionService;
use TYPO3\CMS\Install\Service\LateBootService;
......@@ -407,17 +409,12 @@ class UpgradeController extends AbstractController
return new JsonResponse([
'success' => true,
'extensions' => array_keys($this->packageManager->getActivePackages()),
'html' => $view->render(),
'buttons' => [
[
'btnClass' => 'btn-default disabled t3js-extensionCompatTester-check',
'text' => 'Check extensions',
],
[
'btnClass' => 'btn-default hidden t3js-extensionCompatTester-uninstall',
'text' => 'Uninstall extension',
],
],
]);
}
......@@ -430,22 +427,26 @@ class UpgradeController extends AbstractController
*/
public function extensionCompatTesterLoadExtLocalconfAction(ServerRequestInterface $request): ResponseInterface
{
$brokenExtensions = [];
$container = $this->lateBootService->getContainer();
$backup = $this->lateBootService->makeCurrent($container);
$extension = $request->getParsedBody()['install']['extension'];
foreach ($this->packageManager->getActivePackages() as $package) {
$this->extensionCompatTesterLoadExtLocalconfForExtension($package);
if ($package->getPackageKey() === $extension) {
break;
try {
$this->extensionCompatTesterLoadExtLocalconfForExtension($package);
} catch (\Throwable $e) {
$brokenExtensions[] = [
'name' => $package->getPackageKey(),
'isProtected' => $package->isProtected()
];
}
}
$this->lateBootService->makeCurrent(null, $backup);
return new JsonResponse([
'success' => true,
]);
'brokenExtensions' => $brokenExtensions,
], empty($brokenExtensions) ? 200 : 500);
}
/**
......@@ -456,27 +457,31 @@ class UpgradeController extends AbstractController
*/
public function extensionCompatTesterLoadExtTablesAction(ServerRequestInterface $request): ResponseInterface
{
$brokenExtensions = [];
$container = $this->lateBootService->getContainer();
$backup = $this->lateBootService->makeCurrent($container);
$extension = $request->getParsedBody()['install']['extension'];
$activePackages = $this->packageManager->getActivePackages();
foreach ($activePackages as $package) {
// Load all ext_localconf files first
$this->extensionCompatTesterLoadExtLocalconfForExtension($package);
}
foreach ($activePackages as $package) {
$this->extensionCompatTesterLoadExtTablesForExtension($package);
if ($package->getPackageKey() === $extension) {
break;
try {
$this->extensionCompatTesterLoadExtTablesForExtension($package);
} catch (\Throwable $e) {
$brokenExtensions[] = [
'name' => $package->getPackageKey(),
'isProtected' => $package->isProtected()
];
}
}
$this->lateBootService->makeCurrent(null, $backup);
return new JsonResponse([
'success' => true,
]);
'brokenExtensions' => $brokenExtensions,
], empty($brokenExtensions) ? 200 : 500);
}
/**
......@@ -499,6 +504,9 @@ class UpgradeController extends AbstractController
if (ExtensionManagementUtility::isLoaded($extension)) {
try {
ExtensionManagementUtility::unloadExtension($extension);
GeneralUtility::makeInstance(ClearCacheService::class)->clearAll();
GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive();
$messageQueue->enqueue(new FlashMessage(
'Extension "' . $extension . '" unloaded.',
'',
......@@ -1172,9 +1180,9 @@ class UpgradeController extends AbstractController
* Loads ext_localconf.php for a single extension. Method is a modified copy of
* the original bootstrap method.
*
* @param Package $package
* @param PackageInterface $package
*/
protected function extensionCompatTesterLoadExtLocalconfForExtension(Package $package)
protected function extensionCompatTesterLoadExtLocalconfForExtension(PackageInterface $package)
{
$extLocalconfPath = $package->getPackagePath() . 'ext_localconf.php';
if (@file_exists($extLocalconfPath)) {
......@@ -1186,9 +1194,9 @@ class UpgradeController extends AbstractController
* Loads ext_tables.php for a single extension. Method is a modified copy of
* the original bootstrap method.
*
* @param Package $package
* @param PackageInterface $package
*/
protected function extensionCompatTesterLoadExtTablesForExtension(Package $package)
protected function extensionCompatTesterLoadExtTablesForExtension(PackageInterface $package)
{
$extTablesPath = $package->getPackagePath() . 'ext_tables.php';
if (@file_exists($extTablesPath)) {
......
......@@ -10,4 +10,4 @@
*
* The TYPO3 project - inspiring people to share!
*/
define(["require","exports","../AbstractInteractableModule","jquery","../../Router","../../Renderable/ProgressBar","../../Renderable/InfoBox","../../Renderable/Severity","TYPO3/CMS/Backend/Modal","TYPO3/CMS/Backend/Notification","bootstrap"],(function(e,t,n,s,o,a,i,r,l,d){"use strict";class c extends n.AbstractInteractableModule{constructor(){super(...arguments),this.selectorCheckTrigger=".t3js-extensionCompatTester-check",this.selectorUninstallTrigger=".t3js-extensionCompatTester-uninstall",this.selectorOutputContainer=".t3js-extensionCompatTester-output"}initialize(e){this.currentModal=e,this.getLoadedExtensionList(),e.on("click",this.selectorCheckTrigger,()=>{this.findInModal(this.selectorUninstallTrigger).addClass("hidden"),this.findInModal(this.selectorOutputContainer).empty(),this.getLoadedExtensionList()}),e.on("click",this.selectorUninstallTrigger,e=>{this.uninstallExtension(s(e.target).data("extension"))})}getLoadedExtensionList(){this.findInModal(this.selectorCheckTrigger).addClass("disabled").prop("disabled",!0),this.findInModal(".modal-loading").hide();const e=this.getModalBody(),t=this.getModalFooter(),n=this.findInModal(this.selectorOutputContainer),c=a.render(r.loading,"Loading...","");n.append(c),s.ajax({url:o.getUrl("extensionCompatTesterLoadedExtensionList"),cache:!1,success:n=>{e.empty().append(n.html),l.setButtons(n.buttons);const o=this.findInModal(this.selectorOutputContainer),c=a.render(r.loading,"Loading...","");if(o.append(c),!0===n.success&&Array.isArray(n.extensions)){const e=()=>{const e=[];return n.extensions.forEach(t=>{e.push(this.loadExtLocalconf(t))}),s.when.apply(s,e).done(()=>{const e=i.render(r.ok,"ext_localconf.php of all loaded extensions successfully loaded","");o.append(e)})},a=()=>{const e=[];return n.extensions.forEach(t=>{e.push(this.loadExtTables(t))}),s.when.apply(s,e).done(()=>{const e=i.render(r.ok,"ext_tables.php of all loaded extensions successfully loaded","");o.append(e)})};s.when(e(),a()).fail(e=>{const n=i.render(r.error,"Loading "+e.scope+' of extension "'+e.extension+'" failed');o.append(n),t.find(this.selectorUninstallTrigger).text('Unload extension "'+e.extension+'"').attr("data-extension",e.extension).removeClass("hidden")}).always(()=>{o.find(".alert-loading").remove(),this.findInModal(this.selectorCheckTrigger).removeClass("disabled").prop("disabled",!1)})}else d.error("Something went wrong")},error:t=>{o.handleAjaxError(t,e)}})}loadExtLocalconf(e){const t=this.getModuleContent().data("extension-compat-tester-load-ext_localconf-token");return s.ajax({url:o.getUrl(),method:"POST",cache:!1,data:{install:{action:"extensionCompatTesterLoadExtLocalconf",token:t,extension:e}}}).promise().then(null,()=>{throw{scope:"ext_localconf.php",extension:e}})}loadExtTables(e){const t=this.getModuleContent().data("extension-compat-tester-load-ext_tables-token");return s.ajax({url:o.getUrl(),method:"POST",cache:!1,data:{install:{action:"extensionCompatTesterLoadExtTables",token:t,extension:e}}}).promise().then(null,()=>{throw{scope:"ext_tables.php",extension:e}})}uninstallExtension(e){const t=this.getModuleContent().data("extension-compat-tester-uninstall-extension-token"),n=this.getModalBody(),l=s(this.selectorOutputContainer),c=a.render(r.loading,"Loading...","");l.append(c),s.ajax({url:o.getUrl(),cache:!1,method:"POST",data:{install:{action:"extensionCompatTesterUninstallExtension",token:t,extension:e}},success:e=>{e.success?(Array.isArray(e.status)&&e.status.forEach(e=>{const t=i.render(e.severity,e.title,e.message);n.find(this.selectorOutputContainer).empty().append(t)}),this.findInModal(this.selectorUninstallTrigger).addClass("hidden"),this.getLoadedExtensionList()):d.error("Something went wrong")},error:e=>{o.handleAjaxError(e,n)}})}}return new c}));
\ No newline at end of file
define(["require","exports","jquery","../AbstractInteractableModule","TYPO3/CMS/Backend/Modal","TYPO3/CMS/Backend/Notification","../../Renderable/InfoBox","../../Renderable/ProgressBar","../../Renderable/Severity","../../Router","bootstrap"],(function(e,t,n,s,o,a,i,r,l,d){"use strict";class c extends s.AbstractInteractableModule{constructor(){super(...arguments),this.selectorCheckTrigger=".t3js-extensionCompatTester-check",this.selectorUninstallTrigger=".t3js-extensionCompatTester-uninstall",this.selectorOutputContainer=".t3js-extensionCompatTester-output"}initialize(e){this.currentModal=e,this.getLoadedExtensionList(),e.on("click",this.selectorCheckTrigger,()=>{this.findInModal(this.selectorUninstallTrigger).addClass("hidden"),this.findInModal(this.selectorOutputContainer).empty(),this.getLoadedExtensionList()}),e.on("click",this.selectorUninstallTrigger,e=>{this.uninstallExtension(n(e.target).data("extension"))})}getLoadedExtensionList(){this.findInModal(this.selectorCheckTrigger).addClass("disabled").prop("disabled",!0),this.findInModal(".modal-loading").hide();const e=this.getModalBody(),t=this.findInModal(this.selectorOutputContainer),s=r.render(l.loading,"Loading...","");t.append(s),n.ajax({url:d.getUrl("extensionCompatTesterLoadedExtensionList"),cache:!1,success:t=>{e.empty().append(t.html),o.setButtons(t.buttons);const n=this.findInModal(this.selectorOutputContainer),s=r.render(l.loading,"Loading...","");n.append(s),!0===t.success?this.loadExtLocalconf().done(()=>{n.append(i.render(l.ok,"ext_localconf.php of all loaded extensions successfully loaded","")),this.loadExtTables().done(()=>{n.append(i.render(l.ok,"ext_tables.php of all loaded extensions successfully loaded",""))}).fail(e=>{this.renderFailureMessages("ext_tables.php",e.responseJSON.brokenExtensions,n)}).always(()=>{this.unlockModal()})}).fail(e=>{this.renderFailureMessages("ext_localconf.php",e.responseJSON.brokenExtensions,n),n.append(i.render(l.notice,"Skipped scanning ext_tables.php files due to previous errors","")),this.unlockModal()}):a.error("Something went wrong")},error:t=>{d.handleAjaxError(t,e)}})}unlockModal(){this.findInModal(this.selectorOutputContainer).find(".alert-loading").remove(),this.findInModal(this.selectorCheckTrigger).removeClass("disabled").prop("disabled",!1)}renderFailureMessages(e,t,s){for(let o of t){let t;o.isProtected||(t=n("<button />",{class:"btn btn-danger t3js-extensionCompatTester-uninstall"}).attr("data-extension",o.name).text('Uninstall extension "'+o.name+'"')),s.append(i.render(l.error,"Loading "+e+' of extension "'+o.name+'" failed',o.isProtected?"Extension is mandatory and cannot be uninstalled.":""),t)}this.unlockModal()}loadExtLocalconf(){const e=this.getModuleContent().data("extension-compat-tester-load-ext_localconf-token");return n.ajax({url:d.getUrl(),method:"POST",cache:!1,data:{install:{action:"extensionCompatTesterLoadExtLocalconf",token:e}}})}loadExtTables(){const e=this.getModuleContent().data("extension-compat-tester-load-ext_tables-token");return n.ajax({url:d.getUrl(),method:"POST",cache:!1,data:{install:{action:"extensionCompatTesterLoadExtTables",token:e}}})}uninstallExtension(e){const t=this.getModuleContent().data("extension-compat-tester-uninstall-extension-token"),s=this.getModalBody(),o=n(this.selectorOutputContainer),c=r.render(l.loading,"Loading...","");o.append(c),n.ajax({url:d.getUrl(),cache:!1,method:"POST",data:{install:{action:"extensionCompatTesterUninstallExtension",token:t,extension:e}},success:e=>{e.success?(Array.isArray(e.status)&&e.status.forEach(e=>{const t=i.render(e.severity,e.title,e.message);s.find(this.selectorOutputContainer).empty().append(t)}),this.findInModal(this.selectorUninstallTrigger).addClass("hidden"),this.getLoadedExtensionList()):a.error("Something went wrong")},error:e=>{d.handleAjaxError(e,s)}})}}return new c}));
\ No newline at end of file
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment