JSInclude.java
/*
* #%L
* wcm.io
* %%
* Copyright (C) 2019 wcm.io
* %%
* Licensed 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.
* #L%
*/
package io.wcm.wcm.ui.clientlibs.components;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.PostConstruct;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.InjectionStrategy;
import org.apache.sling.models.annotations.injectorspecific.OSGiService;
import org.apache.sling.models.annotations.injectorspecific.RequestAttribute;
import org.apache.sling.models.annotations.injectorspecific.SlingObject;
import org.apache.sling.xss.XSSAPI;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.osgi.annotation.versioning.ProviderType;
import com.adobe.granite.ui.clientlibs.HtmlLibraryManager;
import com.adobe.granite.ui.clientlibs.LibraryType;
/**
* Include JavaScript client libraries with optional attributes for script tag.
*/
@Model(adaptables = SlingHttpServletRequest.class)
@ProviderType
public class JSInclude {
private static final Set<String> CROSSORIGIN_ALLOWED_VALUES = Set.of(
"anonymous", "use-credentials");
private static final Set<String> REFERRERPOLICY_ALLOWED_VALUES = Set.of(
"no-referrer", "no-referrer-when-downgrade", "origin", "origin-when-cross-origin",
"same-origin", "strict-origin", "strict-origin-when-cross-origin", "unsafe-url");
private static final Set<String> TYPE_ALLOWED_VALUES = Set.of(
"text/javascript", "module");
@SlingObject
private SlingHttpServletRequest request;
@SlingObject
private ResourceResolver resourceResolver;
@OSGiService
private HtmlLibraryManager htmlLibraryManager;
@OSGiService
private XSSAPI xssApi;
@RequestAttribute(injectionStrategy = InjectionStrategy.OPTIONAL)
private Object categories;
@RequestAttribute(injectionStrategy = InjectionStrategy.OPTIONAL)
private boolean async;
@RequestAttribute(injectionStrategy = InjectionStrategy.OPTIONAL)
private String crossorigin;
@RequestAttribute(injectionStrategy = InjectionStrategy.OPTIONAL)
private boolean defer;
@RequestAttribute(injectionStrategy = InjectionStrategy.OPTIONAL)
private String integrity;
@RequestAttribute(injectionStrategy = InjectionStrategy.OPTIONAL)
private boolean nomodule;
@RequestAttribute(injectionStrategy = InjectionStrategy.OPTIONAL)
private String nonce;
@RequestAttribute(injectionStrategy = InjectionStrategy.OPTIONAL)
private String referrerpolicy;
@RequestAttribute(injectionStrategy = InjectionStrategy.OPTIONAL)
private String type;
@RequestAttribute(injectionStrategy = InjectionStrategy.OPTIONAL)
private Object allowMultipleIncludes;
@RequestAttribute(injectionStrategy = InjectionStrategy.OPTIONAL)
private Object customAttributes;
private String include;
@PostConstruct
private void activate() {
// build include string
String[] categoryArray = IncludeUtil.toArray(categories);
if (categoryArray != null) {
List<String> libraryPaths = IncludeUtil.getLibraryUrls(htmlLibraryManager, resourceResolver,
categoryArray, LibraryType.JS);
if (!libraryPaths.isEmpty()) {
Map<String, String> attrs = validateAndBuildAttributes();
Map<String, String> customAttrs = IncludeUtil.getCustomAttributes(customAttributes);
this.include = buildIncludeString(libraryPaths, attrs, customAttrs);
}
}
}
/**
* Validate attribute values from HTL script, escape them properly and build a map with all attributes
* for the resulting script tag(s).
* @return Map with attribute for script tag
*/
private @NotNull Map<String, String> validateAndBuildAttributes() {
Map<String, String> attrs = new HashMap<>();
if (async) {
attrs.put("async", null);
}
if (crossorigin != null && CROSSORIGIN_ALLOWED_VALUES.contains(crossorigin)) {
attrs.put("crossorigin", crossorigin);
}
if (defer) {
attrs.put("defer", null);
}
if (StringUtils.isNotEmpty(integrity)) {
attrs.put("integrity", xssApi.encodeForHTMLAttr(integrity));
}
if (nomodule) {
attrs.put("nomodule", null);
}
if (StringUtils.isNotEmpty(nonce)) {
attrs.put("nonce", xssApi.encodeForHTMLAttr(nonce));
}
if (referrerpolicy != null && REFERRERPOLICY_ALLOWED_VALUES.contains(referrerpolicy)) {
attrs.put("referrerpolicy", referrerpolicy);
}
if (type != null && TYPE_ALLOWED_VALUES.contains(type)) {
attrs.put("type", type);
}
return attrs;
}
/**
* Build script tags for all client libraries with the defined custom script tag attributes set.
* @param libraryPaths Library paths
* @param attrs HTML attributes for script tag
* @param customAttrs Custom HTML attributes for script tag
* @return HTML markup with script tags
*/
private @NotNull String buildIncludeString(@NotNull List<String> libraryPaths, @NotNull Map<String, String> attrs,
@NotNull Map<String, String> customAttrs) {
return new RequestIncludedLibraries(request, allowMultipleIncludes)
.buildMarkupIgnoringDuplicateLibraries(libraryPaths, libraryPath -> {
HtmlTagBuilder builder = new HtmlTagBuilder("script", true, xssApi);
builder.setAttrs(attrs);
builder.setAttrs(customAttrs);
builder.setAttr("src", IncludeUtil.appendRequestPath(libraryPath, request));
return builder;
});
}
/**
* @return HTML markup with script tags
*/
public @Nullable String getInclude() {
return include;
}
}