diff --git a/Classes/Controller/RequireJsController.php b/Classes/Controller/RequireJsController.php
new file mode 100644
index 0000000000..23b88cf80e
--- /dev/null
+++ b/Classes/Controller/RequireJsController.php
@@ -0,0 +1,109 @@
+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;
+ }
+}
diff --git a/Classes/Page/PageRenderer.php b/Classes/Page/PageRenderer.php
index 303ac76463..5947a717a6 100644
--- a/Classes/Page/PageRenderer.php
+++ b/Classes/Page/PageRenderer.php
@@ -45,6 +45,9 @@ class PageRenderer implements \TYPO3\CMS\Core\SingletonInterface
// @deprecated will be removed in TYPO3 v10.0.
const JQUERY_NAMESPACE_NONE = 'none';
+ const REQUIREJS_SCOPE_CONFIG = 'config';
+ const REQUIREJS_SCOPE_RESOLVE = 'resolve';
+
/**
* @var bool
*/
@@ -332,11 +335,23 @@ 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 bool
*/
@@ -599,6 +614,34 @@ public function setRequireJsPath($path)
$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 */
@@ -1399,7 +1442,7 @@ public function loadJquery($version = null, $source = null, $namespace = self::J
public function loadRequireJs()
{
$this->addRequireJs = true;
- if (!empty($this->requireJsConfig)) {
+ if (!empty($this->requireJsConfig) && !empty($this->publicRequireJsConfig)) {
return;
}
@@ -1408,13 +1451,17 @@ public function loadRequireJs()
$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'];
}
/**
@@ -1428,18 +1475,22 @@ public function loadRequireJs()
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',
@@ -1455,16 +1506,32 @@ protected function computeRequireJsConfig($isDevelopment, array $loadedExtension
'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)) {
@@ -1498,6 +1565,72 @@ public function addRequireJsConfiguration(array $configuration)
\TYPO3\CMS\Core\Utility\ArrayUtility::mergeRecursiveWithOverrule($this->requireJsConfig, $configuration);
}
+ /**
+ * 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 .= '' . LF;
+
+ if (!empty($requireJsConfig['typo3BaseUrl'])) {
+ $html .= '' . 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
@@ -1519,7 +1652,13 @@ public function loadRequireJsModule($mainModuleName, $callBackFunction = null)
$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) {
@@ -1530,6 +1669,24 @@ public function loadRequireJsModule($mainModuleName, $callBackFunction = null)
$this->addJsInlineCode('RequireJS-Module-' . $inlineCodeKey, $javaScriptCode);
}
+ /**
+ * 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.
@@ -1938,10 +2095,7 @@ protected function renderMainJavaScriptLibraries()
// 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 .= '' . LF;
+ $out .= $this->getRequireJsLoader();
}
// Include jQuery Core for each namespace, depending on the version and source
@@ -1953,7 +2107,8 @@ protected function renderMainJavaScriptLibraries()
$this->loadJavaScriptLanguageStrings();
if (TYPO3_MODE === 'BE') {
- $this->addAjaxUrlsToInlineSettings();
+ $noBackendUserLoggedIn = empty($GLOBALS['BE_USER']->user['uid']);
+ $this->addAjaxUrlsToInlineSettings($noBackendUserLoggedIn);
}
$inlineSettings = '';
$languageLabels = $this->parseLanguageLabelsForJavaScript();
@@ -2031,8 +2186,10 @@ protected function convertCharsetRecursivelyToUtf8(&$data, string $fromCharset)
/**
* 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
@@ -2042,6 +2199,9 @@ protected function addAjaxUrlsToInlineSettings()
$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/Configuration/Backend/AjaxRoutes.php b/Configuration/Backend/AjaxRoutes.php
new file mode 100644
index 0000000000..7a8faaecea
--- /dev/null
+++ b/Configuration/Backend/AjaxRoutes.php
@@ -0,0 +1,14 @@
+ [
+ 'path' => '/core/requirejs',
+ 'access' => 'public',
+ 'target' => RequireJsController::class . '::retrieveConfiguration',
+ ],
+];
diff --git a/Resources/Public/JavaScript/requirejs-loader.js b/Resources/Public/JavaScript/requirejs-loader.js
new file mode 100644
index 0000000000..e0a000af30
--- /dev/null
+++ b/Resources/Public/JavaScript/requirejs-loader.js
@@ -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);
diff --git a/ext_localconf.php b/ext_localconf.php
index d441fc77f8..3227779f19 100644
--- a/ext_localconf.php
+++ b/ext_localconf.php
@@ -89,6 +89,7 @@
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();