[SECURITY] Avoid disclosing loaded extensions 34/59534/2
authorOliver Hader <oliver@typo3.org>
Tue, 22 Jan 2019 08:42:46 +0000 (09:42 +0100)
committerOliver Hader <oliver.hader@typo3.org>
Tue, 22 Jan 2019 08:42:49 +0000 (09:42 +0100)
Inline JavaScript settings for RequireJS and ajaxUrls disclose the
existence of specific extensions in a TYPO3 installation.

In case no backend user is logged in RequireJS settings are fetched
using an according endpoint, ajaxUrls (for backend AJAX routes) are
limited to those that are accessible without having a user session.

Resolves: #83855
Releases: master, 9.5, 8.7
Security-Commit: a9b60d26597449fec46bd26e0b511bc6e423ef24
Security-Bulletin: TYPO3-CORE-SA-2019-001
Change-Id: Ifa4029340e750baaf216fa953bf41b6d06d3138b
Reviewed-on: https://review.typo3.org/59534
Reviewed-by: Oliver Hader <oliver.hader@typo3.org>
Tested-by: Oliver Hader <oliver.hader@typo3.org>
typo3/sysext/backend/Classes/Middleware/BackendUserAuthenticator.php
typo3/sysext/core/Classes/Controller/RequireJsController.php [new file with mode: 0644]
typo3/sysext/core/Classes/Page/PageRenderer.php
typo3/sysext/core/Configuration/Backend/AjaxRoutes.php [new file with mode: 0644]
typo3/sysext/core/Resources/Public/JavaScript/requirejs-loader.js [new file with mode: 0644]
typo3/sysext/core/ext_localconf.php

index d868567..685e604 100644 (file)
@@ -45,7 +45,8 @@ class BackendUserAuthenticator implements MiddlewareInterface
         '/ajax/logout',
         '/ajax/login/refresh',
         '/ajax/login/timedout',
-        '/ajax/rsa/publickey'
+        '/ajax/rsa/publickey',
+        '/ajax/core/requirejs',
     ];
 
     /**
diff --git a/typo3/sysext/core/Classes/Controller/RequireJsController.php b/typo3/sysext/core/Classes/Controller/RequireJsController.php
new file mode 100644 (file)
index 0000000..23b88cf
--- /dev/null
@@ -0,0 +1,109 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Controller;
+
+/*
+ * 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 Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use TYPO3\CMS\Core\Http\JsonResponse;
+use TYPO3\CMS\Core\Page\PageRenderer;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Handling requirejs client requests.
+ */
+class RequireJsController
+{
+    /**
+     * @var PageRenderer
+     */
+    protected $pageRenderer;
+
+    public function __construct()
+    {
+        $this->pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
+    }
+
+    /**
+     * Retrieves additional requirejs configuration for a given module name or module path.
+     *
+     * The JSON result e.g. could look like:
+     * {
+     *   "shim": {
+     *     "vendor/module": ["exports" => "TheModule"]
+     *   },
+     *   "paths": {
+     *     "vendor/module": "/public/web/path/"
+     *   },
+     *   "packages": {
+     *     [
+     *       "name": "module",
+     *       ...
+     *     ]
+     *   }
+     * }
+     *
+     * Parameter name either could be the module name ("vendor/module") or a
+     * module path ("vendor/module/component") belonging to a module.
+     *
+     * @param ServerRequestInterface $request
+     * @return ResponseInterface
+     */
+    public function retrieveConfiguration(ServerRequestInterface $request): ResponseInterface
+    {
+        $name = $request->getQueryParams()['name'] ?? null;
+        if (empty($name) || !is_string($name)) {
+            return new JsonResponse(null, 404);
+        }
+        $configuration = $this->findConfiguration($name);
+        return new JsonResponse($configuration, !empty($configuration) ? 200 : 404);
+    }
+
+    /**
+     * @param string $name
+     * @return array
+     */
+    protected function findConfiguration(string $name): array
+    {
+        $relevantConfiguration = [];
+        $this->pageRenderer->loadRequireJs();
+        $configuration = $this->pageRenderer->getRequireJsConfig(PageRenderer::REQUIREJS_SCOPE_RESOLVE);
+
+        $shim = $configuration['shim'] ?? [];
+        foreach ($shim as $baseModuleName => $baseModuleConfiguration) {
+            if (strpos($name . '/', $baseModuleName . '/') === 0) {
+                $relevantConfiguration['shim'][$baseModuleName] = $baseModuleConfiguration;
+            }
+        }
+
+        $paths = $configuration['paths'] ?? [];
+        foreach ($paths as $baseModuleName => $baseModulePath) {
+            if (strpos($name . '/', $baseModuleName . '/') === 0) {
+                $relevantConfiguration['paths'][$baseModuleName] = $baseModulePath;
+            }
+        }
+
+        $packages = $configuration['packages'] ?? [];
+        foreach ($packages as $package) {
+            if (!empty($package['name'])
+                && strpos($name . '/', $package['name'] . '/') === 0
+            ) {
+                $relevantConfiguration['packages'][] = $package;
+            }
+        }
+
+        return $relevantConfiguration;
+    }
+}
index decf6bd..1fbd107 100644 (file)
@@ -39,6 +39,9 @@ class PageRenderer implements \TYPO3\CMS\Core\SingletonInterface
     const PART_HEADER = 1;
     const PART_FOOTER = 2;
 
+    const REQUIREJS_SCOPE_CONFIG = 'config';
+    const REQUIREJS_SCOPE_RESOLVE = 'resolve';
+
     /**
      * @var bool
      */
@@ -270,12 +273,24 @@ class PageRenderer implements \TYPO3\CMS\Core\SingletonInterface
     protected $addRequireJs = false;
 
     /**
-     * inline configuration for requireJS
+     * Inline configuration for requireJS (internal)
      * @var array
      */
     protected $requireJsConfig = [];
 
     /**
+     * Module names of internal requireJS 'paths'
+     * @var array
+     */
+    protected $internalRequireJsPathModuleNames = [];
+
+    /**
+     * Inline configuration for requireJS (public)
+     * @var array
+     */
+    protected $publicRequireJsConfig = [];
+
+    /**
      * @var array
      */
     protected $inlineLanguageLabels = [];
@@ -531,6 +546,34 @@ class PageRenderer implements \TYPO3\CMS\Core\SingletonInterface
         $this->requireJsPath = $path;
     }
 
+    /**
+     * @param string $scope
+     * @return array
+     */
+    public function getRequireJsConfig(string $scope = null): array
+    {
+        // return basic RequireJS configuration without shim, paths and packages
+        if ($scope === static::REQUIREJS_SCOPE_CONFIG) {
+            return array_replace_recursive(
+                $this->publicRequireJsConfig,
+                $this->filterArrayKeys(
+                    $this->requireJsConfig,
+                    ['shim', 'paths', 'packages'],
+                    false
+                )
+            );
+        }
+        // return RequireJS configuration for resolving only shim, paths and packages
+        if ($scope === static::REQUIREJS_SCOPE_RESOLVE) {
+            return $this->filterArrayKeys(
+                $this->requireJsConfig,
+                ['shim', 'paths', 'packages'],
+                true
+            );
+        }
+        return [];
+    }
+
     /*****************************************************/
     /*                                                   */
     /*  Public Enablers / Disablers                      */
@@ -1251,7 +1294,7 @@ class PageRenderer implements \TYPO3\CMS\Core\SingletonInterface
     public function loadRequireJs()
     {
         $this->addRequireJs = true;
-        if (!empty($this->requireJsConfig)) {
+        if (!empty($this->requireJsConfig) && !empty($this->publicRequireJsConfig)) {
             return;
         }
 
@@ -1260,13 +1303,17 @@ class PageRenderer implements \TYPO3\CMS\Core\SingletonInterface
         $cacheIdentifier = 'requireJS_' . md5(implode(',', $loadedExtensions) . ($isDevelopment ? ':dev' : '') . GeneralUtility::getIndpEnv('TYPO3_REQUEST_SCRIPT'));
         /** @var FrontendInterface $cache */
         $cache = static::$cache ?? GeneralUtility::makeInstance(CacheManager::class)->getCache('assets');
-        $this->requireJsConfig = $cache->get($cacheIdentifier);
+        $requireJsConfig = $cache->get($cacheIdentifier);
 
         // if we did not get a configuration from the cache, compute and store it in the cache
-        if (empty($this->requireJsConfig)) {
-            $this->requireJsConfig = $this->computeRequireJsConfig($isDevelopment, $loadedExtensions);
-            $cache->set($cacheIdentifier, $this->requireJsConfig);
+        if (!isset($requireJsConfig['internal']) || !isset($requireJsConfig['public']) || true) {
+            $requireJsConfig = $this->computeRequireJsConfig($isDevelopment, $loadedExtensions);
+            $cache->set($cacheIdentifier, $requireJsConfig);
         }
+
+        $this->requireJsConfig = $requireJsConfig['internal'];
+        $this->publicRequireJsConfig = $requireJsConfig['public'];
+        $this->internalRequireJsPathModuleNames = $requireJsConfig['internalNames'];
     }
 
     /**
@@ -1280,18 +1327,22 @@ class PageRenderer implements \TYPO3\CMS\Core\SingletonInterface
     protected function computeRequireJsConfig($isDevelopment, array $loadedExtensions)
     {
         // load all paths to map to package names / namespaces
-        $requireJsConfig = [];
+        $requireJsConfig = [
+            'public' => [],
+            'internal' => [],
+            'internalNames' => [],
+        ];
 
         // In order to avoid browser caching of JS files, adding a GET parameter to the files loaded via requireJS
         if ($isDevelopment) {
-            $requireJsConfig['urlArgs'] = 'bust=' . $GLOBALS['EXEC_TIME'];
+            $requireJsConfig['public']['urlArgs'] = 'bust=' . $GLOBALS['EXEC_TIME'];
         } else {
-            $requireJsConfig['urlArgs'] = 'bust=' . GeneralUtility::hmac(TYPO3_version . Environment::getProjectPath());
+            $requireJsConfig['public']['urlArgs'] = 'bust=' . GeneralUtility::hmac(TYPO3_version . Environment::getProjectPath());
         }
         $corePath = ExtensionManagementUtility::extPath('core', 'Resources/Public/JavaScript/Contrib/');
         $corePath = PathUtility::getAbsoluteWebPath($corePath);
         // first, load all paths for the namespaces, and configure contrib libs.
-        $requireJsConfig['paths'] = [
+        $requireJsConfig['public']['paths'] = [
             'jquery' => $corePath . '/jquery/jquery',
             'jquery-ui' => $corePath . 'jquery-ui',
             'datatables' => $corePath . 'jquery.dataTables',
@@ -1307,16 +1358,32 @@ class PageRenderer implements \TYPO3\CMS\Core\SingletonInterface
             'jquery/autocomplete' => $corePath . 'jquery.autocomplete',
             'd3' => $corePath . 'd3/d3'
         ];
-        $requireJsConfig['waitSeconds']  = 30;
+        $requireJsConfig['public']['waitSeconds'] = 30;
+        $requireJsConfig['public']['typo3BaseUrl'] = false;
+        $publicPackageNames = ['core', 'frontend', 'backend'];
         foreach ($loadedExtensions as $packageName) {
-            $fullJsPath = 'EXT:' . $packageName . '/Resources/Public/JavaScript/';
-            $fullJsPath = GeneralUtility::getFileAbsFileName($fullJsPath);
-            $fullJsPath = PathUtility::getAbsoluteWebPath($fullJsPath);
+            $jsPath = 'EXT:' . $packageName . '/Resources/Public/JavaScript/';
+            $absoluteJsPath = GeneralUtility::getFileAbsFileName($jsPath);
+            $fullJsPath = PathUtility::getAbsoluteWebPath($absoluteJsPath);
             $fullJsPath = rtrim($fullJsPath, '/');
-            if ($fullJsPath) {
-                $requireJsConfig['paths']['TYPO3/CMS/' . GeneralUtility::underscoredToUpperCamelCase($packageName)] = $fullJsPath;
+            if (!empty($fullJsPath) && file_exists($absoluteJsPath)) {
+                $type = in_array($packageName, $publicPackageNames, true) ? 'public' : 'internal';
+                $requireJsConfig[$type]['paths']['TYPO3/CMS/' . GeneralUtility::underscoredToUpperCamelCase($packageName)] = $fullJsPath;
             }
         }
+        // sanitize module names in internal 'paths'
+        $internalPathModuleNames = array_keys($requireJsConfig['internal']['paths'] ?? []);
+        $sanitizedInternalPathModuleNames = array_map(
+            function ($moduleName) {
+                // trim spaces and slashes & add ending slash
+                return trim($moduleName, ' /') . '/';
+            },
+            $internalPathModuleNames
+        );
+        $requireJsConfig['internalNames'] = array_combine(
+            $sanitizedInternalPathModuleNames,
+            $internalPathModuleNames
+        );
 
         // check if additional AMD modules need to be loaded if a single AMD module is initialized
         if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['RequireJS']['postInitializationModules'] ?? false)) {
@@ -1351,6 +1418,72 @@ class PageRenderer implements \TYPO3\CMS\Core\SingletonInterface
     }
 
     /**
+     * Generates RequireJS loader HTML markup.
+     *
+     * @return string
+     * @throws \TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException
+     */
+    protected function getRequireJsLoader(): string
+    {
+        $html = '';
+        $backendRequest = TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_BE;
+        $backendUserLoggedIn = !empty($GLOBALS['BE_USER']->user['uid']);
+
+        // no backend request - basically frontend
+        if (!$backendRequest) {
+            $requireJsConfig = $this->getRequireJsConfig(static::REQUIREJS_SCOPE_CONFIG);
+            $requireJsConfig['typo3BaseUrl'] = GeneralUtility::getIndpEnv('TYPO3_SITE_PATH') . '?eID=requirejs';
+        // backend request, but no backend user logged in
+        } elseif (!$backendUserLoggedIn) {
+            $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
+            $requireJsConfig = $this->getRequireJsConfig(static::REQUIREJS_SCOPE_CONFIG);
+            $requireJsConfig['typo3BaseUrl'] = (string)$uriBuilder->buildUriFromRoute('ajax_core_requirejs');
+        // backend request, having backend user logged in
+        } else {
+            $requireJsConfig = array_replace_recursive(
+                $this->publicRequireJsConfig,
+                $this->requireJsConfig
+            );
+        }
+
+        // add (probably filtered) RequireJS configuration
+        $html .= GeneralUtility::wrapJS('var require = ' . json_encode($requireJsConfig)) . LF;
+        // directly after that, include the require.js file
+        $html .= '<script src="'
+            . $this->processJsFile($this->requireJsPath . 'require.js')
+            . '" type="text/javascript"></script>' . LF;
+
+        if (!empty($requireJsConfig['typo3BaseUrl'])) {
+            $html .= '<script src="'
+                . $this->processJsFile($this->getAbsoluteWebPath(
+                    GeneralUtility::getFileAbsFileName(
+                        'EXT:core/Resources/Public/JavaScript/requirejs-loader.js'
+                    )
+                ))
+                . '" type="text/javascript"></script>' . LF;
+        }
+
+        return $html;
+    }
+
+    /**
+     * @param array $array
+     * @param string[] $keys
+     * @param bool $keep
+     * @return array
+     */
+    protected function filterArrayKeys(array $array, array $keys, bool $keep = true): array
+    {
+        return array_filter(
+            $array,
+            function (string $key) use ($keys, $keep) {
+                return in_array($key, $keys, true) === $keep;
+            },
+            ARRAY_FILTER_USE_KEY
+        );
+    }
+
+    /**
      * includes an AMD-compatible JS file by resolving the ModuleName, and then requires the file via a requireJS request,
      * additionally allowing to execute JavaScript code afterwards
      *
@@ -1371,7 +1504,13 @@ class PageRenderer implements \TYPO3\CMS\Core\SingletonInterface
         $inlineCodeKey = $mainModuleName;
         // make sure requireJS is initialized
         $this->loadRequireJs();
-
+        // move internal module path definition to public module definition
+        // (since loading a module ends up disclosing the existence anyway)
+        $baseModuleName = $this->findRequireJsBaseModuleName($mainModuleName);
+        if ($baseModuleName !== null && isset($this->requireJsConfig['paths'][$baseModuleName])) {
+            $this->publicRequireJsConfig['paths'][$baseModuleName] = $this->requireJsConfig['paths'][$baseModuleName];
+            unset($this->requireJsConfig['paths'][$baseModuleName]);
+        }
         // execute the main module, and load a possible callback function
         $javaScriptCode = 'require(["' . $mainModuleName . '"]';
         if ($callBackFunction !== null) {
@@ -1383,6 +1522,24 @@ class PageRenderer implements \TYPO3\CMS\Core\SingletonInterface
     }
 
     /**
+     * Determines requireJS base module name (if defined).
+     *
+     * @param string $moduleName
+     * @return string|null
+     */
+    protected function findRequireJsBaseModuleName(string $moduleName)
+    {
+        // trim spaces and slashes & add ending slash
+        $sanitizedModuleName = trim($moduleName, ' /') . '/';
+        foreach ($this->internalRequireJsPathModuleNames as $sanitizedBaseModuleName => $baseModuleName) {
+            if (strpos($sanitizedModuleName, $sanitizedBaseModuleName) === 0) {
+                return $baseModuleName;
+            }
+        }
+        return null;
+    }
+
+    /**
      * Adds Javascript Inline Label. This will occur in TYPO3.lang - object
      * The label can be used in scripts with TYPO3.lang.<key>
      *
@@ -1776,15 +1933,13 @@ class PageRenderer implements \TYPO3\CMS\Core\SingletonInterface
 
         // Include RequireJS
         if ($this->addRequireJs) {
-            // load the paths of the requireJS configuration
-            $out .= GeneralUtility::wrapJS('var require = ' . json_encode($this->requireJsConfig)) . LF;
-            // directly after that, include the require.js file
-            $out .= '<script src="' . $this->processJsFile($this->requireJsPath . 'require.js') . '" type="text/javascript"></script>' . LF;
+            $out .= $this->getRequireJsLoader();
         }
 
         $this->loadJavaScriptLanguageStrings();
         if (TYPO3_MODE === 'BE') {
-            $this->addAjaxUrlsToInlineSettings();
+            $noBackendUserLoggedIn = empty($GLOBALS['BE_USER']->user['uid']);
+            $this->addAjaxUrlsToInlineSettings($noBackendUserLoggedIn);
         }
         $inlineSettings = '';
         $languageLabels = $this->parseLanguageLabelsForJavaScript();
@@ -1862,8 +2017,10 @@ class PageRenderer implements \TYPO3\CMS\Core\SingletonInterface
 
     /**
      * Make URLs to all backend ajax handlers available as inline setting.
+     *
+     * @param bool $publicRoutesOnly
      */
-    protected function addAjaxUrlsToInlineSettings()
+    protected function addAjaxUrlsToInlineSettings(bool $publicRoutesOnly = false)
     {
         $ajaxUrls = [];
         // Add the ajax-based routes
@@ -1873,6 +2030,9 @@ class PageRenderer implements \TYPO3\CMS\Core\SingletonInterface
         $router = GeneralUtility::makeInstance(Router::class);
         $routes = $router->getRoutes();
         foreach ($routes as $routeIdentifier => $route) {
+            if ($publicRoutesOnly && $route->getOption('access') !== 'public') {
+                continue;
+            }
             if ($route->getOption('ajax')) {
                 $uri = (string)$uriBuilder->buildUriFromRoute($routeIdentifier);
                 // use the shortened value in order to use this in JavaScript
diff --git a/typo3/sysext/core/Configuration/Backend/AjaxRoutes.php b/typo3/sysext/core/Configuration/Backend/AjaxRoutes.php
new file mode 100644 (file)
index 0000000..7a8faae
--- /dev/null
@@ -0,0 +1,14 @@
+<?php
+use TYPO3\CMS\Core\Controller\RequireJsController;
+
+/**
+ * Definitions for routes provided by EXT:core
+ */
+return [
+    // dynamically load requirejs module definitions
+    'core_requirejs' => [
+        'path' => '/core/requirejs',
+        'access' => 'public',
+        'target' => RequireJsController::class . '::retrieveConfiguration',
+    ],
+];
diff --git a/typo3/sysext/core/Resources/Public/JavaScript/requirejs-loader.js b/typo3/sysext/core/Resources/Public/JavaScript/requirejs-loader.js
new file mode 100644 (file)
index 0000000..e0a000a
--- /dev/null
@@ -0,0 +1,134 @@
+(function(req) {
+  /**
+   * Determines whether moduleName is configured in requirejs paths
+   * (this code was taken from RequireJS context.nameToUrl).
+   *
+   * @see context.nameToUrl
+   * @see https://github.com/requirejs/requirejs/blob/2.3.3/require.js#L1650-L1670
+   *
+   * @param {Object} config the require context to find state.
+   * @param {String} moduleName the name of the module.
+   * @return {boolean}
+   */
+  var inPath = function(config, moduleName) {
+    var i, parentModule, parentPath;
+    var paths = config.paths;
+    var syms = moduleName.split('/');
+    //For each module name segment, see if there is a path
+    //registered for it. Start with most specific name
+    //and work up from it.
+    for (i = syms.length; i > 0; i -= 1) {
+      parentModule = syms.slice(0, i).join('/');
+      parentPath = paths[parentModule];
+      if (parentPath) {
+        return true;
+      }
+    }
+    return false;
+  };
+
+  /**
+   * @return {XMLHttpRequest}
+   */
+  var createXhr = function() {
+    if (typeof XMLHttpRequest !== 'undefined') {
+      return new XMLHttpRequest();
+    } else {
+      return new ActiveXObject('Microsoft.XMLHTTP');
+    }
+  };
+
+  /**
+   * Fetches RequireJS configuration from server via XHR call.
+   *
+   * @param {object} config
+   * @param {string} name
+   * @param {function} success
+   * @param {function} error
+   */
+  var fetchConfiguration = function(config, name, success, error) {
+    // cannot use jQuery here which would be loaded via RequireJS...
+    var xhr = createXhr();
+    xhr.onreadystatechange = function() {
+      if (this.readyState !== 4) {
+        return;
+      }
+      try {
+        if (this.status === 200) {
+          success(JSON.parse(xhr.responseText));
+        } else {
+          error(this.status, xhr.statusText);
+        }
+      } catch (error) {
+        error(this.status, error);
+      }
+    };
+    xhr.open('GET', config.typo3BaseUrl + '&name=' + encodeURIComponent(name));
+    xhr.send();
+  };
+
+  /**
+   * Adds aspects to RequireJS configuration keys paths and packages.
+   *
+   * @param {object} config
+   * @param {string} data
+   * @param {object} context
+   */
+  var addToConfiguration = function(config, data, context) {
+    if (data.shim && data.shim instanceof Object) {
+      if (typeof config.shim === 'undefined') {
+        config.shim = {};
+      }
+      Object.keys(data.shim).forEach(function(moduleName) {
+        config.shim[moduleName] = data.shim[moduleName];
+      });
+    }
+    if (data.paths && data.paths instanceof Object) {
+      if (typeof config.paths === 'undefined') {
+        config.paths = {};
+      }
+      Object.keys(data.paths).forEach(function(moduleName) {
+        config.paths[moduleName] = data.paths[moduleName];
+      });
+    }
+    if (data.packages && data.packages instanceof Array) {
+      if (typeof config.packages === 'undefined') {
+        config.packages = [];
+      }
+      data.packages.forEach(function (packageName) {
+        config.packages.push(packageName);
+      });
+    }
+    context.configure(config);
+  };
+
+  // keep reference to RequireJS default loader
+  var originalLoad = req.load;
+
+  /**
+   * Does the request to load a module for the browser case.
+   * Make this a separate function to allow other environments
+   * to override it.
+   *
+   * @param {Object} context the require context to find state.
+   * @param {String} name the name of the module.
+   * @param {Object} url the URL to the module.
+   */
+  req.load = function(context, name, url) {
+    if (inPath(context.config, name)) {
+      return originalLoad.call(req, context, name, url);
+    }
+
+    fetchConfiguration(
+      context.config,
+      name,
+      function(data) {
+        addToConfiguration(context.config, data, context);
+        url = context.nameToUrl(name);
+        // result cannot be returned since nested in two asynchronous calls
+        originalLoad.call(req, context, name, url);
+      },
+      function() {}
+    );
+  };
+})(requirejs);
index 090f128..48a66b1 100644 (file)
@@ -89,6 +89,7 @@ $signalSlotDispatcher->connect(
 unset($signalSlotDispatcher);
 
 $GLOBALS['TYPO3_CONF_VARS']['FE']['eID_include']['dumpFile'] = \TYPO3\CMS\Core\Controller\FileDumpController::class . '::dumpAction';
+$GLOBALS['TYPO3_CONF_VARS']['FE']['eID_include']['requirejs'] = \TYPO3\CMS\Core\Controller\RequireJsController::class . '::retrievePath';
 
 /** @var \TYPO3\CMS\Core\Resource\Rendering\RendererRegistry $rendererRegistry */
 $rendererRegistry = \TYPO3\CMS\Core\Resource\Rendering\RendererRegistry::getInstance();