mirror of
https://github.com/apache/struts.git
synced 2026-08-05 06:36:58 +00:00
WW-5640 Add WebJars support to Struts core (#1765)
* WW-5640 docs: design for WebJars support in Struts core Adds first-class WebJars support so client-side libraries can be referenced by a version-less logical path and served through the existing static-content pipeline. Grounded against 7.2.x source. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * WW-5640 docs: implementation plan for WebJars support Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * WW-5640 build: add webjars-locator-lite dependency Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * WW-5640 feat: add webjars config constants and defaults Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * WW-5640 docs: correct plan test framework to JUnit 4 core uses JUnit 4 + AssertJ + Mockito, not JUnit 5 Jupiter (no Jupiter engine on the classpath). Test tasks translate accordingly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * WW-5640 feat: add WebJarUrlProvider resolution seam Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * WW-5640 feat: register WebJarUrlProvider bean Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * WW-5640 feat: extend static content-type map for webjar assets Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * WW-5640 feat: serve webjar assets via static content loader Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * WW-5640 feat: add <s:webjar> tag and <@s.webjar> macro Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * WW-5640 docs: add generated tag reference for <s:webjar> Annotation-processor-generated tag reference (attributes + description), tracked like every other tag's docs under core/src/site/resources/tags/. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * WW-5640 fix: address final review (log level, resolveUrl traversal test, javadoc) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * WW-5640 refactor: address SonarCloud code smells - getContentType: replace long if/else chain with a static extension-> MIME map (S3776 cognitive complexity) - DefaultWebJarUrlProvider.split: return Optional<String[]> instead of a null sentinel (S1168; Optional fits the reject semantics, empty-array would not) - serving tests: rename local 'loader' -> 'webJarLoader' to stop hiding the ContentTypeProbe field (S1117) - WebJarTest: use assertThat(writer).hasToString(...) (S5838) S110 (WebJarTag inheritance depth) is inherent to the Struts tag base class hierarchy shared by every tag; left as-is. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -153,6 +153,17 @@
|
||||
<artifactId>caffeine</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.webjars</groupId>
|
||||
<artifactId>webjars-locator-lite</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.webjars</groupId>
|
||||
<artifactId>jquery</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
|
||||
@@ -195,6 +195,16 @@ public final class StrutsConstants {
|
||||
*/
|
||||
public static final String STRUTS_UI_STATIC_CONTENT_PATH = "struts.ui.staticContentPath";
|
||||
|
||||
/**
|
||||
* Whether WebJars support is enabled (serving and URL building)
|
||||
*/
|
||||
public static final String STRUTS_WEBJARS_ENABLED = "struts.webjars.enabled";
|
||||
|
||||
/**
|
||||
* Optional comma-separated allowlist of WebJar names permitted to be served (empty = all)
|
||||
*/
|
||||
public static final String STRUTS_WEBJARS_ALLOWLIST = "struts.webjars.allowlist";
|
||||
|
||||
/**
|
||||
* A global flag to enable/disable html body escaping in tags, can be overwritten per tag
|
||||
*/
|
||||
@@ -434,6 +444,11 @@ public final class StrutsConstants {
|
||||
*/
|
||||
public static final String STRUTS_STATIC_CONTENT_LOADER = "struts.staticContentLoader";
|
||||
|
||||
/**
|
||||
* The {@link org.apache.struts2.webjars.WebJarUrlProvider} implementation class
|
||||
*/
|
||||
public static final String STRUTS_WEBJARS_URL_PROVIDER = "struts.webjars.urlProvider";
|
||||
|
||||
/**
|
||||
* The {@link org.apache.struts2.UnknownHandlerManager} implementation class
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.struts2.components;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.struts2.inject.Inject;
|
||||
import org.apache.struts2.util.ValueStack;
|
||||
import org.apache.struts2.views.annotations.StrutsTag;
|
||||
import org.apache.struts2.views.annotations.StrutsTagAttribute;
|
||||
import org.apache.struts2.webjars.WebJarUrlProvider;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* <p>Resolves a version-less WebJar resource path to a servable URL and writes it to the output
|
||||
* (or stores it in a variable when {@code var} is set). Compose it with {@code <s:script>}/{@code <s:link>}
|
||||
* or a raw {@code <link>}/{@code <script>} element.</p>
|
||||
*
|
||||
* <b>Examples</b>
|
||||
* <pre>
|
||||
* <link rel="stylesheet" href="<s:webjar path="bootstrap/css/bootstrap.min.css" />" />
|
||||
* <@s.webjar path="jquery/jquery.min.js"/>
|
||||
* </pre>
|
||||
*/
|
||||
@StrutsTag(
|
||||
name = "webjar",
|
||||
tldTagClass = "org.apache.struts2.views.jsp.WebJarTag",
|
||||
description = "Resolve a version-less WebJar resource path to a servable URL")
|
||||
public class WebJar extends ContextBean {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(WebJar.class);
|
||||
|
||||
protected String path;
|
||||
|
||||
private final HttpServletRequest request;
|
||||
private WebJarUrlProvider webJarUrlProvider;
|
||||
|
||||
public WebJar(ValueStack stack, HttpServletRequest request) {
|
||||
super(stack);
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
@Inject
|
||||
public void setWebJarUrlProvider(WebJarUrlProvider webJarUrlProvider) {
|
||||
this.webJarUrlProvider = webJarUrlProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean end(Writer writer, String body) {
|
||||
String logicalPath = findString(path);
|
||||
Optional<String> url = (logicalPath == null)
|
||||
? Optional.empty()
|
||||
: webJarUrlProvider.resolveUrl(logicalPath, request);
|
||||
|
||||
if (url.isPresent()) {
|
||||
if (StringUtils.isNotBlank(getVar())) {
|
||||
putInContext(url.get());
|
||||
} else {
|
||||
try {
|
||||
writer.write(url.get());
|
||||
} catch (IOException e) {
|
||||
LOG.error("Could not write WebJar URL for path '{}'", path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.end(writer, body);
|
||||
}
|
||||
|
||||
@StrutsTagAttribute(required = true,
|
||||
description = "The version-less WebJar resource path, e.g. bootstrap/css/bootstrap.min.css")
|
||||
public void setPath(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
}
|
||||
@@ -84,6 +84,7 @@ import org.apache.struts2.util.reflection.ReflectionProvider;
|
||||
import org.apache.struts2.validator.ActionValidatorManager;
|
||||
import org.apache.struts2.views.freemarker.FreemarkerManager;
|
||||
import org.apache.struts2.views.util.UrlHelper;
|
||||
import org.apache.struts2.webjars.WebJarUrlProvider;
|
||||
|
||||
/**
|
||||
* Selects the implementations of key framework extension points, using the loaded
|
||||
@@ -430,6 +431,7 @@ public class StrutsBeanSelectionProvider extends AbstractBeanSelectionProvider {
|
||||
alias(PatternMatcher.class, StrutsConstants.STRUTS_PATTERNMATCHER, builder, props);
|
||||
alias(ContentTypeMatcher.class, StrutsConstants.STRUTS_CONTENT_TYPE_MATCHER, builder, props);
|
||||
alias(StaticContentLoader.class, StrutsConstants.STRUTS_STATIC_CONTENT_LOADER, builder, props);
|
||||
alias(WebJarUrlProvider.class, StrutsConstants.STRUTS_WEBJARS_URL_PROVIDER, builder, props);
|
||||
alias(UnknownHandlerManager.class, StrutsConstants.STRUTS_UNKNOWN_HANDLER_MANAGER, builder, props);
|
||||
alias(UrlHelper.class, StrutsConstants.STRUTS_URL_HELPER, builder, props);
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.struts2.StrutsConstants;
|
||||
import org.apache.struts2.webjars.WebJarUrlProvider;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -39,6 +40,8 @@ import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
/**
|
||||
@@ -70,6 +73,8 @@ import java.util.StringTokenizer;
|
||||
*/
|
||||
public class DefaultStaticContentLoader implements StaticContentLoader {
|
||||
|
||||
protected static final String WEBJARS_REQUEST_PREFIX = "/webjars/";
|
||||
|
||||
/**
|
||||
* Provide a logging instance.
|
||||
*/
|
||||
@@ -107,6 +112,13 @@ public class DefaultStaticContentLoader implements StaticContentLoader {
|
||||
|
||||
protected boolean devMode;
|
||||
|
||||
protected WebJarUrlProvider webJarUrlProvider;
|
||||
|
||||
@Inject
|
||||
public void setWebJarUrlProvider(WebJarUrlProvider webJarUrlProvider) {
|
||||
this.webJarUrlProvider = webJarUrlProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify state of StrutsConstants.STRUTS_SERVE_STATIC_CONTENT setting.
|
||||
*
|
||||
@@ -209,6 +221,14 @@ public class DefaultStaticContentLoader implements StaticContentLoader {
|
||||
public void findStaticResource(String path, HttpServletRequest request, HttpServletResponse response)
|
||||
throws IOException {
|
||||
String name = cleanupPath(path);
|
||||
|
||||
if (name.startsWith(WEBJARS_REQUEST_PREFIX)) {
|
||||
if (!findWebJarResource(name, path, request, response)) {
|
||||
sendNotFound(response);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (String pathPrefix : pathPrefixes) {
|
||||
URL resourceUrl = findResource(buildPath(name, pathPrefix));
|
||||
if (resourceUrl != null) {
|
||||
@@ -231,6 +251,32 @@ public class DefaultStaticContentLoader implements StaticContentLoader {
|
||||
}
|
||||
}
|
||||
|
||||
sendNotFound(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve and serve a WebJar asset requested under {@code <staticContentPath>/webjars/**}.
|
||||
*
|
||||
* @param name the request path with the static-content prefix stripped, e.g. {@code /webjars/jquery/jquery.min.js}
|
||||
* @param path the original request path (used for content-type detection)
|
||||
* @return true if the asset was resolved and streamed; false otherwise (caller sends 404)
|
||||
*/
|
||||
protected boolean findWebJarResource(String name, String path, HttpServletRequest request, HttpServletResponse response)
|
||||
throws IOException {
|
||||
String logicalPath = name.substring(WEBJARS_REQUEST_PREFIX.length());
|
||||
Optional<String> resource = webJarUrlProvider.resolveResourcePath(logicalPath);
|
||||
if (resource.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
URL resourceUrl = findResource(resource.get());
|
||||
if (resourceUrl == null) {
|
||||
return false;
|
||||
}
|
||||
process(resourceUrl.openStream(), path, request, response);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void sendNotFound(HttpServletResponse response) {
|
||||
try {
|
||||
response.sendError(HttpServletResponse.SC_NOT_FOUND);
|
||||
} catch (IOException e1) {
|
||||
@@ -321,32 +367,43 @@ public class DefaultStaticContentLoader implements StaticContentLoader {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Maps a lower-case file extension to its content type. Not using the code provided by
|
||||
* activation.jar to avoid adding yet another dependency; this covers the files we serve up
|
||||
* (Struts' own static assets plus WebJar assets).
|
||||
*/
|
||||
private static final Map<String, String> CONTENT_TYPES = Map.ofEntries(
|
||||
Map.entry("js", "text/javascript"),
|
||||
Map.entry("mjs", "text/javascript"),
|
||||
Map.entry("css", "text/css"),
|
||||
Map.entry("html", "text/html"),
|
||||
Map.entry("txt", "text/plain"),
|
||||
Map.entry("gif", "image/gif"),
|
||||
Map.entry("jpg", "image/jpeg"),
|
||||
Map.entry("jpeg", "image/jpeg"),
|
||||
Map.entry("png", "image/png"),
|
||||
Map.entry("svg", "image/svg+xml"),
|
||||
Map.entry("ico", "image/x-icon"),
|
||||
Map.entry("woff2", "font/woff2"),
|
||||
Map.entry("woff", "font/woff"),
|
||||
Map.entry("ttf", "font/ttf"),
|
||||
Map.entry("otf", "font/otf"),
|
||||
Map.entry("eot", "application/vnd.ms-fontobject"),
|
||||
Map.entry("json", "application/json"),
|
||||
Map.entry("map", "application/json"));
|
||||
|
||||
/**
|
||||
* Determine the content type for the resource name.
|
||||
*
|
||||
* @param name The resource name
|
||||
* @return The mime type
|
||||
* @return The mime type, or {@code null} if the extension is unknown
|
||||
*/
|
||||
protected String getContentType(String name) {
|
||||
// NOT using the code provided activation.jar to avoid adding yet another dependency
|
||||
// this is generally OK, since these are the main files we server up
|
||||
if (name.endsWith(".js")) {
|
||||
return "text/javascript";
|
||||
} else if (name.endsWith(".css")) {
|
||||
return "text/css";
|
||||
} else if (name.endsWith(".html")) {
|
||||
return "text/html";
|
||||
} else if (name.endsWith(".txt")) {
|
||||
return "text/plain";
|
||||
} else if (name.endsWith(".gif")) {
|
||||
return "image/gif";
|
||||
} else if (name.endsWith(".jpg") || name.endsWith(".jpeg")) {
|
||||
return "image/jpeg";
|
||||
} else if (name.endsWith(".png")) {
|
||||
return "image/png";
|
||||
} else {
|
||||
int dot = name.lastIndexOf('.');
|
||||
if (dot < 0) {
|
||||
return null;
|
||||
}
|
||||
return CONTENT_TYPES.get(name.substring(dot + 1));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -53,6 +53,7 @@ public class StrutsModels {
|
||||
protected RadioModel radio;
|
||||
protected SelectModel select;
|
||||
protected SetModel set;
|
||||
protected WebJarModel webjar;
|
||||
protected SubmitModel submit;
|
||||
protected ResetModel reset;
|
||||
protected TextAreaModel textarea;
|
||||
@@ -339,6 +340,13 @@ public class StrutsModels {
|
||||
return set;
|
||||
}
|
||||
|
||||
public WebJarModel getWebjar() {
|
||||
if (webjar == null) {
|
||||
webjar = new WebJarModel(stack, req, res);
|
||||
}
|
||||
return webjar;
|
||||
}
|
||||
|
||||
public PropertyModel getProperty() {
|
||||
if (property == null) {
|
||||
property = new PropertyModel(stack, req, res);
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.struts2.views.freemarker.tags;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.apache.struts2.components.Component;
|
||||
import org.apache.struts2.components.WebJar;
|
||||
import org.apache.struts2.util.ValueStack;
|
||||
|
||||
public class WebJarModel extends TagModel {
|
||||
|
||||
public WebJarModel(ValueStack stack, HttpServletRequest req, HttpServletResponse res) {
|
||||
super(stack, req, res);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Component getBean() {
|
||||
return new WebJar(stack, req);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.struts2.views.jsp;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.apache.struts2.components.Component;
|
||||
import org.apache.struts2.components.WebJar;
|
||||
import org.apache.struts2.util.ValueStack;
|
||||
|
||||
public class WebJarTag extends ContextBeanTag {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
protected String path;
|
||||
|
||||
@Override
|
||||
public Component getBean(ValueStack stack, HttpServletRequest req, HttpServletResponse res) {
|
||||
return new WebJar(stack, req);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void populateParams() {
|
||||
super.populateParams();
|
||||
WebJar webJar = (WebJar) component;
|
||||
webJar.setPath(path);
|
||||
}
|
||||
|
||||
public void setPath(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.struts2.webjars;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.struts2.StrutsConstants;
|
||||
import org.apache.struts2.dispatcher.StaticContentLoader;
|
||||
import org.apache.struts2.inject.Inject;
|
||||
import org.webjars.WebJarVersionLocator;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Default {@link WebJarUrlProvider} backed by a singleton {@link WebJarVersionLocator}
|
||||
* (from {@code webjars-locator-lite}). Thread-safe.
|
||||
*/
|
||||
public class DefaultWebJarUrlProvider implements WebJarUrlProvider {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(DefaultWebJarUrlProvider.class);
|
||||
|
||||
private static final String WEBJARS_URL_SEGMENT = "/webjars/";
|
||||
|
||||
private final WebJarVersionLocator locator = new WebJarVersionLocator();
|
||||
|
||||
private boolean enabled = true;
|
||||
private Set<String> allowlist = Collections.emptySet();
|
||||
private String uiStaticContentPath = StaticContentLoader.DEFAULT_STATIC_CONTENT_PATH;
|
||||
|
||||
@Inject(value = StrutsConstants.STRUTS_WEBJARS_ENABLED, required = false)
|
||||
public void setEnabled(String enabled) {
|
||||
this.enabled = BooleanUtils.toBoolean(enabled);
|
||||
}
|
||||
|
||||
@Inject(value = StrutsConstants.STRUTS_WEBJARS_ALLOWLIST, required = false)
|
||||
public void setAllowlist(String allowlist) {
|
||||
Set<String> names = new HashSet<>();
|
||||
if (StringUtils.isNotBlank(allowlist)) {
|
||||
for (String name : allowlist.split(",")) {
|
||||
String trimmed = name.trim();
|
||||
if (!trimmed.isEmpty()) {
|
||||
names.add(trimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.allowlist = Collections.unmodifiableSet(names);
|
||||
}
|
||||
|
||||
@Inject(StrutsConstants.STRUTS_UI_STATIC_CONTENT_PATH)
|
||||
public void setStaticContentPath(String uiStaticContentPath) {
|
||||
this.uiStaticContentPath = StaticContentLoader.Validator.validateStaticContentPath(uiStaticContentPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> resolveResourcePath(String logicalPath) {
|
||||
Optional<String[]> parts = split(logicalPath);
|
||||
if (parts.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
String[] p = parts.get();
|
||||
String full = locator.fullPath(p[0], p[1]);
|
||||
if (full == null || !full.startsWith(WebJarVersionLocator.WEBJARS_PATH_PREFIX + "/")) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(full);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> resolveUrl(String logicalPath, HttpServletRequest request) {
|
||||
Optional<String[]> parts = split(logicalPath);
|
||||
if (parts.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
String[] p = parts.get();
|
||||
String versioned = locator.path(p[0], p[1]);
|
||||
if (versioned == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
StringBuilder url = new StringBuilder();
|
||||
String contextPath = request.getContextPath();
|
||||
if (StringUtils.isNotEmpty(contextPath) && !"/".equals(contextPath)) {
|
||||
url.append(contextPath);
|
||||
}
|
||||
url.append(uiStaticContentPath).append(WEBJARS_URL_SEGMENT).append(versioned);
|
||||
return Optional.of(url.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize, validate and split a logical path into {webJarName, filePath}.
|
||||
*
|
||||
* @return the two-element {webJarName, filePath} pair, or {@link Optional#empty()} if disabled,
|
||||
* blank, single-segment, traversal-tainted, or allowlist-blocked
|
||||
*/
|
||||
private Optional<String[]> split(String logicalPath) {
|
||||
if (!enabled || StringUtils.isBlank(logicalPath)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
String normalized = StringUtils.stripStart(logicalPath, "/");
|
||||
if (normalized.contains("\\")) {
|
||||
return Optional.empty();
|
||||
}
|
||||
for (String segment : normalized.split("/")) {
|
||||
if (segment.equals("..") || segment.equals(".")) {
|
||||
LOG.debug("Rejecting WebJar path with traversal segment: {}", logicalPath);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
int slash = normalized.indexOf('/');
|
||||
if (slash < 1 || slash == normalized.length() - 1) {
|
||||
return Optional.empty();
|
||||
}
|
||||
String webJarName = normalized.substring(0, slash);
|
||||
String filePath = normalized.substring(slash + 1);
|
||||
if (!isAllowed(webJarName)) {
|
||||
LOG.debug("WebJar '{}' is not on the allowlist", webJarName);
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(new String[]{webJarName, filePath});
|
||||
}
|
||||
|
||||
private boolean isAllowed(String webJarName) {
|
||||
return allowlist.isEmpty() || allowlist.contains(webJarName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.struts2.webjars;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Resolves version-less WebJar logical paths (e.g. {@code bootstrap/css/bootstrap.min.css}) to either a
|
||||
* concrete classpath resource under {@code META-INF/resources/webjars/} (for serving) or a servable URL
|
||||
* (for tags/macros). Resolution is constrained to the WebJars root, honours an optional allowlist and the
|
||||
* {@code struts.webjars.enabled} switch, and fails closed (empty result) when unresolved or blocked.
|
||||
*/
|
||||
public interface WebJarUrlProvider {
|
||||
|
||||
/**
|
||||
* @param logicalPath version-less path such as {@code bootstrap/css/bootstrap.min.css}
|
||||
* @return the concrete classpath resource path (e.g.
|
||||
* {@code META-INF/resources/webjars/bootstrap/5.3.8/css/bootstrap.min.css}), or empty
|
||||
*/
|
||||
Optional<String> resolveResourcePath(String logicalPath);
|
||||
|
||||
/**
|
||||
* @param logicalPath version-less path such as {@code bootstrap/css/bootstrap.min.css}
|
||||
* @param request the current request (used for the servlet context path)
|
||||
* @return a servable URL, or empty
|
||||
*/
|
||||
Optional<String> resolveUrl(String logicalPath, HttpServletRequest request);
|
||||
|
||||
/**
|
||||
* @return whether WebJars support is enabled
|
||||
*/
|
||||
boolean isEnabled();
|
||||
}
|
||||
@@ -106,6 +106,12 @@ struts.ui.staticContentPath=/static
|
||||
### headers)
|
||||
struts.serve.static.browserCache=true
|
||||
|
||||
### WebJars support
|
||||
### Master switch for resolving/serving WebJar assets under <staticContentPath>/webjars/**
|
||||
struts.webjars.enabled=true
|
||||
### Optional comma-separated allowlist of WebJar names (empty = all WebJars on the classpath)
|
||||
struts.webjars.allowlist=
|
||||
|
||||
### Set this to false if you wish to disable implicit dynamic method invocation
|
||||
### via the URL request. This includes URLs like foo!bar.action, as well as params
|
||||
### like method:bar (but not action:foo).
|
||||
|
||||
@@ -207,6 +207,8 @@
|
||||
|
||||
<bean type="org.apache.struts2.dispatcher.StaticContentLoader"
|
||||
class="org.apache.struts2.dispatcher.DefaultStaticContentLoader" name="struts"/>
|
||||
<bean type="org.apache.struts2.webjars.WebJarUrlProvider"
|
||||
class="org.apache.struts2.webjars.DefaultWebJarUrlProvider" name="struts"/>
|
||||
<bean type="org.apache.struts2.UnknownHandlerManager"
|
||||
class="org.apache.struts2.DefaultUnknownHandlerManager" name="struts"/>
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<table class="tag-reference">
|
||||
<tr>
|
||||
<td colspan="6"><h4>Dynamic Attributes Allowed:</h4> false</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="6"><hr/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="tag-header"><h4>Name</h4></th>
|
||||
<th class="tag-header"><h4>Required</h4></th>
|
||||
<th class="tag-header"><h4>Default</h4></th>
|
||||
<th class="tag-header"><h4>Evaluated</h4></th>
|
||||
<th class="tag-header"><h4>Type</h4></th>
|
||||
<th class="tag-header"><h4>Description</h4></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tag-attribute">path</td>
|
||||
<td class="tag-attribute"><strong>true</strong></td>
|
||||
<td class="tag-attribute"></td>
|
||||
<td class="tag-attribute">false</td>
|
||||
<td class="tag-attribute">String</td>
|
||||
<td class="tag-attribute">The version-less WebJar resource path, e.g. bootstrap/css/bootstrap.min.css</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tag-attribute">performClearTagStateForTagPoolingServers</td>
|
||||
<td class="tag-attribute">false</td>
|
||||
<td class="tag-attribute">false</td>
|
||||
<td class="tag-attribute">false</td>
|
||||
<td class="tag-attribute">Boolean</td>
|
||||
<td class="tag-attribute">Whether to clear all tag state during doEndTag() processing (if applicable)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tag-attribute">var</td>
|
||||
<td class="tag-attribute">false</td>
|
||||
<td class="tag-attribute"></td>
|
||||
<td class="tag-attribute">false</td>
|
||||
<td class="tag-attribute">String</td>
|
||||
<td class="tag-attribute">Name used to reference the value pushed into the Value Stack (scope: action).</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -0,0 +1 @@
|
||||
Resolve a version-less WebJar resource path to a servable URL
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.struts2.components;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.apache.struts2.util.ValueStack;
|
||||
import org.apache.struts2.webjars.WebJarUrlProvider;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.StringWriter;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class WebJarTest {
|
||||
|
||||
private WebJar newComponent(ValueStack stack, HttpServletRequest request, WebJarUrlProvider provider) {
|
||||
Map<String, Object> context = new HashMap<>();
|
||||
when(stack.getContext()).thenReturn(context);
|
||||
WebJar webJar = new WebJar(stack, request);
|
||||
webJar.setWebJarUrlProvider(provider);
|
||||
return webJar;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writesResolvedUrl() {
|
||||
ValueStack stack = mock(ValueStack.class);
|
||||
when(stack.findString("jquery/jquery.min.js")).thenReturn("jquery/jquery.min.js");
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
WebJarUrlProvider provider = mock(WebJarUrlProvider.class);
|
||||
when(provider.resolveUrl("jquery/jquery.min.js", request))
|
||||
.thenReturn(Optional.of("/myapp/static/webjars/jquery/3.7.1/jquery.min.js"));
|
||||
|
||||
WebJar webJar = newComponent(stack, request, provider);
|
||||
webJar.setPath("jquery/jquery.min.js");
|
||||
StringWriter writer = new StringWriter();
|
||||
|
||||
webJar.start(writer);
|
||||
webJar.end(writer, "");
|
||||
|
||||
assertThat(writer).hasToString("/myapp/static/webjars/jquery/3.7.1/jquery.min.js");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unresolvedPathWritesNothing() {
|
||||
ValueStack stack = mock(ValueStack.class);
|
||||
when(stack.findString("nope/x.js")).thenReturn("nope/x.js");
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
WebJarUrlProvider provider = mock(WebJarUrlProvider.class);
|
||||
when(provider.resolveUrl("nope/x.js", request)).thenReturn(Optional.empty());
|
||||
|
||||
WebJar webJar = newComponent(stack, request, provider);
|
||||
webJar.setPath("nope/x.js");
|
||||
StringWriter writer = new StringWriter();
|
||||
|
||||
webJar.start(writer);
|
||||
webJar.end(writer, "");
|
||||
|
||||
assertThat(writer.toString()).isEmpty();
|
||||
}
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.struts2.dispatcher;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class DefaultStaticContentLoaderWebJarTest {
|
||||
|
||||
private final ContentTypeProbe loader = new ContentTypeProbe();
|
||||
|
||||
/** Exposes the protected getContentType for assertion. */
|
||||
static class ContentTypeProbe extends DefaultStaticContentLoader {
|
||||
String type(String name) {
|
||||
return getContentType(name);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mapsWebJarAssetTypes() {
|
||||
assertThat(loader.type("x.woff2")).isEqualTo("font/woff2");
|
||||
assertThat(loader.type("x.woff")).isEqualTo("font/woff");
|
||||
assertThat(loader.type("x.ttf")).isEqualTo("font/ttf");
|
||||
assertThat(loader.type("x.otf")).isEqualTo("font/otf");
|
||||
assertThat(loader.type("x.eot")).isEqualTo("application/vnd.ms-fontobject");
|
||||
assertThat(loader.type("x.svg")).isEqualTo("image/svg+xml");
|
||||
assertThat(loader.type("x.map")).isEqualTo("application/json");
|
||||
assertThat(loader.type("x.json")).isEqualTo("application/json");
|
||||
assertThat(loader.type("x.ico")).isEqualTo("image/x-icon");
|
||||
assertThat(loader.type("x.mjs")).isEqualTo("text/javascript");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preservesExistingTypes() {
|
||||
assertThat(loader.type("x.js")).isEqualTo("text/javascript");
|
||||
assertThat(loader.type("x.css")).isEqualTo("text/css");
|
||||
assertThat(loader.type("x.png")).isEqualTo("image/png");
|
||||
assertThat(loader.type("x.unknown")).isNull();
|
||||
}
|
||||
|
||||
private DefaultStaticContentLoader newLoader(boolean enabled) {
|
||||
DefaultStaticContentLoader webJarLoader = new DefaultStaticContentLoader();
|
||||
webJarLoader.setServeStaticContent("true");
|
||||
webJarLoader.setStaticContentPath("/static");
|
||||
webJarLoader.setServeStaticBrowserCache("true");
|
||||
webJarLoader.setEncoding("UTF-8");
|
||||
org.apache.struts2.webjars.DefaultWebJarUrlProvider provider =
|
||||
new org.apache.struts2.webjars.DefaultWebJarUrlProvider();
|
||||
provider.setEnabled(String.valueOf(enabled));
|
||||
provider.setAllowlist("");
|
||||
provider.setStaticContentPath("/static");
|
||||
webJarLoader.setWebJarUrlProvider(provider);
|
||||
return webJarLoader;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void servesKnownWebJarAssetWithContentType() throws Exception {
|
||||
DefaultStaticContentLoader webJarLoader = newLoader(true);
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||
ByteArrayOutputStream captured = new ByteArrayOutputStream();
|
||||
when(response.getOutputStream())
|
||||
.thenReturn(new WebJarTestServletOutputStream(captured));
|
||||
|
||||
webJarLoader.findStaticResource("/static/webjars/jquery/jquery.min.js", request, response);
|
||||
|
||||
verify(response).setContentType("text/javascript");
|
||||
verify(response, never())
|
||||
.sendError(HttpServletResponse.SC_NOT_FOUND);
|
||||
assertThat(captured.size()).isGreaterThan(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknownWebJarAssetReturns404() throws Exception {
|
||||
DefaultStaticContentLoader webJarLoader = newLoader(true);
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||
|
||||
webJarLoader.findStaticResource("/static/webjars/nope/nope.js", request, response);
|
||||
|
||||
verify(response).sendError(HttpServletResponse.SC_NOT_FOUND);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void disabledWebJarsReturns404() throws Exception {
|
||||
DefaultStaticContentLoader webJarLoader = newLoader(false);
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||
|
||||
webJarLoader.findStaticResource("/static/webjars/jquery/jquery.min.js", request, response);
|
||||
|
||||
verify(response).sendError(HttpServletResponse.SC_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.struts2.dispatcher;
|
||||
|
||||
import jakarta.servlet.ServletOutputStream;
|
||||
import jakarta.servlet.WriteListener;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/** Minimal ServletOutputStream backed by an OutputStream, for tests. */
|
||||
public class WebJarTestServletOutputStream extends ServletOutputStream {
|
||||
|
||||
private final OutputStream delegate;
|
||||
|
||||
public WebJarTestServletOutputStream(OutputStream delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
delegate.write(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setWriteListener(WriteListener writeListener) {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.struts2.webjars;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class DefaultWebJarUrlProviderTest {
|
||||
|
||||
private DefaultWebJarUrlProvider provider;
|
||||
private HttpServletRequest request;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
provider = new DefaultWebJarUrlProvider();
|
||||
provider.setEnabled("true");
|
||||
provider.setAllowlist("");
|
||||
provider.setStaticContentPath("/static");
|
||||
request = mock(HttpServletRequest.class);
|
||||
when(request.getContextPath()).thenReturn("/myapp");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolvesKnownResourceToVersionedClasspathPath() {
|
||||
assertThat(provider.resolveResourcePath("jquery/jquery.min.js"))
|
||||
.hasValueSatisfying(p -> assertThat(p)
|
||||
.startsWith("META-INF/resources/webjars/jquery/")
|
||||
.endsWith("/jquery.min.js"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolvesKnownResourceToServableUrl() {
|
||||
assertThat(provider.resolveUrl("jquery/jquery.min.js", request))
|
||||
.hasValueSatisfying(u -> assertThat(u)
|
||||
.startsWith("/myapp/static/webjars/jquery/")
|
||||
.endsWith("/jquery.min.js"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknownWebjarResolvesEmpty() {
|
||||
assertThat(provider.resolveResourcePath("no-such-lib/x.js")).isEmpty();
|
||||
assertThat(provider.resolveUrl("no-such-lib/x.js", request)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void traversalIsRejected() {
|
||||
assertThat(provider.resolveResourcePath("jquery/../../../etc/passwd")).isEmpty();
|
||||
assertThat(provider.resolveResourcePath("../jquery/jquery.min.js")).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveUrlRejectsTraversal() {
|
||||
assertThat(provider.resolveUrl("jquery/../../../etc/passwd", request)).isEmpty();
|
||||
assertThat(provider.resolveUrl("../jquery/jquery.min.js", request)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allowlistBlocksNonListedWebjar() {
|
||||
provider.setAllowlist("bootstrap");
|
||||
assertThat(provider.resolveResourcePath("jquery/jquery.min.js")).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allowlistPermitsListedWebjar() {
|
||||
provider.setAllowlist("jquery, bootstrap");
|
||||
assertThat(provider.resolveResourcePath("jquery/jquery.min.js")).isPresent();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void disabledResolvesEmpty() {
|
||||
provider.setEnabled("false");
|
||||
assertThat(provider.isEnabled()).isFalse();
|
||||
assertThat(provider.resolveResourcePath("jquery/jquery.min.js")).isEmpty();
|
||||
assertThat(provider.resolveUrl("jquery/jquery.min.js", request)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rootContextPathIsNotDuplicated() {
|
||||
when(request.getContextPath()).thenReturn("/");
|
||||
assertThat(provider.resolveUrl("jquery/jquery.min.js", request))
|
||||
.hasValueSatisfying(u -> assertThat(u).startsWith("/static/webjars/jquery/"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blankOrSingleSegmentPathResolvesEmpty() {
|
||||
assertThat(provider.resolveResourcePath("")).isEmpty();
|
||||
assertThat(provider.resolveResourcePath("jquery")).isEmpty();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user