diff --git a/modules/apps/client-extension/client-extension-web/src/main/java/com/liferay/client/extension/web/internal/portlet/CustomElementCETPortlet.java b/modules/apps/client-extension/client-extension-web/src/main/java/com/liferay/client/extension/web/internal/portlet/CustomElementCETPortlet.java index 1a0e05d8b90775..37267cecc1f23a 100644 --- a/modules/apps/client-extension/client-extension-web/src/main/java/com/liferay/client/extension/web/internal/portlet/CustomElementCETPortlet.java +++ b/modules/apps/client-extension/client-extension-web/src/main/java/com/liferay/client/extension/web/internal/portlet/CustomElementCETPortlet.java @@ -166,10 +166,6 @@ private String[] _prepareURLs(long lastModified, String[] urls) { if (urls[i].contains(contextPath + "/o/")) { urls[i] = urls[i].replace(contextPath + "/o/", "/o/"); } - - if (!urls[i].startsWith("module:")) { - urls[i] = "nocombo:" + urls[i]; - } } return urls; diff --git a/modules/apps/frontend-js/frontend-js-loader-modules-extender/build.gradle b/modules/apps/frontend-js/frontend-js-loader-modules-extender/build.gradle index 22881a3df2dc10..7120d0c25a4f9b 100644 --- a/modules/apps/frontend-js/frontend-js-loader-modules-extender/build.gradle +++ b/modules/apps/frontend-js/frontend-js-loader-modules-extender/build.gradle @@ -1,4 +1,6 @@ -import com.liferay.gradle.util.copy.StripPathSegmentsAction +import com.liferay.gradle.util.hash.HashUtil + +import org.gradle.api.file.RelativePath task buildLiferayAMDLoader(type: Copy) @@ -6,7 +8,6 @@ File jsDestinationDir = file("tmp/META-INF/resources") buildLiferayAMDLoader { dependsOn npmInstall - eachFile new StripPathSegmentsAction(4) from npmInstall.nodeModulesDir include "@liferay/amd-loader/build/loader/loader.js.map" @@ -14,6 +15,32 @@ buildLiferayAMDLoader { includeEmptyDirs = false into jsDestinationDir + + eachFile { + details -> + + def sourcePath = details.sourcePath + + if (sourcePath.endsWith(".map")) { + sourcePath = sourcePath[0..-5] + } + + def md5 = HashUtil.md5( + new File("${npmInstall.nodeModulesDir}/${sourcePath}")) + + def hash = md5.asCompactString()[0..11] + + def segments = details.relativePath.segments + + def fileName = segments[-1] + + def i = fileName.indexOf(".") + + segments[-1] = "${fileName[0..i]}(${hash})${fileName[i..-1]}" + + details.relativePath = new RelativePath( + true, segments[4..-1].toArray(new String[]{})) + } } classes { diff --git a/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/configuration/FrontendCachingConfiguration.java b/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/configuration/FrontendCachingConfiguration.java index 78973d971b490e..ec73ae8fbe5195 100644 --- a/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/configuration/FrontendCachingConfiguration.java +++ b/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/configuration/FrontendCachingConfiguration.java @@ -35,6 +35,12 @@ public interface FrontendCachingConfiguration { ) public long esModulesMaxAge(); + @Meta.AD( + deflt = "3600", description = "labels-modules-max-age-help", + name = "labels-modules-max-age", required = false + ) + public long labelsModulesMaxAge(); + @Meta.AD( deflt = "false", description = "send-no-cache-for-css-style-sheets-help", @@ -48,4 +54,10 @@ public interface FrontendCachingConfiguration { ) public boolean sendNoCacheForESModules(); + @Meta.AD( + deflt = "false", description = "send-no-cache-for-labels-modules-help", + name = "send-no-cache-for-labels-modules", required = false + ) + public boolean sendNoCacheForLabelsModules(); + } \ No newline at end of file diff --git a/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/js/importmaps/extender/FrontendJsWebDynamicJSImportMapsContributor.java b/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/js/importmaps/extender/FrontendJsWebDynamicJSImportMapsContributor.java index 856ba654bbcbf9..e810e28554ecb3 100644 --- a/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/js/importmaps/extender/FrontendJsWebDynamicJSImportMapsContributor.java +++ b/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/js/importmaps/extender/FrontendJsWebDynamicJSImportMapsContributor.java @@ -6,6 +6,7 @@ package com.liferay.frontend.js.web.internal.js.importmaps.extender; import com.liferay.frontend.js.importmaps.extender.DynamicJSImportMapsContributor; +import com.liferay.frontend.js.web.internal.resource.handler.LanguageFrontendResourceRequestHandler; import com.liferay.petra.string.StringPool; import com.liferay.portal.kernel.exception.PortalException; import com.liferay.portal.kernel.frontend.hashed.files.HashedFilesRegistry; @@ -31,14 +32,19 @@ public void writeGlobalImports( HttpServletRequest httpServletRequest, Writer writer) throws IOException { - writer.write("\"@liferay/language/\": \""); + writer.write(StringPool.QUOTE); + writer.write( + LanguageFrontendResourceRequestHandler.LANGUAGE_MODULE_PREFIX); + writer.write("\": \""); String cdnHost = _getCDNHost(httpServletRequest); writer.write(cdnHost); writer.write(_portal.getPathContext(httpServletRequest)); - writer.write("/o/js/language/\""); + writer.write( + LanguageFrontendResourceRequestHandler.LANGUAGE_URI_PREFIX); + writer.write(StringPool.QUOTE); _hashedFilesRegistry.forEach( (unhashedFileURI, hashedFileURI) -> { diff --git a/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/resource/JavaScriptFrontendResource.java b/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/resource/JavaScriptFrontendResource.java new file mode 100644 index 00000000000000..4646a02142d086 --- /dev/null +++ b/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/resource/JavaScriptFrontendResource.java @@ -0,0 +1,99 @@ +/** + * SPDX-FileCopyrightText: (c) 2025 Liferay, Inc. https://liferay.com + * SPDX-License-Identifier: LGPL-2.1-or-later OR LicenseRef-Liferay-DXP-EULA-2.0.0-2023-06 + */ + +package com.liferay.frontend.js.web.internal.resource; + +import com.liferay.petra.io.StreamUtil; +import com.liferay.portal.kernel.language.Language; +import com.liferay.portal.kernel.util.ContentTypes; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import java.net.URL; + +import java.nio.charset.StandardCharsets; + +import java.util.ResourceBundle; + +/** + * @author Iván Zaera Avellón + */ +public class JavaScriptFrontendResource implements FrontendResource { + + public JavaScriptFrontendResource( + String eTag, boolean immutable, Language language, long maxAge, + ResourceBundle resourceBundle, boolean sendNoCache, URL url) { + + if (resourceBundle != null) { + if (eTag != null) { + throw new IllegalArgumentException( + "Translated resources cannot have an eTag"); + } + + if (immutable) { + throw new IllegalArgumentException( + "Translated resources cannot be immutable"); + } + } + + _eTag = eTag; + _immutable = immutable; + _language = language; + _maxAge = maxAge; + _resourceBundle = resourceBundle; + _sendNoCache = sendNoCache; + _url = url; + } + + @Override + public String getContentType() { + return ContentTypes.APPLICATION_JAVASCRIPT; + } + + @Override + public String getETag() { + return _eTag; + } + + @Override + public InputStream getInputStream() throws IOException { + if (_resourceBundle == null) { + return _url.openStream(); + } + + String content = _language.process( + () -> _resourceBundle, _resourceBundle.getLocale(), + StreamUtil.toString(_url.openStream())); + + return new ByteArrayInputStream( + content.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public long getMaxAge() { + return _maxAge; + } + + @Override + public boolean isImmutable() { + return _immutable; + } + + @Override + public boolean isSendNoCache() { + return _sendNoCache; + } + + private final String _eTag; + private final boolean _immutable; + private final Language _language; + private final long _maxAge; + private final ResourceBundle _resourceBundle; + private final boolean _sendNoCache; + private final URL _url; + +} \ No newline at end of file diff --git a/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/resource/LanguageFrontendResource.java b/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/resource/LanguageFrontendResource.java new file mode 100644 index 00000000000000..1c8ee3593ffdaf --- /dev/null +++ b/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/resource/LanguageFrontendResource.java @@ -0,0 +1,144 @@ +/** + * SPDX-FileCopyrightText: (c) 2025 Liferay, Inc. https://liferay.com + * SPDX-License-Identifier: LGPL-2.1-or-later OR LicenseRef-Liferay-DXP-EULA-2.0.0-2023-06 + */ + +package com.liferay.frontend.js.web.internal.resource; + +import com.liferay.petra.string.StringPool; +import com.liferay.portal.kernel.json.JSONArray; +import com.liferay.portal.kernel.json.JSONException; +import com.liferay.portal.kernel.json.JSONFactory; +import com.liferay.portal.kernel.json.JSONObject; +import com.liferay.portal.kernel.language.Language; +import com.liferay.portal.kernel.log.Log; +import com.liferay.portal.kernel.log.LogFactoryUtil; +import com.liferay.portal.kernel.util.ContentTypes; +import com.liferay.portal.kernel.util.LocaleUtil; +import com.liferay.portal.kernel.util.StringUtil; +import com.liferay.portal.kernel.util.URLUtil; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import java.net.URL; + +import java.nio.charset.StandardCharsets; + +import java.util.Locale; + +/** + * @author Iván Zaera Avellón + */ +public class LanguageFrontendResource implements FrontendResource { + + public LanguageFrontendResource( + JSONFactory jsonFactory, Language language, String languageId, + long maxAge, boolean sendNoCache, URL url) { + + _jsonFactory = jsonFactory; + _language = language; + _languageId = languageId; + _maxAge = maxAge; + _sendNoCache = sendNoCache; + _url = url; + } + + @Override + public String getContentType() { + return ContentTypes.TEXT_JAVASCRIPT; + } + + @Override + public String getETag() { + return null; + } + + @Override + public InputStream getInputStream() throws IOException { + JSONArray languageKeysJSONArray = _getLanguageKeysJSONArray(); + + StringBuilder sb = new StringBuilder(); + + Locale locale = LocaleUtil.fromLanguageId(_languageId); + + for (int i = 0; i < languageKeysJSONArray.length(); i++) { + String key = languageKeysJSONArray.getString(i); + + String label = _language.get(locale, key); + + sb.append(StringPool.APOSTROPHE); + sb.append(key.replaceAll("'", "\\\\'")); + sb.append("':'"); + sb.append(label.replaceAll("'", "\\\\'")); + sb.append("',\n"); + } + + String content = StringUtil.replace( + _TPL_JAVA_SCRIPT, new String[] {"[$LABELS$]"}, + new String[] {sb.toString()}); + + return new ByteArrayInputStream( + content.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public long getMaxAge() { + return _maxAge; + } + + @Override + public boolean isImmutable() { + return false; + } + + @Override + public boolean isSendNoCache() { + return _sendNoCache; + } + + private static String _loadTemplate(String name) { + try (InputStream inputStream = + LanguageFrontendResource.class.getResourceAsStream( + "dependencies/" + name)) { + + return StringUtil.read(inputStream); + } + catch (Exception exception) { + _log.error("Unable to read template " + name, exception); + } + + return StringPool.BLANK; + } + + private JSONArray _getLanguageKeysJSONArray() throws IOException { + try { + JSONObject jsonObject = _jsonFactory.createJSONObject( + URLUtil.toString(_url)); + + return jsonObject.getJSONArray("keys"); + } + catch (JSONException jsonException) { + throw new IOException( + "Invalid language JSON file " + _url, jsonException); + } + } + + private static final String _TPL_JAVA_SCRIPT; + + private static final Log _log = LogFactoryUtil.getLog( + LanguageFrontendResource.class); + + static { + _TPL_JAVA_SCRIPT = _loadTemplate("all.js.tpl"); + } + + private final JSONFactory _jsonFactory; + private final Language _language; + private final String _languageId; + private final long _maxAge; + private final boolean _sendNoCache; + private final URL _url; + +} \ No newline at end of file diff --git a/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/resource/handler/JavaScriptFrontendResourceRequestHandler.java b/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/resource/handler/JavaScriptFrontendResourceRequestHandler.java new file mode 100644 index 00000000000000..c079627b1ff30c --- /dev/null +++ b/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/resource/handler/JavaScriptFrontendResourceRequestHandler.java @@ -0,0 +1,218 @@ +/** + * SPDX-FileCopyrightText: (c) 2025 Liferay, Inc. https://liferay.com + * SPDX-License-Identifier: LGPL-2.1-or-later OR LicenseRef-Liferay-DXP-EULA-2.0.0-2023-06 + */ + +package com.liferay.frontend.js.web.internal.resource.handler; + +import com.liferay.frontend.js.web.internal.configuration.FrontendCachingConfiguration; +import com.liferay.frontend.js.web.internal.resource.FrontendResource; +import com.liferay.frontend.js.web.internal.resource.JavaScriptFrontendResource; +import com.liferay.petra.string.CharPool; +import com.liferay.petra.string.StringBundler; +import com.liferay.portal.kernel.frontend.hashed.files.HashedFilesRegistry; +import com.liferay.portal.kernel.frontend.hashed.files.HashedFilesUtil; +import com.liferay.portal.kernel.language.Language; +import com.liferay.portal.kernel.log.Log; +import com.liferay.portal.kernel.log.LogFactoryUtil; +import com.liferay.portal.kernel.portlet.PortletConfigFactory; +import com.liferay.portal.kernel.util.AggregateResourceBundle; +import com.liferay.portal.kernel.util.LocaleUtil; +import com.liferay.portal.language.LanguageResources; + +import jakarta.portlet.PortletConfig; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; + +import java.io.IOException; + +import java.net.URL; + +import java.util.Enumeration; +import java.util.Locale; +import java.util.ResourceBundle; + +/** + * @author Iván Zaera Avellón + */ +public class JavaScriptFrontendResourceRequestHandler + implements FrontendResourceRequestHandler { + + public JavaScriptFrontendResourceRequestHandler( + FrontendCachingConfiguration frontendCachingConfiguration, + HashedFilesRegistry hashedFilesRegistry, Language language, + PortletConfigFactory portletConfigFactory) { + + _frontendCachingConfiguration = frontendCachingConfiguration; + _hashedFilesRegistry = hashedFilesRegistry; + _language = language; + _portletConfigFactory = portletConfigFactory; + } + + @Override + public boolean canHandleRequest(HttpServletRequest httpServletRequest) { + String requestURI = httpServletRequest.getRequestURI(); + + if (!requestURI.endsWith(".js")) { + return false; + } + + if (HashedFilesUtil.containsHash(requestURI)) { + return true; + } + + String hashedFileURI = _hashedFilesRegistry.getHashedFileURI( + requestURI); + + if (hashedFileURI != null) { + return true; + } + + URL resourceURL = _hashedFilesRegistry.getResource(requestURI); + + if (resourceURL != null) { + return true; + } + + return false; + } + + @Override + public FrontendResource handleRequest(HttpServletRequest httpServletRequest) + throws IOException, ServletException { + + String requestURI = httpServletRequest.getRequestURI(); + + String requestHash = HashedFilesUtil.getHash(requestURI); + + ResourceBundle resourceBundle = _getResourceBundle(httpServletRequest); + + if (requestHash != null) { + if (_log.isDebugEnabled()) { + _log.debug("Handling request " + requestURI); + } + + if (resourceBundle == null) { + return _createFrontendResource( + requestHash, true, null, requestURI); + } + + return _createFrontendResource( + null, false, resourceBundle, requestURI); + } + + String hashedFileURI = _hashedFilesRegistry.getHashedFileURI( + requestURI); + + if (hashedFileURI == null) { + if (_log.isDebugEnabled()) { + _log.debug("Handling request " + requestURI); + } + + return _createFrontendResource( + null, false, resourceBundle, requestURI); + } + + if (_log.isDebugEnabled()) { + _log.debug( + StringBundler.concat( + "Handling request ", requestURI, " with static file ", + hashedFileURI)); + } + + return _createFrontendResource( + (resourceBundle == null) ? HashedFilesUtil.getHash(hashedFileURI) : + null, + false, resourceBundle, hashedFileURI); + } + + private FrontendResource _createFrontendResource( + String eTag, boolean immutable, ResourceBundle resourceBundle, + String resourceURI) { + + long maxAge = 31536000; + boolean sendNoCache = false; + + if (!immutable) { + maxAge = _frontendCachingConfiguration.esModulesMaxAge(); + sendNoCache = + _frontendCachingConfiguration.sendNoCacheForESModules(); + } + + URL resourceURL = _hashedFilesRegistry.getResource(resourceURI); + + if (resourceURL == null) { + return null; + } + + return new JavaScriptFrontendResource( + eTag, immutable, _language, maxAge, resourceBundle, sendNoCache, + resourceURL); + } + + private ResourceBundle _getResourceBundle( + HttpServletRequest httpServletRequest) { + + if (Boolean.valueOf(httpServletRequest.getParameter("translate"))) { + Locale locale = LocaleUtil.fromLanguageId( + _language.getLanguageId(httpServletRequest)); + + ResourceBundle resourceBundle = LanguageResources.getResourceBundle( + locale); + + PortletConfig portletConfig = null; + + Enumeration enumeration = + httpServletRequest.getParameterNames(); + + while (enumeration.hasMoreElements()) { + String parameterName = enumeration.nextElement(); + + int index = parameterName.indexOf(CharPool.COLON); + + if (index > 0) { + portletConfig = _portletConfigFactory.get( + parameterName.substring(0, index)); + } + } + + if (portletConfig != null) { + resourceBundle = new AggregateResourceBundle( + portletConfig.getResourceBundle(locale), resourceBundle); + } + + final ResourceBundle finalResourceBundle = resourceBundle; + + return new ResourceBundle() { + + @Override + public Enumeration getKeys() { + return finalResourceBundle.getKeys(); + } + + @Override + public Locale getLocale() { + return locale; + } + + @Override + protected Object handleGetObject(String s) { + return finalResourceBundle.getObject(s); + } + + }; + } + + return null; + } + + private static final Log _log = LogFactoryUtil.getLog( + JavaScriptFrontendResourceRequestHandler.class); + + private final FrontendCachingConfiguration _frontendCachingConfiguration; + private final HashedFilesRegistry _hashedFilesRegistry; + private final Language _language; + private final PortletConfigFactory _portletConfigFactory; + +} \ No newline at end of file diff --git a/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/resource/handler/LanguageFrontendResourceRequestHandler.java b/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/resource/handler/LanguageFrontendResourceRequestHandler.java new file mode 100644 index 00000000000000..1f0caefca4d9c2 --- /dev/null +++ b/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/resource/handler/LanguageFrontendResourceRequestHandler.java @@ -0,0 +1,114 @@ +/** + * SPDX-FileCopyrightText: (c) 2025 Liferay, Inc. https://liferay.com + * SPDX-License-Identifier: LGPL-2.1-or-later OR LicenseRef-Liferay-DXP-EULA-2.0.0-2023-06 + */ + +package com.liferay.frontend.js.web.internal.resource.handler; + +import com.liferay.frontend.js.web.internal.configuration.FrontendCachingConfiguration; +import com.liferay.frontend.js.web.internal.resource.FrontendResource; +import com.liferay.frontend.js.web.internal.resource.LanguageFrontendResource; +import com.liferay.petra.string.StringBundler; +import com.liferay.petra.string.StringPool; +import com.liferay.portal.configuration.module.configuration.ConfigurationProvider; +import com.liferay.portal.kernel.frontend.hashed.files.HashedFilesRegistry; +import com.liferay.portal.kernel.json.JSONFactory; +import com.liferay.portal.kernel.language.Language; +import com.liferay.portal.kernel.log.Log; +import com.liferay.portal.kernel.log.LogFactoryUtil; +import com.liferay.portal.kernel.module.configuration.ConfigurationException; +import com.liferay.portal.kernel.util.Portal; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; + +import java.io.IOException; + +import java.net.URL; + +/** + * @author Iván Zaera Avellón + */ +public class LanguageFrontendResourceRequestHandler + implements FrontendResourceRequestHandler { + + public static final String LANGUAGE_MODULE_PREFIX = "@liferay/language/"; + + public static final String LANGUAGE_URI_PREFIX = "/o/js/language/"; + + public LanguageFrontendResourceRequestHandler( + ConfigurationProvider configurationProvider, + HashedFilesRegistry hashedFilesRegistry, JSONFactory jsonFactory, + Language language, Portal portal) { + + _configurationProvider = configurationProvider; + _hashedFilesRegistry = hashedFilesRegistry; + _jsonFactory = jsonFactory; + _language = language; + _portal = portal; + } + + @Override + public boolean canHandleRequest(HttpServletRequest httpServletRequest) { + String requestURI = httpServletRequest.getRequestURI(); + + return requestURI.startsWith(LANGUAGE_URI_PREFIX); + } + + @Override + public FrontendResource handleRequest(HttpServletRequest httpServletRequest) + throws IOException, ServletException { + + String requestURI = httpServletRequest.getRequestURI(); + + requestURI = requestURI.substring(LANGUAGE_URI_PREFIX.length()); + + String[] requestURIParts = requestURI.split(StringPool.SLASH); + + if ((requestURIParts.length != 3) || + !requestURIParts[2].equals("all.js")) { + + return null; + } + + long maxAge = 3600; + boolean sendNoCache = false; + + long companyId = _portal.getCompanyId(httpServletRequest); + + try { + FrontendCachingConfiguration frontendCachingConfiguration = + _configurationProvider.getCompanyConfiguration( + FrontendCachingConfiguration.class, companyId); + + maxAge = frontendCachingConfiguration.labelsModulesMaxAge(); + sendNoCache = + frontendCachingConfiguration.sendNoCacheForLabelsModules(); + } + catch (ConfigurationException configurationException) { + _log.error( + "Unable to get frontend caching configuration: will use " + + "reasonable defaults instead", + configurationException); + } + + URL resourceURL = _hashedFilesRegistry.getResource( + StringBundler.concat( + Portal.PATH_MODULE, StringPool.SLASH, requestURIParts[1], + "/language.json")); + + return new LanguageFrontendResource( + _jsonFactory, _language, requestURIParts[0], maxAge, sendNoCache, + resourceURL); + } + + private static final Log _log = LogFactoryUtil.getLog( + LanguageFrontendResourceRequestHandler.class); + + private final ConfigurationProvider _configurationProvider; + private final HashedFilesRegistry _hashedFilesRegistry; + private final JSONFactory _jsonFactory; + private final Language _language; + private final Portal _portal; + +} \ No newline at end of file diff --git a/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/servlet/FrontendJsWebLanguageServlet.java b/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/servlet/FrontendJsWebLanguageServlet.java deleted file mode 100644 index 5fd1d002e4e5ab..00000000000000 --- a/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/servlet/FrontendJsWebLanguageServlet.java +++ /dev/null @@ -1,254 +0,0 @@ -/** - * SPDX-FileCopyrightText: (c) 2024 Liferay, Inc. https://liferay.com - * SPDX-License-Identifier: LGPL-2.1-or-later OR LicenseRef-Liferay-DXP-EULA-2.0.0-2023-06 - */ - -package com.liferay.frontend.js.web.internal.servlet; - -import com.liferay.osgi.service.tracker.collections.map.ServiceTrackerMap; -import com.liferay.osgi.service.tracker.collections.map.ServiceTrackerMapFactory; -import com.liferay.petra.string.StringPool; -import com.liferay.portal.kernel.json.JSONArray; -import com.liferay.portal.kernel.json.JSONException; -import com.liferay.portal.kernel.json.JSONFactory; -import com.liferay.portal.kernel.json.JSONObject; -import com.liferay.portal.kernel.language.Language; -import com.liferay.portal.kernel.log.Log; -import com.liferay.portal.kernel.log.LogFactoryUtil; -import com.liferay.portal.kernel.servlet.HttpHeaders; -import com.liferay.portal.kernel.util.ContentTypes; -import com.liferay.portal.kernel.util.DigesterUtil; -import com.liferay.portal.kernel.util.LocaleUtil; -import com.liferay.portal.kernel.util.Portal; -import com.liferay.portal.kernel.util.StringUtil; -import com.liferay.portal.kernel.util.URLUtil; - -import jakarta.servlet.Servlet; -import jakarta.servlet.ServletContext; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -import java.io.IOException; -import java.io.InputStream; -import java.io.PrintWriter; - -import java.net.URL; - -import java.util.Locale; -import java.util.concurrent.ConcurrentHashMap; - -import org.osgi.framework.BundleContext; -import org.osgi.service.component.annotations.Activate; -import org.osgi.service.component.annotations.Component; -import org.osgi.service.component.annotations.Deactivate; -import org.osgi.service.component.annotations.Reference; - -/** - * @author Iván Zaera Avellón - */ -@Component( - property = { - "osgi.http.whiteboard.servlet.name=Language Resources Servlet", - "osgi.http.whiteboard.servlet.pattern=/js/language/*", - "service.ranking:Integer=" + (Integer.MAX_VALUE - 1000) - }, - service = Servlet.class -) -public class FrontendJsWebLanguageServlet extends HttpServlet { - - @Activate - protected void activate(BundleContext bundleContext) { - _eTags.clear(); - - _serviceTrackerMap = ServiceTrackerMapFactory.openSingleValueMap( - bundleContext, ServletContext.class, null, - (serviceReference, emitter) -> { - ServletContext servletContext = bundleContext.getService( - serviceReference); - - try { - emitter.emit(servletContext.getContextPath()); - } - finally { - bundleContext.ungetService(serviceReference); - } - }); - } - - @Deactivate - protected void deactivate() { - _eTags.clear(); - - _serviceTrackerMap.close(); - - _serviceTrackerMap = null; - } - - @Override - protected void doGet( - HttpServletRequest httpServletRequest, - HttpServletResponse httpServletResponse) - throws IOException, ServletException { - - String pathInfo = httpServletRequest.getPathInfo(); - - // Check if path is valid - - String[] parts = pathInfo.split(StringPool.SLASH); - - if ((parts.length != 4) || !parts[3].equals("all.js")) { - httpServletResponse.sendError(HttpServletResponse.SC_NOT_FOUND); - - return; - } - - // Check if browser cache can be used - - String ifNoneMatch = httpServletRequest.getHeader( - HttpHeaders.IF_NONE_MATCH); - - if (ifNoneMatch != null) { - String eTag = _eTags.get(pathInfo); - - if ((eTag != null) && eTag.equals(ifNoneMatch)) { - httpServletResponse.setStatus( - HttpServletResponse.SC_NOT_MODIFIED); - httpServletResponse.setContentLength(0); - - return; - } - } - - // Check if servlet context exists - - String modulePath = _portal.getPathModule(); - String proxyPath = _portal.getPathProxy(); - String webContextPath = parts[2]; - - ServletContext servletContext = _serviceTrackerMap.getService( - modulePath.substring(proxyPath.length()) + StringPool.SLASH + - webContextPath); - - if (servletContext == null) { - httpServletResponse.sendError(HttpServletResponse.SC_NOT_FOUND); - - return; - } - - // Send response - - Locale locale = LocaleUtil.fromLanguageId(parts[1]); - - String content = _getContent(locale, servletContext); - - if (content == null) { - httpServletResponse.sendError(HttpServletResponse.SC_NOT_FOUND); - - return; - } - - String etag = - StringPool.QUOTE + DigesterUtil.digestBase64("SHA-1", content) + - StringPool.QUOTE; - - _eTags.put(pathInfo, etag); - - httpServletResponse.setCharacterEncoding(StringPool.UTF8); - httpServletResponse.setContentType(ContentTypes.TEXT_JAVASCRIPT_UTF8); - httpServletResponse.setHeader(HttpHeaders.ETAG, etag); - - PrintWriter printWriter = httpServletResponse.getWriter(); - - printWriter.write(content); - } - - private static String _loadTemplate(String name) { - try (InputStream inputStream = - FrontendJsWebLanguageServlet.class.getResourceAsStream( - "dependencies/" + name)) { - - return StringUtil.read(inputStream); - } - catch (Exception exception) { - _log.error("Unable to read template " + name, exception); - } - - return StringPool.BLANK; - } - - private String _getContent(Locale locale, ServletContext servletContext) - throws IOException { - - JSONArray languageKeysJSONArray = _getLanguageKeysJSONArray( - servletContext); - - if (languageKeysJSONArray == null) { - return null; - } - - StringBuilder sb = new StringBuilder(); - - for (int i = 0; i < languageKeysJSONArray.length(); i++) { - String key = languageKeysJSONArray.getString(i); - - String label = _language.get(locale, key); - - sb.append(StringPool.APOSTROPHE); - sb.append(key.replaceAll("'", "\\\\'")); - sb.append("':'"); - sb.append(label.replaceAll("'", "\\\\'")); - sb.append("',\n"); - } - - return StringUtil.replace( - _TPL_JAVA_SCRIPT, new String[] {"[$LABELS$]"}, - new String[] {sb.toString()}); - } - - private JSONArray _getLanguageKeysJSONArray(ServletContext servletContext) - throws IOException { - - URL url = servletContext.getResource("/language.json"); - - if (url == null) { - return null; - } - - try { - JSONObject jsonObject = _jsonFactory.createJSONObject( - URLUtil.toString(url)); - - return jsonObject.getJSONArray("keys"); - } - catch (JSONException jsonException) { - throw new IOException( - "Invalid language JSON file " + url, jsonException); - } - } - - private static final String _TPL_JAVA_SCRIPT; - - private static final Log _log = LogFactoryUtil.getLog( - FrontendJsWebLanguageServlet.class); - - static { - _TPL_JAVA_SCRIPT = _loadTemplate("all.js.tpl"); - } - - private final ConcurrentHashMap _eTags = - new ConcurrentHashMap<>(); - - @Reference - private JSONFactory _jsonFactory; - - @Reference - private Language _language; - - @Reference - private Portal _portal; - - private ServiceTrackerMap _serviceTrackerMap; - -} \ No newline at end of file diff --git a/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/servlet/filter/FrontendResourceFilter.java b/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/servlet/filter/FrontendResourceFilter.java index 3dde2d34afb7e2..6808c878700e5d 100644 --- a/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/servlet/filter/FrontendResourceFilter.java +++ b/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/servlet/filter/FrontendResourceFilter.java @@ -9,11 +9,17 @@ import com.liferay.frontend.js.web.internal.resource.FrontendResource; import com.liferay.frontend.js.web.internal.resource.handler.FrontendResourceRequestHandler; import com.liferay.frontend.js.web.internal.resource.handler.HashedFileFrontendResourceRequestHandler; +import com.liferay.frontend.js.web.internal.resource.handler.JavaScriptFrontendResourceRequestHandler; +import com.liferay.frontend.js.web.internal.resource.handler.LanguageFrontendResourceRequestHandler; import com.liferay.frontend.js.web.internal.resource.handler.StyleSheetFrontendResourceRequestHandler; import com.liferay.petra.io.StreamUtil; import com.liferay.petra.string.StringPool; import com.liferay.portal.configuration.metatype.bnd.util.ConfigurableUtil; +import com.liferay.portal.configuration.module.configuration.ConfigurationProvider; import com.liferay.portal.kernel.frontend.hashed.files.HashedFilesRegistry; +import com.liferay.portal.kernel.json.JSONFactory; +import com.liferay.portal.kernel.language.Language; +import com.liferay.portal.kernel.portlet.PortletConfigFactoryUtil; import com.liferay.portal.kernel.service.ThemeLocalService; import com.liferay.portal.kernel.servlet.HttpHeaders; import com.liferay.portal.kernel.util.ContentTypes; @@ -86,21 +92,25 @@ protected void activate(Map properties) { ContentTypes.APPLICATION_JSON, ".map", _hashedFilesRegistry, 86400, "esModulesMaxAge", _portal, false, "sendNoCacheForESModules")); - frontendResourceRequestHandlers.add( - new HashedFileFrontendResourceRequestHandler( - ContentTypes.TEXT_JAVASCRIPT, ".js", _hashedFilesRegistry, - 86400, "esModulesMaxAge", _portal, false, - "sendNoCacheForESModules")); FrontendCachingConfiguration frontendCachingConfiguration = ConfigurableUtil.createConfigurable( FrontendCachingConfiguration.class, properties); + frontendResourceRequestHandlers.add( + new JavaScriptFrontendResourceRequestHandler( + frontendCachingConfiguration, _hashedFilesRegistry, _language, + PortletConfigFactoryUtil.getPortletConfigFactory())); frontendResourceRequestHandlers.add( new StyleSheetFrontendResourceRequestHandler( frontendCachingConfiguration, _hashedFilesRegistry, _portal, _themeLocalService)); + frontendResourceRequestHandlers.add( + new LanguageFrontendResourceRequestHandler( + _configurationProvider, _hashedFilesRegistry, _jsonFactory, + _language, _portal)); + _frontendResourceRequestHandlers.set(frontendResourceRequestHandlers); } @@ -199,6 +209,9 @@ protected void send( } } + @Reference + private ConfigurationProvider _configurationProvider; + private final AtomicReference> _frontendResourceRequestHandlers = new AtomicReference<>( Collections.emptyList()); @@ -206,6 +219,12 @@ protected void send( @Reference private HashedFilesRegistry _hashedFilesRegistry; + @Reference + private JSONFactory _jsonFactory; + + @Reference + private Language _language; + @Reference private Portal _portal; diff --git a/modules/apps/frontend-js/frontend-js-web/src/main/resources/com/liferay/frontend/js/web/internal/servlet/dependencies/all.js.tpl b/modules/apps/frontend-js/frontend-js-web/src/main/resources/com/liferay/frontend/js/web/internal/resource/dependencies/all.js.tpl similarity index 100% rename from modules/apps/frontend-js/frontend-js-web/src/main/resources/com/liferay/frontend/js/web/internal/servlet/dependencies/all.js.tpl rename to modules/apps/frontend-js/frontend-js-web/src/main/resources/com/liferay/frontend/js/web/internal/resource/dependencies/all.js.tpl diff --git a/modules/apps/frontend-js/frontend-js-web/src/test/java/com/liferay/frontend/js/web/internal/resource/handler/JavaScriptFrontendResourceRequestHandlerTest.java b/modules/apps/frontend-js/frontend-js-web/src/test/java/com/liferay/frontend/js/web/internal/resource/handler/JavaScriptFrontendResourceRequestHandlerTest.java new file mode 100644 index 00000000000000..25dbb619bd19c9 --- /dev/null +++ b/modules/apps/frontend-js/frontend-js-web/src/test/java/com/liferay/frontend/js/web/internal/resource/handler/JavaScriptFrontendResourceRequestHandlerTest.java @@ -0,0 +1,323 @@ +/** + * SPDX-FileCopyrightText: (c) 2025 Liferay, Inc. https://liferay.com + * SPDX-License-Identifier: LGPL-2.1-or-later OR LicenseRef-Liferay-DXP-EULA-2.0.0-2023-06 + */ + +package com.liferay.frontend.js.web.internal.resource.handler; + +import com.liferay.frontend.js.web.internal.configuration.FrontendCachingConfiguration; +import com.liferay.frontend.js.web.internal.resource.FrontendResource; +import com.liferay.frontend.js.web.test.util.FrontendJSWebTestUtil; +import com.liferay.petra.io.StreamUtil; +import com.liferay.portal.kernel.frontend.hashed.files.HashedFilesRegistry; +import com.liferay.portal.kernel.language.Language; +import com.liferay.portal.kernel.portlet.PortletConfigFactory; +import com.liferay.portal.kernel.test.util.RandomTestUtil; +import com.liferay.portal.kernel.util.ContentTypes; +import com.liferay.portal.kernel.util.StringUtil; +import com.liferay.portal.language.LanguageResources; +import com.liferay.portal.test.rule.LiferayUnitTestRule; + +import jakarta.servlet.http.HttpServletRequest; + +import java.io.ByteArrayInputStream; + +import java.net.URL; + +import java.nio.charset.StandardCharsets; + +import java.util.ResourceBundle; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.stubbing.Answer; + +import org.springframework.mock.web.MockHttpServletRequest; + +/** + * @author Iván Zaera Avellón + */ +public class JavaScriptFrontendResourceRequestHandlerTest { + + public static final LiferayUnitTestRule liferayUnitTestRule = + LiferayUnitTestRule.INSTANCE; + + @Before + public void setUp() { + _hashedFilePath = StringUtil.replace( + _UNHASHED_FILE_PATH, ".js", ".(" + _HASH + ").js"); + } + + @After + public void tearDown() { + if (_languageResourcesMockedStatic != null) { + _languageResourcesMockedStatic.close(); + + _languageResourcesMockedStatic = null; + } + } + + @Test + public void testCanHandleRequest() throws Exception { + JavaScriptFrontendResourceRequestHandler + javaScriptFrontendResourceRequestHandler = + new JavaScriptFrontendResourceRequestHandler( + _mockFrontendCachingConfiguration(86400, false), + _mockHashedFilesRegistry(true, false), _mockLanguage(), + Mockito.mock(PortletConfigFactory.class)); + + Assert.assertFalse( + javaScriptFrontendResourceRequestHandler.canHandleRequest( + _mockHttpServletRequest("/nonsense/request/main.js", false))); + Assert.assertTrue( + javaScriptFrontendResourceRequestHandler.canHandleRequest( + _mockHttpServletRequest( + "/o/frontend-js-web" + _UNHASHED_FILE_PATH, false))); + Assert.assertTrue( + javaScriptFrontendResourceRequestHandler.canHandleRequest( + _mockHttpServletRequest( + "/o/frontend-js-web" + _hashedFilePath, false))); + } + + @Test + public void testHandleRequestForTranslatableFile() throws Exception { + _mockLanguageResources(); + + long maxAge = RandomTestUtil.randomLong(); + boolean sendNoCache = true; + + JavaScriptFrontendResourceRequestHandler + javaScriptFrontendResourceRequestHandler = + new JavaScriptFrontendResourceRequestHandler( + _mockFrontendCachingConfiguration(maxAge, sendNoCache), + _mockHashedFilesRegistry(true, true), _mockLanguage(), + Mockito.mock(PortletConfigFactory.class)); + + FrontendResource frontendResource = + javaScriptFrontendResourceRequestHandler.handleRequest( + _mockHttpServletRequest( + "/o/frontend-js-web" + _hashedFilePath, true)); + + Assert.assertEquals( + ContentTypes.APPLICATION_JAVASCRIPT, + frontendResource.getContentType()); + Assert.assertNull(frontendResource.getETag()); + Assert.assertEquals( + StringUtil.replace( + _TRANSLATABLE_JS_CONTENT, "Liferay.Language.get('portlet')", + "'Portlet'"), + StreamUtil.toString(frontendResource.getInputStream())); + Assert.assertEquals(maxAge, frontendResource.getMaxAge()); + Assert.assertFalse(frontendResource.isImmutable()); + Assert.assertEquals(sendNoCache, frontendResource.isSendNoCache()); + } + + @Test + public void testHandleRequestWithHash() throws Exception { + JavaScriptFrontendResourceRequestHandler + javaScriptFrontendResourceRequestHandler = + new JavaScriptFrontendResourceRequestHandler( + _mockFrontendCachingConfiguration(86400, true), + _mockHashedFilesRegistry(true, false), _mockLanguage(), + Mockito.mock(PortletConfigFactory.class)); + + FrontendResource frontendResource = + javaScriptFrontendResourceRequestHandler.handleRequest( + _mockHttpServletRequest( + "/o/frontend-js-web" + _hashedFilePath, false)); + + Assert.assertEquals( + ContentTypes.APPLICATION_JAVASCRIPT, + frontendResource.getContentType()); + Assert.assertEquals(_HASH, frontendResource.getETag()); + Assert.assertEquals( + _JS_CONTENT, + StreamUtil.toString(frontendResource.getInputStream())); + Assert.assertEquals(31536000L, frontendResource.getMaxAge()); + Assert.assertTrue(frontendResource.isImmutable()); + Assert.assertFalse(frontendResource.isSendNoCache()); + } + + @Test + public void testHandleRequestWithoutHashForHashedFile() throws Exception { + long maxAge = RandomTestUtil.randomLong(); + boolean sendNoCache = true; + + JavaScriptFrontendResourceRequestHandler + javaScriptFrontendResourceRequestHandler = + new JavaScriptFrontendResourceRequestHandler( + _mockFrontendCachingConfiguration(maxAge, sendNoCache), + _mockHashedFilesRegistry(true, false), _mockLanguage(), + Mockito.mock(PortletConfigFactory.class)); + + FrontendResource frontendResource = + javaScriptFrontendResourceRequestHandler.handleRequest( + _mockHttpServletRequest( + "/o/frontend-js-web" + _UNHASHED_FILE_PATH, false)); + + Assert.assertEquals( + ContentTypes.APPLICATION_JAVASCRIPT, + frontendResource.getContentType()); + Assert.assertEquals(_HASH, frontendResource.getETag()); + Assert.assertEquals( + _JS_CONTENT, + StreamUtil.toString(frontendResource.getInputStream())); + Assert.assertEquals(maxAge, frontendResource.getMaxAge()); + Assert.assertFalse(frontendResource.isImmutable()); + Assert.assertEquals(sendNoCache, frontendResource.isSendNoCache()); + } + + @Test + public void testHandleRequestWithoutHashForUnhashedFile() throws Exception { + long maxAge = RandomTestUtil.randomLong(); + boolean sendNoCache = true; + + JavaScriptFrontendResourceRequestHandler + javaScriptFrontendResourceRequestHandler = + new JavaScriptFrontendResourceRequestHandler( + _mockFrontendCachingConfiguration(maxAge, sendNoCache), + _mockHashedFilesRegistry(false, false), _mockLanguage(), + Mockito.mock(PortletConfigFactory.class)); + + FrontendResource frontendResource = + javaScriptFrontendResourceRequestHandler.handleRequest( + _mockHttpServletRequest( + "/o/frontend-js-web" + _UNHASHED_FILE_PATH, false)); + + Assert.assertEquals( + ContentTypes.APPLICATION_JAVASCRIPT, + frontendResource.getContentType()); + Assert.assertNull(frontendResource.getETag()); + Assert.assertEquals( + _JS_CONTENT, + StreamUtil.toString(frontendResource.getInputStream())); + Assert.assertEquals(maxAge, frontendResource.getMaxAge()); + Assert.assertFalse(frontendResource.isImmutable()); + Assert.assertEquals(sendNoCache, frontendResource.isSendNoCache()); + } + + private FrontendCachingConfiguration _mockFrontendCachingConfiguration( + long esModulesMaxAge, boolean sendNoCacheForESModules) { + + FrontendCachingConfiguration frontendCachingConfiguration = + Mockito.mock(FrontendCachingConfiguration.class); + + Mockito.when( + frontendCachingConfiguration.esModulesMaxAge() + ).thenReturn( + esModulesMaxAge + ); + + Mockito.when( + frontendCachingConfiguration.sendNoCacheForESModules() + ).thenReturn( + sendNoCacheForESModules + ); + + return frontendCachingConfiguration; + } + + private HashedFilesRegistry _mockHashedFilesRegistry( + boolean hashedFile, boolean translatableFile) + throws Exception { + + HashedFilesRegistry hashedFilesRegistry = Mockito.mock( + HashedFilesRegistry.class); + + if (hashedFile) { + Mockito.when( + hashedFilesRegistry.getHashedFileURI( + Mockito.eq("/o/frontend-js-web" + _UNHASHED_FILE_PATH)) + ).thenReturn( + "/o/frontend-js-web" + _hashedFilePath + ); + } + + URL url = Mockito.mock(URL.class); + + String content = + translatableFile ? _TRANSLATABLE_JS_CONTENT : _JS_CONTENT; + + Mockito.when( + url.openStream() + ).thenReturn( + new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)) + ); + + Mockito.when( + hashedFilesRegistry.getResource( + Mockito.eq( + "/o/frontend-js-web" + + (hashedFile ? _hashedFilePath : _UNHASHED_FILE_PATH))) + ).thenReturn( + url + ); + + return hashedFilesRegistry; + } + + private HttpServletRequest _mockHttpServletRequest( + String requestURI, boolean translatableRequest) { + + MockHttpServletRequest mockHttpServletRequest = + new MockHttpServletRequest(); + + mockHttpServletRequest.setRequestURI(requestURI); + + if (translatableRequest) { + mockHttpServletRequest.addParameter("languageId", "en_US"); + mockHttpServletRequest.addParameter("translate", "true"); + } + + return mockHttpServletRequest; + } + + private Language _mockLanguage() { + Language language = Mockito.mock(Language.class); + + Mockito.when( + language.process(Mockito.any(), Mockito.any(), Mockito.any()) + ).thenAnswer( + (Answer)invocationOnMock -> { + String content = invocationOnMock.getArgument(2); + + return StringUtil.replace( + content, "Liferay.Language.get('portlet')", "'Portlet'"); + } + ); + + return language; + } + + private void _mockLanguageResources() { + _languageResourcesMockedStatic = Mockito.mockStatic( + LanguageResources.class); + + ResourceBundle resourceBundle = Mockito.mock(ResourceBundle.class); + + _languageResourcesMockedStatic.when( + () -> LanguageResources.getResourceBundle(Mockito.any()) + ).thenReturn( + resourceBundle + ); + } + + private static final String _HASH = + FrontendJSWebTestUtil.randomHashedFileHash(); + + private static final String _JS_CONTENT = "function x(){return 'Portlet';}"; + + private static final String _TRANSLATABLE_JS_CONTENT = + "function x(){return Liferay.Language.get('portlet');}"; + + private static final String _UNHASHED_FILE_PATH = "/js/main.js"; + + private String _hashedFilePath; + private MockedStatic _languageResourcesMockedStatic; + +} \ No newline at end of file diff --git a/modules/apps/frontend-js/frontend-js-web/src/test/java/com/liferay/frontend/js/web/internal/resource/handler/LanguageFrontendResourceRequestHandlerTest.java b/modules/apps/frontend-js/frontend-js-web/src/test/java/com/liferay/frontend/js/web/internal/resource/handler/LanguageFrontendResourceRequestHandlerTest.java new file mode 100644 index 00000000000000..f91f6653940caf --- /dev/null +++ b/modules/apps/frontend-js/frontend-js-web/src/test/java/com/liferay/frontend/js/web/internal/resource/handler/LanguageFrontendResourceRequestHandlerTest.java @@ -0,0 +1,220 @@ +/** + * SPDX-FileCopyrightText: (c) 2025 Liferay, Inc. https://liferay.com + * SPDX-License-Identifier: LGPL-2.1-or-later OR LicenseRef-Liferay-DXP-EULA-2.0.0-2023-06 + */ + +package com.liferay.frontend.js.web.internal.resource.handler; + +import com.liferay.frontend.js.web.internal.configuration.FrontendCachingConfiguration; +import com.liferay.frontend.js.web.internal.resource.FrontendResource; +import com.liferay.petra.io.StreamUtil; +import com.liferay.portal.configuration.module.configuration.ConfigurationProvider; +import com.liferay.portal.json.JSONFactoryImpl; +import com.liferay.portal.kernel.frontend.hashed.files.HashedFilesRegistry; +import com.liferay.portal.kernel.language.Language; +import com.liferay.portal.kernel.language.LanguageUtil; +import com.liferay.portal.kernel.util.ContentTypes; +import com.liferay.portal.kernel.util.LocaleUtil; +import com.liferay.portal.kernel.util.Portal; +import com.liferay.portal.test.rule.LiferayUnitTestRule; + +import jakarta.servlet.http.HttpServletRequest; + +import java.io.ByteArrayInputStream; + +import java.net.URL; + +import java.nio.charset.StandardCharsets; + +import java.util.Locale; + +import org.junit.Assert; +import org.junit.Test; + +import org.mockito.Mockito; +import org.mockito.stubbing.Answer; + +import org.springframework.mock.web.MockHttpServletRequest; + +/** + * @author Iván Zaera Avellón + */ +public class LanguageFrontendResourceRequestHandlerTest { + + public static final LiferayUnitTestRule liferayUnitTestRule = + LiferayUnitTestRule.INSTANCE; + + @Test + public void testCanHandleRequest() throws Exception { + LanguageFrontendResourceRequestHandler + languageFrontendResourceRequestHandler = + new LanguageFrontendResourceRequestHandler( + _mockConfigurationProvider(1234L, false), + _mockHashedFilesRegistry(), new JSONFactoryImpl(), + _mockLanguage(), _mockPortal()); + + Assert.assertTrue( + languageFrontendResourceRequestHandler.canHandleRequest( + _mockHttpServletRequest( + "/o/js/language/en_US/frontend-js-web/all.js"))); + + Assert.assertFalse( + languageFrontendResourceRequestHandler.canHandleRequest( + _mockHttpServletRequest("/nonsense/request/index.js"))); + } + + @Test + public void testConfiguration() throws Exception { + LanguageFrontendResourceRequestHandler + languageFrontendResourceRequestHandler = + new LanguageFrontendResourceRequestHandler( + _mockConfigurationProvider(4321L, true), + _mockHashedFilesRegistry(), new JSONFactoryImpl(), + _mockLanguage(), _mockPortal()); + + FrontendResource frontendResource = + languageFrontendResourceRequestHandler.handleRequest( + _mockHttpServletRequest( + "/o/js/language/en_US/frontend-js-web/all.js")); + + Assert.assertNotNull(frontendResource); + + Assert.assertEquals(4321L, frontendResource.getMaxAge()); + + Assert.assertTrue(frontendResource.isSendNoCache()); + } + + @Test + public void testHandleRequest() throws Exception { + LanguageFrontendResourceRequestHandler + languageFrontendResourceRequestHandler = + new LanguageFrontendResourceRequestHandler( + _mockConfigurationProvider(1234L, false), + _mockHashedFilesRegistry(), new JSONFactoryImpl(), + _mockLanguage(), _mockPortal()); + + FrontendResource frontendResource = + languageFrontendResourceRequestHandler.handleRequest( + _mockHttpServletRequest( + "/o/js/language/en_US/frontend-js-web/all.js")); + + Assert.assertNotNull(frontendResource); + + Assert.assertEquals( + ContentTypes.TEXT_JAVASCRIPT, frontendResource.getContentType()); + + Assert.assertNull(frontendResource.getETag()); + + Assert.assertEquals(1234L, frontendResource.getMaxAge()); + + Assert.assertFalse(frontendResource.isImmutable()); + + Assert.assertFalse(frontendResource.isSendNoCache()); + + String content = StreamUtil.toString(frontendResource.getInputStream()); + + Assert.assertTrue(content.contains("'a-key':'a-key',")); + } + + private ConfigurationProvider _mockConfigurationProvider( + long maxAge, boolean sendNoCache) + throws Exception { + + ConfigurationProvider configurationProvider = Mockito.mock( + ConfigurationProvider.class); + + FrontendCachingConfiguration frontendCachingConfiguration = + Mockito.mock(FrontendCachingConfiguration.class); + + Mockito.when( + frontendCachingConfiguration.labelsModulesMaxAge() + ).thenReturn( + maxAge + ); + + Mockito.when( + frontendCachingConfiguration.sendNoCacheForLabelsModules() + ).thenReturn( + sendNoCache + ); + + Mockito.when( + configurationProvider.getCompanyConfiguration( + FrontendCachingConfiguration.class, _COMPANY_ID) + ).thenReturn( + frontendCachingConfiguration + ); + + return configurationProvider; + } + + private HashedFilesRegistry _mockHashedFilesRegistry() throws Exception { + HashedFilesRegistry hashedFilesRegistry = Mockito.mock( + HashedFilesRegistry.class); + + URL url = Mockito.mock(URL.class); + + Mockito.when( + url.openStream() + ).thenReturn( + new ByteArrayInputStream( + "{keys:['a-key']}".getBytes(StandardCharsets.UTF_8)) + ); + + Mockito.when( + hashedFilesRegistry.getResource(Mockito.anyString()) + ).thenReturn( + url + ); + + return hashedFilesRegistry; + } + + private HttpServletRequest _mockHttpServletRequest(String requestURI) { + MockHttpServletRequest mockHttpServletRequest = + new MockHttpServletRequest(); + + mockHttpServletRequest.setRequestURI(requestURI); + + return mockHttpServletRequest; + } + + private Language _mockLanguage() { + Language language = Mockito.mock(Language.class); + + Mockito.when( + language.isAvailableLocale(LocaleUtil.ENGLISH) + ).thenReturn( + true + ); + + Mockito.when( + language.get(Mockito.any(Locale.class), Mockito.anyString()) + ).thenAnswer( + (Answer)invocationOnMock -> invocationOnMock.getArgument( + 1, String.class) + ); + + new LanguageUtil( + ).setLanguage( + language + ); + + return language; + } + + private Portal _mockPortal() { + Portal portal = Mockito.mock(Portal.class); + + Mockito.when( + portal.getCompanyId(Mockito.any(HttpServletRequest.class)) + ).thenReturn( + _COMPANY_ID + ); + + return portal; + } + + private static final long _COMPANY_ID = 1L; + +} \ No newline at end of file diff --git a/modules/apps/frontend-js/frontend-js-web/src/test/java/com/liferay/frontend/js/web/internal/resource/handler/StyleSheetFrontendResourceRequestHandlerTest.java b/modules/apps/frontend-js/frontend-js-web/src/test/java/com/liferay/frontend/js/web/internal/resource/handler/StyleSheetFrontendResourceRequestHandlerTest.java index 5a7a960f4440b1..b13fe0ac13eeca 100644 --- a/modules/apps/frontend-js/frontend-js-web/src/test/java/com/liferay/frontend/js/web/internal/resource/handler/StyleSheetFrontendResourceRequestHandlerTest.java +++ b/modules/apps/frontend-js/frontend-js-web/src/test/java/com/liferay/frontend/js/web/internal/resource/handler/StyleSheetFrontendResourceRequestHandlerTest.java @@ -29,7 +29,6 @@ import org.junit.Assert; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.mockito.Mockito; @@ -50,12 +49,8 @@ public void setUp() { _UNHASHED_FILE_PATH, ".css", ".(" + _HASH + ").css"); } - @Ignore @Test public void testCanHandleRequest() throws Exception { - - // LPD-57326 - StyleSheetFrontendResourceRequestHandler styleSheetFrontendResourceRequestHandler = new StyleSheetFrontendResourceRequestHandler( diff --git a/modules/apps/portal-language/portal-language-lang/src/main/resources/content/Language.properties b/modules/apps/portal-language/portal-language-lang/src/main/resources/content/Language.properties index 6a938b8763ae07..e4e90929342c69 100644 --- a/modules/apps/portal-language/portal-language-lang/src/main/resources/content/Language.properties +++ b/modules/apps/portal-language/portal-language-lang/src/main/resources/content/Language.properties @@ -10454,6 +10454,8 @@ label-text=Label Text label-x-was-added-to-the-list=Label {0} was added to the list. label-x-was-removed-from-the-list=Label {0} was removed from the list. labels=Labels +labels-modules-max-age=Labels Modules Max Age Directive Value +labels-modules-max-age-help=Set the value of the "max-age" directive in the "Cache-Control" header for labels modules. landscape-phone=Landscape Phone lang.dir=ltr lang.line.begin=left @@ -17189,6 +17191,8 @@ send-no-cache-for-css-style-sheets=Send No Cache Directive For CSS Style Sheets send-no-cache-for-css-style-sheets-help=If checked, a "no-cache" directive (instead of "must-revalidate") will be sent in the "Cache-Control" header for CSS style sheets. send-no-cache-for-es-modules=Send No Cache Directive For ES Modules send-no-cache-for-es-modules-help=If checked, a "no-cache" directive (instead of "must-revalidate") will be sent in the "Cache-Control" header for ES modules. +send-no-cache-for-labels-modules=Send No Cache Directive For Labels Modules +send-no-cache-for-labels-modules-help=If checked, a "no-cache" directive (instead of "must-revalidate") will be sent in the "Cache-Control" header for labels modules. send-notifications-to-blogs-entry-creator=Send Notifications to Blogs Entry Creator send-order-email=Send Order Email send-order-email-to-users=Send order email to users? diff --git a/modules/sdk/gradle-plugins-workspace/src/main/resources/schemas/client-extension-type-instance-settings.schema.json b/modules/sdk/gradle-plugins-workspace/src/main/resources/schemas/client-extension-type-instance-settings.schema.json index a4f16c6fab2add..8af18851965280 100644 --- a/modules/sdk/gradle-plugins-workspace/src/main/resources/schemas/client-extension-type-instance-settings.schema.json +++ b/modules/sdk/gradle-plugins-workspace/src/main/resources/schemas/client-extension-type-instance-settings.schema.json @@ -2008,6 +2008,12 @@ "title": "ES Modules Max Age Directive Value", "type": "number" }, + "labelsModulesMaxAge": { + "default": 3600, + "description": "Sets the value that will be sent to the browser as max-age directive in Cache-Control header for modules that contain translation labels.", + "title": "Labels Modules max-age Directive Value", + "type": "number" + }, "pid": { "const": "com.liferay.frontend.js.web.internal.configuration.FrontendCachingConfiguration", "title": "Frontend Caching" @@ -2023,6 +2029,12 @@ "description": "If checked, a \"no-cache\" directive (instead of \"must-revalidate\") will be sent in the \"Cache-Control\" header for ES modules.", "title": "Send No Cache Directive For ES Modules", "type": "boolean" + }, + "sendNoCacheForLabelsModules": { + "default": false, + "description": "Send no-cache (instead of must-revalidate) directive in Cache-Control header for modules that contain translation labels.", + "title": "Send no-cache Directive For Labels Modules", + "type": "boolean" } }, "required": [ diff --git a/portal-impl/src/com/liferay/portal/language/LanguageImpl.java b/portal-impl/src/com/liferay/portal/language/LanguageImpl.java index 4dfbd0c4e3ba10..acf45454dd490c 100644 --- a/portal-impl/src/com/liferay/portal/language/LanguageImpl.java +++ b/portal-impl/src/com/liferay/portal/language/LanguageImpl.java @@ -17,7 +17,6 @@ import com.liferay.portal.kernel.cookies.constants.CookiesConstants; import com.liferay.portal.kernel.exception.PortalException; import com.liferay.portal.kernel.exception.SystemException; -import com.liferay.portal.kernel.feature.flag.FeatureFlagManagerUtil; import com.liferay.portal.kernel.language.Language; import com.liferay.portal.kernel.language.LanguageWrapper; import com.liferay.portal.kernel.log.Log; @@ -1646,19 +1645,6 @@ public String process( Supplier resourceBundleSupplier, Locale locale, String content) { - if (FeatureFlagManagerUtil.isEnabled("LPD-11848")) { - Matcher matcher = _liferayLanguageImportPattern.matcher(content); - - if (matcher.find()) { - return content; - } - } - else { - content = content.replaceAll( - _LIFERAY_LANGUAGE_IMPORT_REGEXP, - "{/*removed: await import('@liferay/language...')*/}"); - } - StringBundler sb = null; ResourceBundle resourceBundle = null; @@ -2029,9 +2015,6 @@ private void _updateLastModified() { private static final String _GROUP_LOCALES_PORTAL_CACHE_NAME = LanguageImpl.class.getName() + "._groupLocalesPortalCache"; - private static final String _LIFERAY_LANGUAGE_IMPORT_REGEXP = - "await import\\(.@liferay/language/.+?/all\\.js.\\)"; - private static final double _STORAGE_SIZE_DENOMINATOR = 1024.0; private static final Log _log = LogFactoryUtil.getLog(LanguageImpl.class); @@ -2041,8 +2024,6 @@ private void _updateLastModified() { private static PortalCache _companyLocalesPortalCache; private static PortalCache _groupLocalesPortalCache; private static volatile long _lastModified = System.currentTimeMillis(); - private static final Pattern _liferayLanguageImportPattern = - Pattern.compile(_LIFERAY_LANGUAGE_IMPORT_REGEXP, Pattern.MULTILINE); private static final Pattern _pattern = Pattern.compile( "Liferay\\s*\\.\\s*Language\\s*\\.\\s*get\\s*" + "\\(\\s*[\"']([^)]+)[\"']\\s*\\)", diff --git a/portal-impl/src/com/liferay/portal/servlet/filters/language/LanguageFilter.java b/portal-impl/src/com/liferay/portal/servlet/filters/language/LanguageFilter.java index bc498ed47913d0..00501a3f8e8380 100644 --- a/portal-impl/src/com/liferay/portal/servlet/filters/language/LanguageFilter.java +++ b/portal-impl/src/com/liferay/portal/servlet/filters/language/LanguageFilter.java @@ -7,6 +7,7 @@ import com.liferay.petra.string.CharPool; import com.liferay.petra.string.StringPool; +import com.liferay.portal.kernel.frontend.esm.FrontendESMUtil; import com.liferay.portal.kernel.language.LanguageUtil; import com.liferay.portal.kernel.log.Log; import com.liferay.portal.kernel.log.LogFactoryUtil; @@ -48,6 +49,20 @@ public void init(FilterConfig filterConfig) { super.init(filterConfig); } + @Override + public boolean isFilterEnabled( + HttpServletRequest httpServletRequest, + HttpServletResponse httpServletResponse) { + + if (FrontendESMUtil.isInternalESMRequest( + httpServletRequest.getRequestURI())) { + + return false; + } + + return super.isFilterEnabled(httpServletRequest, httpServletResponse); + } + @Override protected void processFilter( HttpServletRequest httpServletRequest, diff --git a/portal-impl/src/portal-osgi-configuration.properties b/portal-impl/src/portal-osgi-configuration.properties index a4f34807b88af3..40a6b9489345fe 100644 --- a/portal-impl/src/portal-osgi-configuration.properties +++ b/portal-impl/src/portal-osgi-configuration.properties @@ -2727,6 +2727,10 @@ # #configuration.override.com.liferay.frontend.js.web.internal.configuration.FrontendCachingConfiguration_esModulesMaxAge= # + # Env: LIFERAY_CONFIGURATION_PERIOD_OVERRIDE_PERIOD_COM_PERIOD_LIFERAY_PERIOD_FRONTEND_PERIOD_JS_PERIOD_WEB_PERIOD_INTERNAL_PERIOD_CONFIGURATION_PERIOD__UPPERCASEF_RONTEND_UPPERCASEC_ACHING_UPPERCASEC_ONFIGURATION_UNDERLINE_LABELS_UPPERCASEM_ODULES_UPPERCASEM_AX_UPPERCASEA_GE + # + #configuration.override.com.liferay.frontend.js.web.internal.configuration.FrontendCachingConfiguration_labelsModulesMaxAge= + # # Env: LIFERAY_CONFIGURATION_PERIOD_OVERRIDE_PERIOD_COM_PERIOD_LIFERAY_PERIOD_FRONTEND_PERIOD_JS_PERIOD_WEB_PERIOD_INTERNAL_PERIOD_CONFIGURATION_PERIOD__UPPERCASEF_RONTEND_UPPERCASEC_ACHING_UPPERCASEC_ONFIGURATION_UNDERLINE_SEND_UPPERCASEN_O_UPPERCASEC_ACHE_UPPERCASEF_OR_UPPERCASEC__UPPERCASES__UPPERCASES__UPPERCASES_TYLE_UPPERCASES_HEETS # #configuration.override.com.liferay.frontend.js.web.internal.configuration.FrontendCachingConfiguration_sendNoCacheForCSSStyleSheets= @@ -2735,6 +2739,10 @@ # #configuration.override.com.liferay.frontend.js.web.internal.configuration.FrontendCachingConfiguration_sendNoCacheForESModules= # + # Env: LIFERAY_CONFIGURATION_PERIOD_OVERRIDE_PERIOD_COM_PERIOD_LIFERAY_PERIOD_FRONTEND_PERIOD_JS_PERIOD_WEB_PERIOD_INTERNAL_PERIOD_CONFIGURATION_PERIOD__UPPERCASEF_RONTEND_UPPERCASEC_ACHING_UPPERCASEC_ONFIGURATION_UNDERLINE_SEND_UPPERCASEN_O_UPPERCASEC_ACHE_UPPERCASEF_OR_UPPERCASEL_ABELS_UPPERCASEM_ODULES + # + #configuration.override.com.liferay.frontend.js.web.internal.configuration.FrontendCachingConfiguration_sendNoCacheForLabelsModules= + # # Env: LIFERAY_CONFIGURATION_PERIOD_OVERRIDE_PERIOD_COM_PERIOD_LIFERAY_PERIOD_FRONTEND_PERIOD_JS_PERIOD_WEB_PERIOD_INTERNAL_PERIOD_SESSION_PERIOD_TIMEOUT_PERIOD_CONFIGURATION_PERIOD__UPPERCASES_ESSION_UPPERCASET_IMEOUT_UPPERCASEC_ONFIGURATION_UNDERLINE_AUTO_UPPERCASEE_XTEND # #configuration.override.com.liferay.frontend.js.web.internal.session.timeout.configuration.SessionTimeoutConfiguration_autoExtend= diff --git a/portal-impl/src/portal.properties b/portal-impl/src/portal.properties index e239f503bc27a4..c1448fffd08847 100644 --- a/portal-impl/src/portal.properties +++ b/portal-impl/src/portal.properties @@ -6521,11 +6521,6 @@ # feature.flag.LPD-11342=false - # - # Env: LIFERAY_FEATURE_PERIOD_FLAG_PERIOD__UPPERCASEL__UPPERCASEP__UPPERCASED__MINUS__NUMBER1__NUMBER1__NUMBER8__NUMBER4__NUMBER8_ - # - feature.flag.LPD-11848=false - # # Env: LIFERAY_FEATURE_PERIOD_FLAG_PERIOD__UPPERCASEL__UPPERCASEP__UPPERCASED__MINUS__NUMBER1__NUMBER3__NUMBER3__NUMBER1__NUMBER1_ # diff --git a/portal-impl/test/unit/com/liferay/portal/language/LanguageImplTest.java b/portal-impl/test/unit/com/liferay/portal/language/LanguageImplTest.java deleted file mode 100644 index c1bec1cb7ff704..00000000000000 --- a/portal-impl/test/unit/com/liferay/portal/language/LanguageImplTest.java +++ /dev/null @@ -1,46 +0,0 @@ -/** - * SPDX-FileCopyrightText: (c) 2024 Liferay, Inc. https://liferay.com - * SPDX-License-Identifier: LGPL-2.1-or-later OR LicenseRef-Liferay-DXP-EULA-2.0.0-2023-06 - */ - -package com.liferay.portal.language; - -import com.liferay.portal.kernel.test.rule.AggregateTestRule; -import com.liferay.portal.test.rule.LiferayUnitTestRule; - -import org.junit.Assert; -import org.junit.ClassRule; -import org.junit.Rule; -import org.junit.Test; - -/** - * @author Bryce Osterhaus - */ -public class LanguageImplTest { - - @ClassRule - @Rule - public static final AggregateTestRule aggregateTestRule = - LiferayUnitTestRule.INSTANCE; - - @Test - public void testProcess() { - LanguageImpl languageImpl = new LanguageImpl(); - - Assert.assertEquals( - "foo;bar;{/*removed: await import('@liferay/language...')*/};baz;", - languageImpl.process( - null, null, - "foo;bar;await import('@liferay/language/foo/all.js');baz;" - ).toString()); - Assert.assertEquals( - "foo;{/*removed: await import('@liferay/language...')*/};bar;" + - "{/*removed: await import('@liferay/language...')*/};baz;", - languageImpl.process( - null, null, - "foo;await import('@liferay/language/foo/all.js');bar;" + - "await import('@liferay/language/foo/all.js');baz;" - ).toString()); - } - -} \ No newline at end of file diff --git a/portal-kernel/src/com/liferay/portal/kernel/frontend/esm/FrontendESMUtil.java b/portal-kernel/src/com/liferay/portal/kernel/frontend/esm/FrontendESMUtil.java index 788e0cef0a1bdd..8948ac91b87cce 100644 --- a/portal-kernel/src/com/liferay/portal/kernel/frontend/esm/FrontendESMUtil.java +++ b/portal-kernel/src/com/liferay/portal/kernel/frontend/esm/FrontendESMUtil.java @@ -37,4 +37,14 @@ public static String buildURL( submodule, ".js"); } + public static boolean isInternalESMRequest(String requestURI) { + if (requestURI.contains("/__liferay__/") || + requestURI.startsWith("/o/js/language/")) { + + return true; + } + + return false; + } + } \ No newline at end of file diff --git a/portal-kernel/src/com/liferay/portal/kernel/frontend/esm/packageinfo b/portal-kernel/src/com/liferay/portal/kernel/frontend/esm/packageinfo index 682b435622a0aa..2c9afe82e39e46 100644 --- a/portal-kernel/src/com/liferay/portal/kernel/frontend/esm/packageinfo +++ b/portal-kernel/src/com/liferay/portal/kernel/frontend/esm/packageinfo @@ -1 +1 @@ -version 2.0.0 \ No newline at end of file +version 2.1.0 \ No newline at end of file diff --git a/portal-kernel/src/com/liferay/portal/kernel/portlet/render/PortletRenderUtil.java b/portal-kernel/src/com/liferay/portal/kernel/portlet/render/PortletRenderUtil.java index 0c63a04c8e3b92..ae97c0a3944c0e 100644 --- a/portal-kernel/src/com/liferay/portal/kernel/portlet/render/PortletRenderUtil.java +++ b/portal-kernel/src/com/liferay/portal/kernel/portlet/render/PortletRenderUtil.java @@ -21,7 +21,6 @@ import com.liferay.portal.kernel.util.HttpComponentsUtil; import com.liferay.portal.kernel.util.ListUtil; import com.liferay.portal.kernel.util.PortalUtil; -import com.liferay.portal.kernel.util.SetUtil; import com.liferay.portal.kernel.util.StringUtil; import com.liferay.portal.kernel.util.Validator; import com.liferay.portal.kernel.util.WebKeys; @@ -240,7 +239,14 @@ private static String _getStaticCSSResourceURL( HttpServletRequest httpServletRequest, String originalURL, ThemeDisplay themeDisplay) { - String url = null; + String queryString = HttpComponentsUtil.getQueryString(originalURL); + + if (!Validator.isBlank(queryString)) { + originalURL = originalURL.substring( + 0, originalURL.length() - queryString.length() - 1); + } + + String url; String proxyPath = PortalUtil.getPathProxy(); @@ -268,6 +274,10 @@ private static String _getStaticCSSResourceURL( } } + if (!Validator.isBlank(queryString)) { + url += "?" + queryString; + } + if (_isTokenized(unhashedFileURI)) { url = HttpComponentsUtil.addParameter( url, "themeId", themeDisplay.getThemeId()); @@ -277,6 +287,45 @@ private static String _getStaticCSSResourceURL( return url; } + private static String _getStaticJSResourceURL( + String originalURL, ThemeDisplay themeDisplay) { + + String queryString = HttpComponentsUtil.getQueryString(originalURL); + + if (!Validator.isBlank(queryString)) { + originalURL = originalURL.substring( + 0, originalURL.length() - queryString.length() - 1); + } + + String url; + + String proxyPath = PortalUtil.getPathProxy(); + + String unhashedFileURI = originalURL.substring(proxyPath.length()); + + String hashedFileURI = HashedFilesRegistryUtil.getHashedFileURI( + unhashedFileURI); + + if (hashedFileURI == null) { + url = originalURL; + } + else { + url = proxyPath + hashedFileURI; + } + + if (!Validator.isBlank(queryString)) { + url += "?" + queryString; + } + + if (_isTranslatable(unhashedFileURI)) { + url = HttpComponentsUtil.addParameter( + url, "languageId", themeDisplay.getLanguageId()); + url = HttpComponentsUtil.addParameter(url, "translate", true); + } + + return url; + } + private static List _getStaticURLs( HttpServletRequest httpServletRequest, Collection portletResourceAccessors, @@ -309,15 +358,10 @@ private static List _getStaticURLs( for (String portletResource : portletResources) { String prefix = null; - for (String specialProtocol : _specialPrefixes) { - if (portletResource.startsWith(specialProtocol)) { - portletResource = portletResource.substring( - specialProtocol.length()); + if (portletResource.startsWith("module:")) { + portletResource = portletResource.substring(7); - prefix = specialProtocol; - - break; - } + prefix = "module:"; } if (!HttpComponentsUtil.hasProtocol(portletResource)) { @@ -327,15 +371,8 @@ private static List _getStaticURLs( contextPath + portletResource, themeDisplay); } else if (urlType == URLType.JAVASCRIPT) { - portletResource = contextPath + portletResource; - - String hashedFileURI = - HashedFilesRegistryUtil.getHashedFileURI( - portletResource); - - if (hashedFileURI != null) { - portletResource = hashedFileURI; - } + portletResource = _getStaticJSResourceURL( + contextPath + portletResource, themeDisplay); } else { throw new UnsupportedOperationException( @@ -394,14 +431,6 @@ private static Collection _getURLs( "Unsupported URL type " + urlType); } - for (int i = 0; i < urls.size(); i++) { - String url = urls.get(i); - - if (url.startsWith("nocombo:")) { - urls.set(i, url.substring(8)); - } - } - return urls; } @@ -437,6 +466,35 @@ private static boolean _isTokenized(String resourceURI) { return _tokenized.get(resourceURI); } + private static boolean _isTranslatable(String resourceURI) { + if (!_translatable.containsKey(resourceURI)) { + URL resourceURL = HashedFilesRegistryUtil.getResource(resourceURI); + + String content; + + try { + content = StreamUtil.toString(resourceURL.openStream()); + } + catch (Exception exception) { + if (_log.isDebugEnabled()) { + _log.debug( + "Assuming " + resourceURI + + " is not translatable because it could not be read", + exception); + } + + _translatable.putIfAbsent(resourceURI, false); + + return false; + } + + _translatable.putIfAbsent( + resourceURI, content.contains("Liferay.Language.get")); + } + + return _translatable.get(resourceURI); + } + private static void _writeCSSPath( PrintWriter printWriter, String cssPath, Map attributes) { @@ -599,10 +657,10 @@ public Collection get(Portlet portlet) { private static final Log _log = LogFactoryUtil.getLog( PortletRenderUtil.class); - private static final Set _specialPrefixes = SetUtil.fromArray( - "module:", "nocombo:"); private static final Map _tokenized = new ConcurrentHashMap<>(); + private static final Map _translatable = + new ConcurrentHashMap<>(); private enum URLType { diff --git a/portal-kernel/test/unit/com/liferay/portal/kernel/portlet/render/PortletRenderUtilTest.java b/portal-kernel/test/unit/com/liferay/portal/kernel/portlet/render/PortletRenderUtilTest.java index 2d51d75392a81e..710846008c2d79 100644 --- a/portal-kernel/test/unit/com/liferay/portal/kernel/portlet/render/PortletRenderUtilTest.java +++ b/portal-kernel/test/unit/com/liferay/portal/kernel/portlet/render/PortletRenderUtilTest.java @@ -34,7 +34,6 @@ import org.mockito.MockedStatic; import org.mockito.Mockito; -import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.springframework.mock.web.MockHttpServletRequest; @@ -68,20 +67,20 @@ public void testGetPortletRenderParts() throws Exception { "/header-portal.(" + _HASH + ").css", "/header-portal.tokenized.(" + _HASH + ").css?themeId=classic_WAR_classictheme&tokenize=true", - "/nocombo-header-portal.(" + _HASH + ").css", "/o/portlet-web/header-portlet.(" + _HASH + ").css", "/o/portlet-web/header-portlet.tokenized.(" + _HASH + ").css?themeId=classic_WAR_classictheme&tokenize=true", - "/o/portlet-web/nocombo-header-portlet.(" + _HASH + ").css", "http://example.com/header-portal.css", "http://example.com/header-portlet.css"), portletRenderParts.getHeaderCssPaths()); _assertEquals( Arrays.asList( "/header-portal.(" + _HASH + ").js", - "/nocombo-header-portal.(" + _HASH + ").js", + "/header-portal.translatable.(" + _HASH + + ").js?languageId=en_US&translate=true", "/o/portlet-web/header-portlet.(" + _HASH + ").js", - "/o/portlet-web/nocombo-header-portlet.(" + _HASH + ").js", + "/o/portlet-web/header-portlet.translatable.(" + _HASH + + ").js?languageId=en_US&translate=true", "http://example.com/header-portal.js", "http://example.com/header-portlet.js", "module:/module-header-portal.(" + _HASH + ").js", @@ -93,9 +92,11 @@ public void testGetPortletRenderParts() throws Exception { _assertEquals( Arrays.asList( "/footer-portal.(" + _HASH + ").js", - "/nocombo-footer-portal.(" + _HASH + ").js", + "/footer-portal.translatable.(" + _HASH + + ").js?languageId=en_US&translate=true", "/o/portlet-web/footer-portlet.(" + _HASH + ").js", - "/o/portlet-web/nocombo-footer-portlet.(" + _HASH + ").js", + "/o/portlet-web/footer-portlet.translatable.(" + _HASH + + ").js?languageId=en_US&translate=true", "http://example.com/footer-portal.js", "http://example.com/footer-portlet.js", "module:/module-footer-portal.(" + _HASH + ").js", @@ -109,11 +110,9 @@ public void testGetPortletRenderParts() throws Exception { "/footer-portal.(" + _HASH + ").css", "/footer-portal.tokenized.(" + _HASH + ").css?themeId=classic_WAR_classictheme&tokenize=true", - "/nocombo-footer-portal.(" + _HASH + ").css", "/o/portlet-web/footer-portlet.(" + _HASH + ").css", "/o/portlet-web/footer-portlet.tokenized.(" + _HASH + ").css?themeId=classic_WAR_classictheme&tokenize=true", - "/o/portlet-web/nocombo-footer-portlet.(" + _HASH + ").css", "http://example.com/footer-portal.css", "http://example.com/footer-portlet.css"), portletRenderParts.getFooterCssPaths()); @@ -137,22 +136,20 @@ public void testGetPortletRenderPartsWithContext() throws Exception { "/portal/header-portal.(" + _HASH + ").css", "/portal/header-portal.tokenized.(" + _HASH + ").css?themeId=classic_WAR_classictheme&tokenize=true", - "/portal/nocombo-header-portal.(" + _HASH + ").css", "/portal/o/portlet-web/header-portlet.(" + _HASH + ").css", "/portal/o/portlet-web/header-portlet.tokenized.(" + _HASH + ").css?themeId=classic_WAR_classictheme&tokenize=true", - "/portal/o/portlet-web/nocombo-header-portlet.(" + _HASH + - ").css", "http://example.com/header-portal.css", "http://example.com/header-portlet.css"), portletRenderParts.getHeaderCssPaths()); _assertEquals( Arrays.asList( "/portal/header-portal.(" + _HASH + ").js", - "/portal/nocombo-header-portal.(" + _HASH + ").js", + "/portal/header-portal.translatable.(" + _HASH + + ").js?languageId=en_US&translate=true", "/portal/o/portlet-web/header-portlet.(" + _HASH + ").js", - "/portal/o/portlet-web/nocombo-header-portlet.(" + _HASH + - ").js", + "/portal/o/portlet-web/header-portlet.translatable.(" + _HASH + + ").js?languageId=en_US&translate=true", "http://example.com/header-portal.js", "http://example.com/header-portlet.js", "module:/portal/module-header-portal.(" + _HASH + ").js", @@ -166,22 +163,20 @@ public void testGetPortletRenderPartsWithContext() throws Exception { "/portal/footer-portal.(" + _HASH + ").css", "/portal/footer-portal.tokenized.(" + _HASH + ").css?themeId=classic_WAR_classictheme&tokenize=true", - "/portal/nocombo-footer-portal.(" + _HASH + ").css", "/portal/o/portlet-web/footer-portlet.(" + _HASH + ").css", "/portal/o/portlet-web/footer-portlet.tokenized.(" + _HASH + ").css?themeId=classic_WAR_classictheme&tokenize=true", - "/portal/o/portlet-web/nocombo-footer-portlet.(" + _HASH + - ").css", "http://example.com/footer-portal.css", "http://example.com/footer-portlet.css"), portletRenderParts.getFooterCssPaths()); _assertEquals( Arrays.asList( "/portal/footer-portal.(" + _HASH + ").js", - "/portal/nocombo-footer-portal.(" + _HASH + ").js", + "/portal/footer-portal.translatable.(" + _HASH + + ").js?languageId=en_US&translate=true", "/portal/o/portlet-web/footer-portlet.(" + _HASH + ").js", - "/portal/o/portlet-web/nocombo-footer-portlet.(" + _HASH + - ").js", + "/portal/o/portlet-web/footer-portlet.translatable.(" + _HASH + + ").js?languageId=en_US&translate=true", "http://example.com/footer-portal.js", "http://example.com/footer-portlet.js", "module:/portal/module-footer-portal.(" + _HASH + ").js", @@ -212,24 +207,22 @@ public void testGetPortletRenderPartsWithContextAndProxy() "/proxy/portal/header-portal.(" + _HASH + ").css", "/proxy/portal/header-portal.tokenized.(" + _HASH + ").css?themeId=classic_WAR_classictheme&tokenize=true", - "/proxy/portal/nocombo-header-portal.(" + _HASH + ").css", "/proxy/portal/o/portlet-web/header-portlet.(" + _HASH + ").css", "/proxy/portal/o/portlet-web/header-portlet.tokenized.(" + _HASH + ").css?themeId=classic_WAR_classictheme&tokenize=true", - "/proxy/portal/o/portlet-web/nocombo-header-portlet.(" + _HASH + - ").css", "http://example.com/header-portal.css", "http://example.com/header-portlet.css"), portletRenderParts.getHeaderCssPaths()); _assertEquals( Arrays.asList( "/proxy/portal/header-portal.(" + _HASH + ").js", - "/proxy/portal/nocombo-header-portal.(" + _HASH + ").js", + "/proxy/portal/header-portal.translatable.(" + _HASH + + ").js?languageId=en_US&translate=true", "/proxy/portal/o/portlet-web/header-portlet.(" + _HASH + ").js", - "/proxy/portal/o/portlet-web/nocombo-header-portlet.(" + _HASH + - ").js", + "/proxy/portal/o/portlet-web/header-portlet.translatable.(" + + _HASH + ").js?languageId=en_US&translate=true", "http://example.com/header-portal.js", "http://example.com/header-portlet.js", "module:/proxy/portal/module-header-portal.(" + _HASH + ").js", @@ -243,24 +236,22 @@ public void testGetPortletRenderPartsWithContextAndProxy() "/proxy/portal/footer-portal.(" + _HASH + ").css", "/proxy/portal/footer-portal.tokenized.(" + _HASH + ").css?themeId=classic_WAR_classictheme&tokenize=true", - "/proxy/portal/nocombo-footer-portal.(" + _HASH + ").css", "/proxy/portal/o/portlet-web/footer-portlet.(" + _HASH + ").css", "/proxy/portal/o/portlet-web/footer-portlet.tokenized.(" + _HASH + ").css?themeId=classic_WAR_classictheme&tokenize=true", - "/proxy/portal/o/portlet-web/nocombo-footer-portlet.(" + _HASH + - ").css", "http://example.com/footer-portal.css", "http://example.com/footer-portlet.css"), portletRenderParts.getFooterCssPaths()); _assertEquals( Arrays.asList( "/proxy/portal/footer-portal.(" + _HASH + ").js", - "/proxy/portal/nocombo-footer-portal.(" + _HASH + ").js", + "/proxy/portal/footer-portal.translatable.(" + _HASH + + ").js?languageId=en_US&translate=true", "/proxy/portal/o/portlet-web/footer-portlet.(" + _HASH + ").js", - "/proxy/portal/o/portlet-web/nocombo-footer-portlet.(" + _HASH + - ").js", + "/proxy/portal/o/portlet-web/footer-portlet.translatable.(" + + _HASH + ").js?languageId=en_US&translate=true", "http://example.com/footer-portal.js", "http://example.com/footer-portlet.js", "module:/proxy/portal/module-footer-portal.(" + _HASH + ").js", @@ -289,22 +280,20 @@ public void testGetPortletRenderPartsWithProxy() throws Exception { "/proxy/header-portal.(" + _HASH + ").css", "/proxy/header-portal.tokenized.(" + _HASH + ").css?themeId=classic_WAR_classictheme&tokenize=true", - "/proxy/nocombo-header-portal.(" + _HASH + ").css", "/proxy/o/portlet-web/header-portlet.(" + _HASH + ").css", "/proxy/o/portlet-web/header-portlet.tokenized.(" + _HASH + ").css?themeId=classic_WAR_classictheme&tokenize=true", - "/proxy/o/portlet-web/nocombo-header-portlet.(" + _HASH + - ").css", "http://example.com/header-portal.css", "http://example.com/header-portlet.css"), portletRenderParts.getHeaderCssPaths()); _assertEquals( Arrays.asList( "/proxy/header-portal.(" + _HASH + ").js", - "/proxy/nocombo-header-portal.(" + _HASH + ").js", + "/proxy/header-portal.translatable.(" + _HASH + + ").js?languageId=en_US&translate=true", "/proxy/o/portlet-web/header-portlet.(" + _HASH + ").js", - "/proxy/o/portlet-web/nocombo-header-portlet.(" + _HASH + - ").js", + "/proxy/o/portlet-web/header-portlet.translatable.(" + _HASH + + ").js?languageId=en_US&translate=true", "http://example.com/header-portal.js", "http://example.com/header-portlet.js", "module:/proxy/module-header-portal.(" + _HASH + ").js", @@ -318,22 +307,20 @@ public void testGetPortletRenderPartsWithProxy() throws Exception { "/proxy/footer-portal.(" + _HASH + ").css", "/proxy/footer-portal.tokenized.(" + _HASH + ").css?themeId=classic_WAR_classictheme&tokenize=true", - "/proxy/nocombo-footer-portal.(" + _HASH + ").css", "/proxy/o/portlet-web/footer-portlet.(" + _HASH + ").css", "/proxy/o/portlet-web/footer-portlet.tokenized.(" + _HASH + ").css?themeId=classic_WAR_classictheme&tokenize=true", - "/proxy/o/portlet-web/nocombo-footer-portlet.(" + _HASH + - ").css", "http://example.com/footer-portal.css", "http://example.com/footer-portlet.css"), portletRenderParts.getFooterCssPaths()); _assertEquals( Arrays.asList( "/proxy/footer-portal.(" + _HASH + ").js", - "/proxy/nocombo-footer-portal.(" + _HASH + ").js", + "/proxy/footer-portal.translatable.(" + _HASH + + ").js?languageId=en_US&translate=true", "/proxy/o/portlet-web/footer-portlet.(" + _HASH + ").js", - "/proxy/o/portlet-web/nocombo-footer-portlet.(" + _HASH + - ").js", + "/proxy/o/portlet-web/footer-portlet.translatable.(" + _HASH + + ").js?languageId=en_US&translate=true", "http://example.com/footer-portal.js", "http://example.com/footer-portlet.js", "module:/proxy/module-footer-portal.(" + _HASH + ").js", @@ -362,8 +349,7 @@ private void _assertEquals( "Nonempty expected set " + expectedSet, expectedSet.isEmpty()); } - private void _setUpMocks(String contextPath, String proxyPath) - throws Exception { + private void _setUpMocks(String contextPath, String proxyPath) { // HashedFilesRegistryUtil @@ -388,6 +374,10 @@ private void _setUpMocks(String contextPath, String proxyPath) content = "@theme_image_path@"; } + if (fileURI.contains(".translatable.")) { + content = "Liferay.Language.get('portlet')"; + } + Mockito.when( url.openStream() ).thenReturn( @@ -404,14 +394,7 @@ private void _setUpMocks(String contextPath, String proxyPath) _htmlUtilMockedStatic.when( () -> HtmlUtil.escapeURL(Mockito.anyString()) ).thenAnswer( - new Answer() { - - @Override - public String answer(InvocationOnMock invocationOnMock) { - return invocationOnMock.getArgument(0); - } - - } + (Answer)invocationOnMock -> invocationOnMock.getArgument(0) ); // PortalUtil @@ -433,21 +416,15 @@ public String answer(InvocationOnMock invocationOnMock) { Mockito.any(HttpServletRequest.class), Mockito.anyString(), Mockito.anyLong()) ).thenAnswer( - new Answer() { - - @Override - public String answer(InvocationOnMock invocationOnMock) { - String uri = invocationOnMock.getArgument(1, String.class); - long timestamp = invocationOnMock.getArgument( - 2, Long.class); - - if (timestamp < 0) { - return uri; - } + (Answer)invocationOnMock -> { + String uri = invocationOnMock.getArgument(1, String.class); + long timestamp = invocationOnMock.getArgument(2, Long.class); - return uri + "?t=" + timestamp; + if (timestamp < 0) { + return uri; } + return uri + "?t=" + timestamp; } ); @@ -470,6 +447,12 @@ public String answer(InvocationOnMock invocationOnMock) { "" ); + Mockito.when( + themeDisplay.getLanguageId() + ).thenReturn( + "en_US" + ); + Mockito.when( themeDisplay.getThemeId() ).thenReturn( @@ -505,18 +488,17 @@ public String answer(InvocationOnMock invocationOnMock) { ).thenReturn( Arrays.asList( "/footer-portal.css", "/footer-portal.tokenized.css", - "http://example.com/footer-portal.css", - "nocombo:/nocombo-footer-portal.css") + "http://example.com/footer-portal.css") ); Mockito.when( _portlet.getFooterPortalJavaScript() ).thenReturn( Arrays.asList( - "/footer-portal.js", "http://example.com/footer-portal.js", + "/footer-portal.js", "/footer-portal.translatable.js", + "http://example.com/footer-portal.js", "module:/module-footer-portal.js", - "module:http://example.com/module-footer-portal.js", - "nocombo:/nocombo-footer-portal.js") + "module:http://example.com/module-footer-portal.js") ); Mockito.when( @@ -524,18 +506,17 @@ public String answer(InvocationOnMock invocationOnMock) { ).thenReturn( Arrays.asList( "/footer-portlet.css", "/footer-portlet.tokenized.css", - "http://example.com/footer-portlet.css", - "nocombo:/nocombo-footer-portlet.css") + "http://example.com/footer-portlet.css") ); Mockito.when( _portlet.getFooterPortletJavaScript() ).thenReturn( Arrays.asList( - "/footer-portlet.js", "http://example.com/footer-portlet.js", + "/footer-portlet.js", "/footer-portlet.translatable.js", + "http://example.com/footer-portlet.js", "module:/module-footer-portlet.js", - "module:http://example.com/module-footer-portlet.js", - "nocombo:/nocombo-footer-portlet.js") + "module:http://example.com/module-footer-portlet.js") ); Mockito.when( @@ -543,18 +524,17 @@ public String answer(InvocationOnMock invocationOnMock) { ).thenReturn( Arrays.asList( "/header-portal.css", "/header-portal.tokenized.css", - "http://example.com/header-portal.css", - "nocombo:/nocombo-header-portal.css") + "http://example.com/header-portal.css") ); Mockito.when( _portlet.getHeaderPortalJavaScript() ).thenReturn( Arrays.asList( - "/header-portal.js", "http://example.com/header-portal.js", + "/header-portal.js", "/header-portal.translatable.js", + "http://example.com/header-portal.js", "module:/module-header-portal.js", - "module:http://example.com/module-header-portal.js", - "nocombo:/nocombo-header-portal.js") + "module:http://example.com/module-header-portal.js") ); Mockito.when( @@ -562,18 +542,17 @@ public String answer(InvocationOnMock invocationOnMock) { ).thenReturn( Arrays.asList( "/header-portlet.css", "/header-portlet.tokenized.css", - "http://example.com/header-portlet.css", - "nocombo:/nocombo-header-portlet.css") + "http://example.com/header-portlet.css") ); Mockito.when( _portlet.getHeaderPortletJavaScript() ).thenReturn( Arrays.asList( - "/header-portlet.js", "http://example.com/header-portlet.js", + "/header-portlet.js", "/header-portlet.translatable.js", + "http://example.com/header-portlet.js", "module:/module-header-portlet.js", - "module:http://example.com/module-header-portlet.js", - "nocombo:/nocombo-header-portlet.js") + "module:http://example.com/module-header-portlet.js") ); Mockito.when(