From e34d152c3f9a1a88d3319eb30ba59caa77476f09 Mon Sep 17 00:00:00 2001
From: Andreas Fernandez <a.fernandez@scripting-base.de>
Date: Fri, 31 Jan 2020 18:00:15 +0100
Subject: [PATCH] [BUGFIX] Rework AJAX request queueing

This patch reworks the queueing system for AJAX requests sent by the
extension scanner. This implementation now really only sends the allowed
amount of concurrent requests and queues the remaining requests.

When the extension scanner gets closed, the queue gets flushed and the
remaining running requests get aborted. Since aborting a request triggers
a DOMException, the rather useless "Oops" notification gets removed.

The extension scanner now also queues requests where it actually makes
sense.

Resolves: #90279
Releases: master, 9.5
Change-Id: Ia5a64a2074fd99b8447063e2135126b7cf46e393
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/63107
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Richard Haeser <richard@maxserv.com>
Tested-by: Georg Ringer <georg.ringer@gmail.com>
Reviewed-by: Richard Haeser <richard@maxserv.com>
Reviewed-by: Georg Ringer <georg.ringer@gmail.com>
---
 .../Public/TypeScript/Ajax/AjaxQueue.ts       |  44 +--
 .../Module/Upgrade/ExtensionScanner.ts        | 265 +++++++++---------
 .../Public/JavaScript/Ajax/AjaxQueue.js       |   2 +-
 .../Module/Upgrade/ExtensionScanner.js        |   2 +-
 4 files changed, 158 insertions(+), 155 deletions(-)

diff --git a/Build/Sources/TypeScript/install/Resources/Public/TypeScript/Ajax/AjaxQueue.ts b/Build/Sources/TypeScript/install/Resources/Public/TypeScript/Ajax/AjaxQueue.ts
index 8a31afa7ca11..92e2fbd9ec12 100644
--- a/Build/Sources/TypeScript/install/Resources/Public/TypeScript/Ajax/AjaxQueue.ts
+++ b/Build/Sources/TypeScript/install/Resources/Public/TypeScript/Ajax/AjaxQueue.ts
@@ -26,33 +26,35 @@ interface Payload {
  * Module: TYPO3/CMS/Install/Module/AjaxQueue
  */
 class AjaxQueue {
+  private requests: Array<AjaxRequest> = [];
   private requestCount: number = 0;
-  private threshold: number = 10;
+  private threshold: number = 5;
   private queue: Array<Payload> = [];
 
-  public async add(payload: Payload): Promise<any> {
-    const oldFinally = payload.finally;
-    if (this.queue.length > 0 && this.requestCount <= this.threshold) {
-      this.sendRequest(this.queue.shift()).finally((): void => {
-        this.decrementRequestCount();
-      });
-    } else {
-      this.decrementRequestCount();
-    }
+  public add(payload: Payload): void {
+    this.queue.push(payload);
+    this.handleNext();
+  }
 
-    if (oldFinally) {
-      oldFinally(...arguments);
-    }
+  public flush(): void {
+    this.queue = [];
+    this.requests.map((request: AjaxRequest): void => {
+      request.abort();
+    });
+    this.requests = [];
+  }
 
-    if (this.requestCount >= this.threshold) {
-      this.queue.push(payload);
-    } else {
+  private handleNext(): void {
+    if (this.queue.length > 0 && this.requestCount < this.threshold) {
       this.incrementRequestCount();
-      this.sendRequest(payload);
+      this.sendRequest(this.queue.shift()).finally((): void => {
+        this.decrementRequestCount();
+        this.handleNext();
+      });
     }
   }
 
-  private async sendRequest(payload: Payload): Promise<any> {
+  private async sendRequest(payload: Payload): Promise<void> {
     const request = new AjaxRequest(payload.url);
     let response: any;
     if (typeof payload.method !== 'undefined' && payload.method.toUpperCase() === 'POST') {
@@ -61,7 +63,11 @@ class AjaxQueue {
       response = request.withQueryArguments(payload.data || {}).get();
     }
 
-    return response.then(payload.onfulfilled, payload.onrejected);
+    this.requests.push(request);
+    return response.then(payload.onfulfilled, payload.onrejected).then((): void => {
+      const idx = this.requests.indexOf(request);
+      delete this.requests[idx];
+    });
   }
 
   private incrementRequestCount(): void {
diff --git a/Build/Sources/TypeScript/install/Resources/Public/TypeScript/Module/Upgrade/ExtensionScanner.ts b/Build/Sources/TypeScript/install/Resources/Public/TypeScript/Module/Upgrade/ExtensionScanner.ts
index a9480170e352..afce0653dfae 100644
--- a/Build/Sources/TypeScript/install/Resources/Public/TypeScript/Module/Upgrade/ExtensionScanner.ts
+++ b/Build/Sources/TypeScript/install/Resources/Public/TypeScript/Module/Upgrade/ExtensionScanner.ts
@@ -13,6 +13,7 @@
 
 import 'bootstrap';
 import * as $ from 'jquery';
+import AjaxRequest = require('TYPO3/CMS/Core/Ajax/AjaxRequest');
 import {AjaxResponse} from 'TYPO3/CMS/Core/Ajax/AjaxResponse';
 import {ResponseError} from 'TYPO3/CMS/Core/Ajax/ResponseError';
 import {AbstractInteractableModule} from '../AbstractInteractableModule';
@@ -67,6 +68,8 @@ class ExtensionScanner extends AbstractInteractableModule {
         this.scanSingleExtension(extension);
         $me.data('scanned', true);
       }
+    }).on('hide.bs.modal', (): void => {
+      AjaxQueue.flush();
     }).on('click', this.selectorScanSingleTrigger, (e: JQueryEventObject): void => {
       // Scan a single extension by clicking "Rescan"
       e.preventDefault();
@@ -83,9 +86,8 @@ class ExtensionScanner extends AbstractInteractableModule {
 
   private getData(): void {
     const modalContent = this.getModalBody();
-    AjaxQueue.add({
-      url: Router.getUrl('extensionScannerGetData'),
-      onfulfilled: async (response: AjaxResponse): Promise<any> => {
+    (new AjaxRequest(Router.getUrl('extensionScannerGetData'))).get().then(
+      async (response: AjaxResponse): Promise<any> => {
         const data = await response.resolve();
         if (data.success === true) {
           modalContent.empty().append(data.html);
@@ -94,10 +96,10 @@ class ExtensionScanner extends AbstractInteractableModule {
           Notification.error('Something went wrong');
         }
       },
-      onrejected: (error: ResponseError): void => {
+      (error: ResponseError): void => {
         Router.handleAjaxError(error, modalContent);
-      },
-    });
+      }
+    );
   }
 
   private getExtensionSelector(extension: string): string {
@@ -157,26 +159,24 @@ class ExtensionScanner extends AbstractInteractableModule {
     if (numberOfScannedExtensions === numberOfExtensions) {
       this.findInModal(this.selectorExtensionScanButton).removeClass('disabled').prop('disabled', false);
       Notification.success('Scan finished', 'All extensions have been scanned');
-      AjaxQueue.add({
-        url: Router.getUrl(),
-        method: 'POST',
-        data: {
-          install: {
-            action: 'extensionScannerMarkFullyScannedRestFiles',
-            token: this.getModuleContent().data('extension-scanner-mark-fully-scanned-rest-files-token'),
-            hashes: this.uniqueArray(this.listOfAffectedRestFileHashes),
-          },
+
+      (new AjaxRequest(Router.getUrl())).post({
+        install: {
+          action: 'extensionScannerMarkFullyScannedRestFiles',
+          token: this.getModuleContent().data('extension-scanner-mark-fully-scanned-rest-files-token'),
+          hashes: this.uniqueArray(this.listOfAffectedRestFileHashes),
         },
-        onfulfilled: async (response: AjaxResponse): Promise<any> => {
+      }).then(
+        async (response: AjaxResponse): Promise<any> => {
           const data = await response.resolve();
           if (data.success === true) {
             Notification.success('Marked not affected files', 'Marked ' + data.markedAsNotAffected + ' ReST files as not affected.');
           }
         },
-        onrejected: (error: ResponseError): void => {
+        (error: ResponseError): void => {
           Router.handleAjaxError(error, modalContent);
-        },
-      });
+        }
+      );
     }
   }
 
@@ -206,139 +206,136 @@ class ExtensionScanner extends AbstractInteractableModule {
     $extensionContainer.find('.t3js-extensionScanner-extension-body-ignored-files').empty().text('0');
     $extensionContainer.find('.t3js-extensionScanner-extension-body-ignored-lines').empty().text('0');
     this.setProgressForAll();
-    AjaxQueue.add({
-      url: Router.getUrl(),
-      method: 'POST',
-      data: {
-        install: {
-          action: 'extensionScannerFiles',
-          token: executeToken,
-          extension: extension,
-        },
+    (new AjaxRequest(Router.getUrl())).post({
+      install: {
+        action: 'extensionScannerFiles',
+        token: executeToken,
+        extension: extension,
       },
-      onfulfilled: async (response: AjaxResponse): Promise<any> => {
+    }).then(
+      async (response: AjaxResponse): Promise<any> => {
         const data = await response.resolve();
         if (data.success === true && Array.isArray(data.files)) {
           const numberOfFiles = data.files.length;
-          if (numberOfFiles > 0) {
-            this.setStatusMessageForScan(extension, 0, numberOfFiles);
-            $extensionContainer.find('.t3js-extensionScanner-extension-body').text('');
-            let doneFiles = 0;
-            data.files.forEach((file: string): void => {
-              AjaxQueue.add({
-                method: 'POST',
-                data: {
-                  install: {
-                    action: 'extensionScannerScanFile',
-                    token: this.getModuleContent().data('extension-scanner-scan-file-token'),
-                    extension: extension,
-                    file: file,
-                  },
+          if (numberOfFiles <= 0) {
+            Notification.warning('No files found', 'The extension EXT:' + extension + ' contains no files we can scan');
+            return;
+          }
+
+          this.setStatusMessageForScan(extension, 0, numberOfFiles);
+          $extensionContainer.find('.t3js-extensionScanner-extension-body').text('');
+          let doneFiles = 0;
+          data.files.forEach((file: string): void => {
+            AjaxQueue.add({
+              method: 'POST',
+              data: {
+                install: {
+                  action: 'extensionScannerScanFile',
+                  token: this.getModuleContent().data('extension-scanner-scan-file-token'),
+                  extension: extension,
+                  file: file,
                 },
-                url: Router.getUrl(),
-                onfulfilled: async (response: AjaxResponse): Promise<any> => {
-                  const fileData: FileData = await response.resolve();
-                  doneFiles++;
-                  this.setStatusMessageForScan(extension, doneFiles, numberOfFiles);
-                  this.setProgressForScan(extension, doneFiles, numberOfFiles);
-                  if (fileData.success && $.isArray(fileData.matches)) {
-                    fileData.matches.forEach((match: Match): void => {
-                      hitFound = true;
-                      const aMatch: any = modalContent.find(hitTemplate).clone();
-                      aMatch.find('.t3js-extensionScanner-hit-file-panel-head').attr('href', '#collapse' + match.uniqueId);
-                      aMatch.find('.t3js-extensionScanner-hit-file-panel-body').attr('id', 'collapse' + match.uniqueId);
-                      aMatch.find('.t3js-extensionScanner-hit-filename').text(file);
-                      aMatch.find('.t3js-extensionScanner-hit-message').text(match.message);
-                      if (match.indicator === 'strong') {
-                        aMatch.find('.t3js-extensionScanner-hit-file-panel-head .badges')
-                          .append('<span class="badge" title="Reliable match, false positive unlikely">strong</span>');
-                      } else {
-                        aMatch.find('.t3js-extensionScanner-hit-file-panel-head .badges')
-                          .append('<span class="badge" title="Probable match, but can be a false positive">weak</span>');
-                      }
-                      if (match.silenced === true) {
-                        aMatch.find('.t3js-extensionScanner-hit-file-panel-head .badges')
-                          .append('<span class="badge" title="Match has been annotated by extension author' +
-                            ' as false positive match">silenced</span>');
-                      }
-                      aMatch.find('.t3js-extensionScanner-hit-file-lineContent').empty().text(match.lineContent);
-                      aMatch.find('.t3js-extensionScanner-hit-file-line').empty().text(match.line + ': ');
-                      if ($.isArray(match.restFiles)) {
-                        match.restFiles.forEach((restFile: RestFile): void => {
-                          const aRest = modalContent.find(restTemplate).clone();
-                          aRest.find('.t3js-extensionScanner-hit-rest-panel-head').attr('href', '#collapse' + restFile.uniqueId);
-                          aRest.find('.t3js-extensionScanner-hit-rest-panel-head .badge').empty().text(restFile.version);
-                          aRest.find('.t3js-extensionScanner-hit-rest-panel-body').attr('id', 'collapse' + restFile.uniqueId);
-                          aRest.find('.t3js-extensionScanner-hit-rest-headline').text(restFile.headline);
-                          aRest.find('.t3js-extensionScanner-hit-rest-body').text(restFile.content);
-                          aRest.addClass('panel-' + restFile.class);
-                          aMatch.find('.t3js-extensionScanner-hit-file-rest-container').append(aRest);
-                          this.listOfAffectedRestFileHashes.push(restFile.file_hash);
-                        });
-                      }
-                      const panelClass =
-                        aMatch.find('.panel-breaking', '.t3js-extensionScanner-hit-file-rest-container').length > 0
-                          ? 'panel-danger'
-                          : 'panel-warning';
-                      aMatch.addClass(panelClass);
-                      $extensionContainer.find('.t3js-extensionScanner-extension-body').removeClass('hide').append(aMatch);
-                      if (panelClass === 'panel-danger') {
-                        $extensionContainer.removeClass('panel-warning').addClass(panelClass);
-                      }
-                      if (panelClass === 'panel-warning' && !$extensionContainer.hasClass('panel-danger')) {
-                        $extensionContainer.addClass(panelClass);
-                      }
-                    });
-                  }
-                  if (fileData.success) {
-                    const currentLinesOfCode = parseInt($extensionContainer.find('.t3js-extensionScanner-extension-body-loc').text(), 10);
-                    $extensionContainer.find('.t3js-extensionScanner-extension-body-loc').empty()
-                      .text(currentLinesOfCode + fileData.effectiveCodeLines);
-                    if (fileData.isFileIgnored) {
-                      const currentIgnoredFiles = parseInt(
-                        $extensionContainer.find('.t3js-extensionScanner-extension-body-ignored-files').text(),
-                        10,
-                      );
-                      $extensionContainer.find('.t3js-extensionScanner-extension-body-ignored-files').empty().text(currentIgnoredFiles + 1);
+              },
+              url: Router.getUrl(),
+              onfulfilled: async (response: AjaxResponse): Promise<void> => {
+                const fileData: FileData = await response.resolve();
+                doneFiles++;
+                this.setStatusMessageForScan(extension, doneFiles, numberOfFiles);
+                this.setProgressForScan(extension, doneFiles, numberOfFiles);
+                if (fileData.success && $.isArray(fileData.matches)) {
+                  fileData.matches.forEach((match: Match): void => {
+                    hitFound = true;
+                    const aMatch: any = modalContent.find(hitTemplate).clone();
+                    aMatch.find('.t3js-extensionScanner-hit-file-panel-head').attr('href', '#collapse' + match.uniqueId);
+                    aMatch.find('.t3js-extensionScanner-hit-file-panel-body').attr('id', 'collapse' + match.uniqueId);
+                    aMatch.find('.t3js-extensionScanner-hit-filename').text(file);
+                    aMatch.find('.t3js-extensionScanner-hit-message').text(match.message);
+                    if (match.indicator === 'strong') {
+                      aMatch.find('.t3js-extensionScanner-hit-file-panel-head .badges')
+                        .append('<span class="badge" title="Reliable match, false positive unlikely">strong</span>');
+                    } else {
+                      aMatch.find('.t3js-extensionScanner-hit-file-panel-head .badges')
+                        .append('<span class="badge" title="Probable match, but can be a false positive">weak</span>');
                     }
-                    const currentIgnoredLines = parseInt(
-                      $extensionContainer.find('.t3js-extensionScanner-extension-body-ignored-lines').text(),
+                    if (match.silenced === true) {
+                      aMatch.find('.t3js-extensionScanner-hit-file-panel-head .badges')
+                        .append('<span class="badge" title="Match has been annotated by extension author' +
+                          ' as false positive match">silenced</span>');
+                    }
+                    aMatch.find('.t3js-extensionScanner-hit-file-lineContent').empty().text(match.lineContent);
+                    aMatch.find('.t3js-extensionScanner-hit-file-line').empty().text(match.line + ': ');
+                    if ($.isArray(match.restFiles)) {
+                      match.restFiles.forEach((restFile: RestFile): void => {
+                        const aRest = modalContent.find(restTemplate).clone();
+                        aRest.find('.t3js-extensionScanner-hit-rest-panel-head').attr('href', '#collapse' + restFile.uniqueId);
+                        aRest.find('.t3js-extensionScanner-hit-rest-panel-head .badge').empty().text(restFile.version);
+                        aRest.find('.t3js-extensionScanner-hit-rest-panel-body').attr('id', 'collapse' + restFile.uniqueId);
+                        aRest.find('.t3js-extensionScanner-hit-rest-headline').text(restFile.headline);
+                        aRest.find('.t3js-extensionScanner-hit-rest-body').text(restFile.content);
+                        aRest.addClass('panel-' + restFile.class);
+                        aMatch.find('.t3js-extensionScanner-hit-file-rest-container').append(aRest);
+                        this.listOfAffectedRestFileHashes.push(restFile.file_hash);
+                      });
+                    }
+                    const panelClass =
+                      aMatch.find('.panel-breaking', '.t3js-extensionScanner-hit-file-rest-container').length > 0
+                        ? 'panel-danger'
+                        : 'panel-warning';
+                    aMatch.addClass(panelClass);
+                    $extensionContainer.find('.t3js-extensionScanner-extension-body').removeClass('hide').append(aMatch);
+                    if (panelClass === 'panel-danger') {
+                      $extensionContainer.removeClass('panel-warning').addClass(panelClass);
+                    }
+                    if (panelClass === 'panel-warning' && !$extensionContainer.hasClass('panel-danger')) {
+                      $extensionContainer.addClass(panelClass);
+                    }
+                  });
+                }
+                if (fileData.success) {
+                  const currentLinesOfCode = parseInt($extensionContainer.find('.t3js-extensionScanner-extension-body-loc').text(), 10);
+                  $extensionContainer.find('.t3js-extensionScanner-extension-body-loc').empty()
+                    .text(currentLinesOfCode + fileData.effectiveCodeLines);
+                  if (fileData.isFileIgnored) {
+                    const currentIgnoredFiles = parseInt(
+                      $extensionContainer.find('.t3js-extensionScanner-extension-body-ignored-files').text(),
                       10,
                     );
-                    $extensionContainer.find('.t3js-extensionScanner-extension-body-ignored-lines').empty()
-                      .text(currentIgnoredLines + fileData.ignoredLines);
+                    $extensionContainer.find('.t3js-extensionScanner-extension-body-ignored-files').empty().text(currentIgnoredFiles + 1);
                   }
-                  if (doneFiles === numberOfFiles) {
-                    if (!hitFound) {
-                      $extensionContainer.addClass('panel-success');
-                    }
-                    $extensionContainer.addClass('t3js-extensionscan-finished');
-                    this.setProgressForAll();
-                    $extensionContainer.find('.t3js-extensionScanner-scan-single').text('Rescan').attr('disabled', null);
+                  const currentIgnoredLines = parseInt(
+                    $extensionContainer.find('.t3js-extensionScanner-extension-body-ignored-lines').text(),
+                    10,
+                  );
+                  $extensionContainer.find('.t3js-extensionScanner-extension-body-ignored-lines').empty()
+                    .text(currentIgnoredLines + fileData.ignoredLines);
+                }
+                if (doneFiles === numberOfFiles) {
+                  if (!hitFound) {
+                    $extensionContainer.addClass('panel-success');
                   }
-                },
-                onrejected: (reason: string): void => {
-                  doneFiles = doneFiles + 1;
-                  this.setStatusMessageForScan(extension, doneFiles, numberOfFiles);
-                  this.setProgressForScan(extension, doneFiles, numberOfFiles);
+                  $extensionContainer.addClass('t3js-extensionscan-finished');
                   this.setProgressForAll();
-                  Notification.error('Oops, an error occurred', 'Please look at the console output for details');
-                  console.error(reason);
-                },
-              });
+                  $extensionContainer.find('.t3js-extensionScanner-scan-single').text('Rescan').attr('disabled', null);
+                }
+              },
+              onrejected: (reason: string): void => {
+                doneFiles = doneFiles + 1;
+                this.setStatusMessageForScan(extension, doneFiles, numberOfFiles);
+                this.setProgressForScan(extension, doneFiles, numberOfFiles);
+                this.setProgressForAll();
+                console.error(reason);
+              },
             });
-          } else {
-            Notification.warning('No files found', 'The extension EXT:' + extension + ' contains no files we can scan');
-          }
+          });
         } else {
           Notification.error('Oops, an error occurred', 'Please look at the console output for details');
           console.error(data);
         }
       },
-      onrejected: (error: ResponseError): void => {
+      (error: ResponseError): void => {
         Router.handleAjaxError(error, modalContent);
-      },
-    });
+      }
+    );
   }
 }
 
diff --git a/typo3/sysext/install/Resources/Public/JavaScript/Ajax/AjaxQueue.js b/typo3/sysext/install/Resources/Public/JavaScript/Ajax/AjaxQueue.js
index 23c88a94f770..68abd1451c50 100644
--- a/typo3/sysext/install/Resources/Public/JavaScript/Ajax/AjaxQueue.js
+++ b/typo3/sysext/install/Resources/Public/JavaScript/Ajax/AjaxQueue.js
@@ -10,4 +10,4 @@
  *
  * The TYPO3 project - inspiring people to share!
  */
-define(["require","exports","TYPO3/CMS/Core/Ajax/AjaxRequest"],(function(e,t,s){"use strict";return new class{constructor(){this.requestCount=0,this.threshold=10,this.queue=[]}async add(e){const t=e.finally;this.queue.length>0&&this.requestCount<=this.threshold?this.sendRequest(this.queue.shift()).finally(()=>{this.decrementRequestCount()}):this.decrementRequestCount(),t&&t(...arguments),this.requestCount>=this.threshold?this.queue.push(e):(this.incrementRequestCount(),this.sendRequest(e))}async sendRequest(e){const t=new s(e.url);let u;return(u=void 0!==e.method&&"POST"===e.method.toUpperCase()?t.post(e.data):t.withQueryArguments(e.data||{}).get()).then(e.onfulfilled,e.onrejected)}incrementRequestCount(){this.requestCount++}decrementRequestCount(){this.requestCount>0&&this.requestCount--}}}));
\ No newline at end of file
+define(["require","exports","TYPO3/CMS/Core/Ajax/AjaxRequest"],(function(e,t,s){"use strict";return new class{constructor(){this.requests=[],this.requestCount=0,this.threshold=5,this.queue=[]}add(e){this.queue.push(e),this.handleNext()}flush(){this.queue=[],this.requests.map(e=>{e.abort()}),this.requests=[]}handleNext(){this.queue.length>0&&this.requestCount<this.threshold&&(this.incrementRequestCount(),this.sendRequest(this.queue.shift()).finally(()=>{this.decrementRequestCount(),this.handleNext()}))}async sendRequest(e){const t=new s(e.url);let u;return u=void 0!==e.method&&"POST"===e.method.toUpperCase()?t.post(e.data):t.withQueryArguments(e.data||{}).get(),this.requests.push(t),u.then(e.onfulfilled,e.onrejected).then(()=>{const e=this.requests.indexOf(t);delete this.requests[e]})}incrementRequestCount(){this.requestCount++}decrementRequestCount(){this.requestCount>0&&this.requestCount--}}}));
\ No newline at end of file
diff --git a/typo3/sysext/install/Resources/Public/JavaScript/Module/Upgrade/ExtensionScanner.js b/typo3/sysext/install/Resources/Public/JavaScript/Module/Upgrade/ExtensionScanner.js
index 4a9710c97069..f5e66deae2d1 100644
--- a/typo3/sysext/install/Resources/Public/JavaScript/Module/Upgrade/ExtensionScanner.js
+++ b/typo3/sysext/install/Resources/Public/JavaScript/Module/Upgrade/ExtensionScanner.js
@@ -10,4 +10,4 @@
  *
  * The TYPO3 project - inspiring people to share!
  */
-define(["require","exports","jquery","../AbstractInteractableModule","TYPO3/CMS/Backend/Modal","TYPO3/CMS/Backend/Notification","../../Ajax/AjaxQueue","../../Router","bootstrap"],(function(e,n,t,s,i,a,o,r){"use strict";class l extends s.AbstractInteractableModule{constructor(){super(...arguments),this.listOfAffectedRestFileHashes=[],this.selectorExtensionContainer=".t3js-extensionScanner-extension",this.selectorNumberOfFiles=".t3js-extensionScanner-number-of-files",this.selectorScanSingleTrigger=".t3js-extensionScanner-scan-single",this.selectorExtensionScanButton=".t3js-extensionScanner-scan-all"}initialize(e){this.currentModal=e,this.getData(),e.on("show.bs.collapse",this.selectorExtensionContainer,e=>{const n=t(e.currentTarget);if(void 0===n.data("scanned")){const e=n.data("extension");this.scanSingleExtension(e),n.data("scanned",!0)}}).on("click",this.selectorScanSingleTrigger,e=>{e.preventDefault();const n=t(e.currentTarget).closest(this.selectorExtensionContainer).data("extension");this.scanSingleExtension(n)}).on("click",this.selectorExtensionScanButton,n=>{n.preventDefault(),t(n.currentTarget).addClass("disabled").prop("disabled",!0);const s=e.find(this.selectorExtensionContainer);this.scanAll(s)})}getData(){const e=this.getModalBody();o.add({url:r.getUrl("extensionScannerGetData"),onfulfilled:async n=>{const t=await n.resolve();!0===t.success?(e.empty().append(t.html),i.setButtons(t.buttons)):a.error("Something went wrong")},onrejected:n=>{r.handleAjaxError(n,e)}})}getExtensionSelector(e){return this.selectorExtensionContainer+"-"+e}scanAll(e){this.findInModal(this.selectorExtensionContainer).removeClass("panel-danger panel-warning panel-success").find(".panel-progress-bar").css("width",0).attr("aria-valuenow",0).find("span").text("0%"),this.setProgressForAll(),e.each((e,n)=>{const s=t(n),i=s.data("extension");this.scanSingleExtension(i),s.data("scanned",!0)})}setStatusMessageForScan(e,n,t){this.findInModal(this.getExtensionSelector(e)).find(this.selectorNumberOfFiles).text("Checked "+n+" of "+t+" files")}setProgressForScan(e,n,t){const s=n/t*100;this.findInModal(this.getExtensionSelector(e)).find(".panel-progress-bar").css("width",s+"%").attr("aria-valuenow",s).find("span").text(s+"%")}setProgressForAll(){const e=this.findInModal(this.selectorExtensionContainer).length,n=this.findInModal(this.selectorExtensionContainer+".t3js-extensionscan-finished.panel-success").length+this.findInModal(this.selectorExtensionContainer+".t3js-extensionscan-finished.panel-warning").length+this.findInModal(this.selectorExtensionContainer+".t3js-extensionscan-finished.panel-danger").length,t=n/e*100,s=this.getModalBody();this.findInModal(".t3js-extensionScanner-progress-all-extension .progress-bar").css("width",t+"%").attr("aria-valuenow",t).find("span").text(n+" of "+e+" scanned"),n===e&&(this.findInModal(this.selectorExtensionScanButton).removeClass("disabled").prop("disabled",!1),a.success("Scan finished","All extensions have been scanned"),o.add({url:r.getUrl(),method:"POST",data:{install:{action:"extensionScannerMarkFullyScannedRestFiles",token:this.getModuleContent().data("extension-scanner-mark-fully-scanned-rest-files-token"),hashes:this.uniqueArray(this.listOfAffectedRestFileHashes)}},onfulfilled:async e=>{const n=await e.resolve();!0===n.success&&a.success("Marked not affected files","Marked "+n.markedAsNotAffected+" ReST files as not affected.")},onrejected:e=>{r.handleAjaxError(e,s)}}))}uniqueArray(e){return e.filter((e,n,t)=>t.indexOf(e)===n)}scanSingleExtension(e){const n=this.getModuleContent().data("extension-scanner-files-token"),s=this.getModalBody(),i=this.findInModal(this.getExtensionSelector(e));let l=!1;i.removeClass("panel-danger panel-warning panel-success t3js-extensionscan-finished"),i.data("hasRun","true"),i.find(".t3js-extensionScanner-scan-single").text("Scanning...").attr("disabled","disabled"),i.find(".t3js-extensionScanner-extension-body-loc").empty().text("0"),i.find(".t3js-extensionScanner-extension-body-ignored-files").empty().text("0"),i.find(".t3js-extensionScanner-extension-body-ignored-lines").empty().text("0"),this.setProgressForAll(),o.add({url:r.getUrl(),method:"POST",data:{install:{action:"extensionScannerFiles",token:n,extension:e}},onfulfilled:async n=>{const d=await n.resolve();if(!0===d.success&&Array.isArray(d.files)){const n=d.files.length;if(n>0){this.setStatusMessageForScan(e,0,n),i.find(".t3js-extensionScanner-extension-body").text("");let c=0;d.files.forEach(d=>{o.add({method:"POST",data:{install:{action:"extensionScannerScanFile",token:this.getModuleContent().data("extension-scanner-scan-file-token"),extension:e,file:d}},url:r.getUrl(),onfulfilled:async a=>{const o=await a.resolve();if(c++,this.setStatusMessageForScan(e,c,n),this.setProgressForScan(e,c,n),o.success&&t.isArray(o.matches)&&o.matches.forEach(e=>{l=!0;const n=s.find("#t3js-extensionScanner-file-hit-template").clone();n.find(".t3js-extensionScanner-hit-file-panel-head").attr("href","#collapse"+e.uniqueId),n.find(".t3js-extensionScanner-hit-file-panel-body").attr("id","collapse"+e.uniqueId),n.find(".t3js-extensionScanner-hit-filename").text(d),n.find(".t3js-extensionScanner-hit-message").text(e.message),"strong"===e.indicator?n.find(".t3js-extensionScanner-hit-file-panel-head .badges").append('<span class="badge" title="Reliable match, false positive unlikely">strong</span>'):n.find(".t3js-extensionScanner-hit-file-panel-head .badges").append('<span class="badge" title="Probable match, but can be a false positive">weak</span>'),!0===e.silenced&&n.find(".t3js-extensionScanner-hit-file-panel-head .badges").append('<span class="badge" title="Match has been annotated by extension author as false positive match">silenced</span>'),n.find(".t3js-extensionScanner-hit-file-lineContent").empty().text(e.lineContent),n.find(".t3js-extensionScanner-hit-file-line").empty().text(e.line+": "),t.isArray(e.restFiles)&&e.restFiles.forEach(e=>{const t=s.find("#t3js-extensionScanner-file-hit-rest-template").clone();t.find(".t3js-extensionScanner-hit-rest-panel-head").attr("href","#collapse"+e.uniqueId),t.find(".t3js-extensionScanner-hit-rest-panel-head .badge").empty().text(e.version),t.find(".t3js-extensionScanner-hit-rest-panel-body").attr("id","collapse"+e.uniqueId),t.find(".t3js-extensionScanner-hit-rest-headline").text(e.headline),t.find(".t3js-extensionScanner-hit-rest-body").text(e.content),t.addClass("panel-"+e.class),n.find(".t3js-extensionScanner-hit-file-rest-container").append(t),this.listOfAffectedRestFileHashes.push(e.file_hash)});const a=n.find(".panel-breaking",".t3js-extensionScanner-hit-file-rest-container").length>0?"panel-danger":"panel-warning";n.addClass(a),i.find(".t3js-extensionScanner-extension-body").removeClass("hide").append(n),"panel-danger"===a&&i.removeClass("panel-warning").addClass(a),"panel-warning"!==a||i.hasClass("panel-danger")||i.addClass(a)}),o.success){const e=parseInt(i.find(".t3js-extensionScanner-extension-body-loc").text(),10);if(i.find(".t3js-extensionScanner-extension-body-loc").empty().text(e+o.effectiveCodeLines),o.isFileIgnored){const e=parseInt(i.find(".t3js-extensionScanner-extension-body-ignored-files").text(),10);i.find(".t3js-extensionScanner-extension-body-ignored-files").empty().text(e+1)}const n=parseInt(i.find(".t3js-extensionScanner-extension-body-ignored-lines").text(),10);i.find(".t3js-extensionScanner-extension-body-ignored-lines").empty().text(n+o.ignoredLines)}c===n&&(l||i.addClass("panel-success"),i.addClass("t3js-extensionscan-finished"),this.setProgressForAll(),i.find(".t3js-extensionScanner-scan-single").text("Rescan").attr("disabled",null))},onrejected:t=>{c+=1,this.setStatusMessageForScan(e,c,n),this.setProgressForScan(e,c,n),this.setProgressForAll(),a.error("Oops, an error occurred","Please look at the console output for details"),console.error(t)}})})}else a.warning("No files found","The extension EXT:"+e+" contains no files we can scan")}else a.error("Oops, an error occurred","Please look at the console output for details"),console.error(d)},onrejected:e=>{r.handleAjaxError(e,s)}})}}return new l}));
\ No newline at end of file
+define(["require","exports","jquery","TYPO3/CMS/Core/Ajax/AjaxRequest","../AbstractInteractableModule","TYPO3/CMS/Backend/Modal","TYPO3/CMS/Backend/Notification","../../Ajax/AjaxQueue","../../Router","bootstrap"],(function(e,n,t,s,i,a,o,r,l){"use strict";class c extends i.AbstractInteractableModule{constructor(){super(...arguments),this.listOfAffectedRestFileHashes=[],this.selectorExtensionContainer=".t3js-extensionScanner-extension",this.selectorNumberOfFiles=".t3js-extensionScanner-number-of-files",this.selectorScanSingleTrigger=".t3js-extensionScanner-scan-single",this.selectorExtensionScanButton=".t3js-extensionScanner-scan-all"}initialize(e){this.currentModal=e,this.getData(),e.on("show.bs.collapse",this.selectorExtensionContainer,e=>{const n=t(e.currentTarget);if(void 0===n.data("scanned")){const e=n.data("extension");this.scanSingleExtension(e),n.data("scanned",!0)}}).on("hide.bs.modal",()=>{r.flush()}).on("click",this.selectorScanSingleTrigger,e=>{e.preventDefault();const n=t(e.currentTarget).closest(this.selectorExtensionContainer).data("extension");this.scanSingleExtension(n)}).on("click",this.selectorExtensionScanButton,n=>{n.preventDefault(),t(n.currentTarget).addClass("disabled").prop("disabled",!0);const s=e.find(this.selectorExtensionContainer);this.scanAll(s)})}getData(){const e=this.getModalBody();new s(l.getUrl("extensionScannerGetData")).get().then(async n=>{const t=await n.resolve();!0===t.success?(e.empty().append(t.html),a.setButtons(t.buttons)):o.error("Something went wrong")},n=>{l.handleAjaxError(n,e)})}getExtensionSelector(e){return this.selectorExtensionContainer+"-"+e}scanAll(e){this.findInModal(this.selectorExtensionContainer).removeClass("panel-danger panel-warning panel-success").find(".panel-progress-bar").css("width",0).attr("aria-valuenow",0).find("span").text("0%"),this.setProgressForAll(),e.each((e,n)=>{const s=t(n),i=s.data("extension");this.scanSingleExtension(i),s.data("scanned",!0)})}setStatusMessageForScan(e,n,t){this.findInModal(this.getExtensionSelector(e)).find(this.selectorNumberOfFiles).text("Checked "+n+" of "+t+" files")}setProgressForScan(e,n,t){const s=n/t*100;this.findInModal(this.getExtensionSelector(e)).find(".panel-progress-bar").css("width",s+"%").attr("aria-valuenow",s).find("span").text(s+"%")}setProgressForAll(){const e=this.findInModal(this.selectorExtensionContainer).length,n=this.findInModal(this.selectorExtensionContainer+".t3js-extensionscan-finished.panel-success").length+this.findInModal(this.selectorExtensionContainer+".t3js-extensionscan-finished.panel-warning").length+this.findInModal(this.selectorExtensionContainer+".t3js-extensionscan-finished.panel-danger").length,t=n/e*100,i=this.getModalBody();this.findInModal(".t3js-extensionScanner-progress-all-extension .progress-bar").css("width",t+"%").attr("aria-valuenow",t).find("span").text(n+" of "+e+" scanned"),n===e&&(this.findInModal(this.selectorExtensionScanButton).removeClass("disabled").prop("disabled",!1),o.success("Scan finished","All extensions have been scanned"),new s(l.getUrl()).post({install:{action:"extensionScannerMarkFullyScannedRestFiles",token:this.getModuleContent().data("extension-scanner-mark-fully-scanned-rest-files-token"),hashes:this.uniqueArray(this.listOfAffectedRestFileHashes)}}).then(async e=>{const n=await e.resolve();!0===n.success&&o.success("Marked not affected files","Marked "+n.markedAsNotAffected+" ReST files as not affected.")},e=>{l.handleAjaxError(e,i)}))}uniqueArray(e){return e.filter((e,n,t)=>t.indexOf(e)===n)}scanSingleExtension(e){const n=this.getModuleContent().data("extension-scanner-files-token"),i=this.getModalBody(),a=this.findInModal(this.getExtensionSelector(e));let c=!1;a.removeClass("panel-danger panel-warning panel-success t3js-extensionscan-finished"),a.data("hasRun","true"),a.find(".t3js-extensionScanner-scan-single").text("Scanning...").attr("disabled","disabled"),a.find(".t3js-extensionScanner-extension-body-loc").empty().text("0"),a.find(".t3js-extensionScanner-extension-body-ignored-files").empty().text("0"),a.find(".t3js-extensionScanner-extension-body-ignored-lines").empty().text("0"),this.setProgressForAll(),new s(l.getUrl()).post({install:{action:"extensionScannerFiles",token:n,extension:e}}).then(async n=>{const s=await n.resolve();if(!0===s.success&&Array.isArray(s.files)){const n=s.files.length;if(n<=0)return void o.warning("No files found","The extension EXT:"+e+" contains no files we can scan");this.setStatusMessageForScan(e,0,n),a.find(".t3js-extensionScanner-extension-body").text("");let d=0;s.files.forEach(s=>{r.add({method:"POST",data:{install:{action:"extensionScannerScanFile",token:this.getModuleContent().data("extension-scanner-scan-file-token"),extension:e,file:s}},url:l.getUrl(),onfulfilled:async o=>{const r=await o.resolve();if(d++,this.setStatusMessageForScan(e,d,n),this.setProgressForScan(e,d,n),r.success&&t.isArray(r.matches)&&r.matches.forEach(e=>{c=!0;const n=i.find("#t3js-extensionScanner-file-hit-template").clone();n.find(".t3js-extensionScanner-hit-file-panel-head").attr("href","#collapse"+e.uniqueId),n.find(".t3js-extensionScanner-hit-file-panel-body").attr("id","collapse"+e.uniqueId),n.find(".t3js-extensionScanner-hit-filename").text(s),n.find(".t3js-extensionScanner-hit-message").text(e.message),"strong"===e.indicator?n.find(".t3js-extensionScanner-hit-file-panel-head .badges").append('<span class="badge" title="Reliable match, false positive unlikely">strong</span>'):n.find(".t3js-extensionScanner-hit-file-panel-head .badges").append('<span class="badge" title="Probable match, but can be a false positive">weak</span>'),!0===e.silenced&&n.find(".t3js-extensionScanner-hit-file-panel-head .badges").append('<span class="badge" title="Match has been annotated by extension author as false positive match">silenced</span>'),n.find(".t3js-extensionScanner-hit-file-lineContent").empty().text(e.lineContent),n.find(".t3js-extensionScanner-hit-file-line").empty().text(e.line+": "),t.isArray(e.restFiles)&&e.restFiles.forEach(e=>{const t=i.find("#t3js-extensionScanner-file-hit-rest-template").clone();t.find(".t3js-extensionScanner-hit-rest-panel-head").attr("href","#collapse"+e.uniqueId),t.find(".t3js-extensionScanner-hit-rest-panel-head .badge").empty().text(e.version),t.find(".t3js-extensionScanner-hit-rest-panel-body").attr("id","collapse"+e.uniqueId),t.find(".t3js-extensionScanner-hit-rest-headline").text(e.headline),t.find(".t3js-extensionScanner-hit-rest-body").text(e.content),t.addClass("panel-"+e.class),n.find(".t3js-extensionScanner-hit-file-rest-container").append(t),this.listOfAffectedRestFileHashes.push(e.file_hash)});const o=n.find(".panel-breaking",".t3js-extensionScanner-hit-file-rest-container").length>0?"panel-danger":"panel-warning";n.addClass(o),a.find(".t3js-extensionScanner-extension-body").removeClass("hide").append(n),"panel-danger"===o&&a.removeClass("panel-warning").addClass(o),"panel-warning"!==o||a.hasClass("panel-danger")||a.addClass(o)}),r.success){const e=parseInt(a.find(".t3js-extensionScanner-extension-body-loc").text(),10);if(a.find(".t3js-extensionScanner-extension-body-loc").empty().text(e+r.effectiveCodeLines),r.isFileIgnored){const e=parseInt(a.find(".t3js-extensionScanner-extension-body-ignored-files").text(),10);a.find(".t3js-extensionScanner-extension-body-ignored-files").empty().text(e+1)}const n=parseInt(a.find(".t3js-extensionScanner-extension-body-ignored-lines").text(),10);a.find(".t3js-extensionScanner-extension-body-ignored-lines").empty().text(n+r.ignoredLines)}d===n&&(c||a.addClass("panel-success"),a.addClass("t3js-extensionscan-finished"),this.setProgressForAll(),a.find(".t3js-extensionScanner-scan-single").text("Rescan").attr("disabled",null))},onrejected:t=>{d+=1,this.setStatusMessageForScan(e,d,n),this.setProgressForScan(e,d,n),this.setProgressForAll(),console.error(t)}})})}else o.error("Oops, an error occurred","Please look at the console output for details"),console.error(s)},e=>{l.handleAjaxError(e,i)})}}return new c}));
\ No newline at end of file
-- 
GitLab