/plugin/src/main/java/com/atlassian/confluence/plugins/macros/html/WhitelistedHttpRetrievalMacro.java
Java | 321 lines | 213 code | 34 blank | 74 comment | 26 complexity | 187e5648dce94bea138abb5ffed04770 MD5 | raw file
- package com.atlassian.confluence.plugins.macros.html;
- import com.atlassian.applinks.api.ReadOnlyApplicationLink;
- import com.atlassian.applinks.api.ReadOnlyApplicationLinkService;
- import com.atlassian.confluence.content.render.xhtml.ConversionContext;
- import com.atlassian.confluence.languages.LocaleManager;
- import com.atlassian.confluence.macro.Macro;
- import com.atlassian.confluence.macro.MacroExecutionException;
- import com.atlassian.confluence.plugin.services.VelocityHelperService;
- import com.atlassian.confluence.user.AuthenticatedUserThreadLocal;
- import com.atlassian.confluence.util.i18n.I18NBean;
- import com.atlassian.confluence.util.i18n.I18NBeanFactory;
- import com.atlassian.plugins.whitelist.OutboundWhitelist;
- import com.atlassian.renderer.RenderContext;
- import com.atlassian.renderer.TokenType;
- import com.atlassian.renderer.v2.RenderMode;
- import com.atlassian.renderer.v2.RenderUtils;
- import com.atlassian.renderer.v2.macro.BaseMacro;
- import com.atlassian.renderer.v2.macro.MacroException;
- import com.atlassian.sal.api.net.NonMarshallingRequestFactory;
- import com.atlassian.sal.api.net.Request;
- import com.atlassian.sal.api.net.Response;
- import com.atlassian.sal.api.net.ResponseException;
- import com.atlassian.sal.api.user.UserKey;
- import com.atlassian.sal.api.user.UserManager;
- import org.apache.commons.lang3.StringUtils;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import org.springframework.util.Assert;
- import java.net.URI;
- import java.net.URISyntaxException;
- import java.util.Comparator;
- import java.util.List;
- import java.util.Map;
- import java.util.Optional;
- import java.util.concurrent.atomic.AtomicReference;
- import java.util.function.Predicate;
- import java.util.stream.Stream;
- import java.util.stream.StreamSupport;
- import static java.util.Collections.singletonList;
- /**
- * This class is an abstract class that provides the template for macro implementations
- * that renders external content (from HTTP sources) and supported for trusted apps.
- *
- * {@link #successfulResponse(java.util.Map, com.atlassian.confluence.content.render.xhtml.ConversionContext, String, com.atlassian.sal.api.net.Response)}
- */
- abstract class WhitelistedHttpRetrievalMacro extends BaseMacro implements Macro
- {
- private static final Logger log = LoggerFactory.getLogger(WhitelistedHttpRetrievalMacro.class);
- private static final String WHITELIST_ERROR_TEMPLATE = "com/atlassian/confluence/plugins/macros/html/whitelist-error.vm";
- private final LocaleManager localeManager;
- private final I18NBeanFactory i18NBeanFactory;
- private final NonMarshallingRequestFactory<Request<?, Response>> requestFactory;
- private final ReadOnlyApplicationLinkService applicationLinkService;
- private final OutboundWhitelist whitelist;
- private final UserManager userManager;
- private final VelocityHelperService velocityHelperService;
- protected WhitelistedHttpRetrievalMacro(
- LocaleManager localeManager,
- I18NBeanFactory i18NBeanFactory,
- NonMarshallingRequestFactory<Request<?, Response>> requestFactory,
- ReadOnlyApplicationLinkService applicationLinkService,
- OutboundWhitelist whitelist,
- UserManager userManager,
- VelocityHelperService velocityHelperService) {
- this.localeManager = localeManager;
- this.i18NBeanFactory = i18NBeanFactory;
- this.requestFactory = requestFactory;
- this.applicationLinkService = applicationLinkService;
- this.whitelist = whitelist;
- this.userManager = userManager;
- this.velocityHelperService = velocityHelperService;
- }
- protected String getText(String i18nKey, List<String> substitution) {
- return getI18nBean().getText(i18nKey, substitution);
- }
- private I18NBean getI18nBean() {
- return i18NBeanFactory.getI18NBean(localeManager.getLocale(AuthenticatedUserThreadLocal.get()));
- }
- protected String getText(String i18nKey) {
- return getI18nBean().getText(i18nKey);
- }
- @Override
- public TokenType getTokenType(Map parameters, String body, RenderContext context) {
- return TokenType.BLOCK;
- }
- @Override
- public final boolean hasBody() {
- return false;
- }
- @Override
- public final RenderMode getBodyRenderMode() {
- return RenderMode.NO_RENDER;
- }
- @Override
- public BodyType getBodyType() {
- return BodyType.NONE;
- }
- @Override
- public OutputType getOutputType() {
- return OutputType.BLOCK;
- }
- /**
- * Encodes common unsafe characters. It converts the following characters:
- * <ol>
- * <li><code>\(</code> to <code>%28</code></li>
- * <li><code>\)</code> to <code>%29</code></li>
- * <li><code>&amp;</code> to <code>&</code></li>
- * </ol>
- * @param url
- * The URL to be encoded
- * @return
- * The encoded URL.
- */
- private static String cleanupUrl(String url)
- {
- if (url.indexOf('(') > 0) {
- url = url.replaceAll("\\(", "%28");
- }
- if (url.indexOf(')') > 0) {
- url = url.replaceAll("\\)", "%29");
- }
- if (url.indexOf("&") > 0) {
- url = url.replaceAll("&", "&");
- }
- return url;
- }
- /**
- * Generates HTML showing white list failures
- * @param url
- * The URL denied by the white list.
- * @return
- * The generated HTML.
- * @throws MacroExecutionException
- * Thrown if there is any error in generating the HTML with the template {@link #WHITELIST_ERROR_TEMPLATE}
- */
- private String renderDeniedByWhiteListConfiguration(String url) throws MacroExecutionException {
- // Generate error html
- Map<String, Object> contextMap = velocityHelperService.createDefaultVelocityContext();
- contextMap.put("invalidURL", "true");
- contextMap.put("url", url);
- contextMap.put("remoteUser", AuthenticatedUserThreadLocal.get());
- try {
- return velocityHelperService.getRenderedTemplate(WHITELIST_ERROR_TEMPLATE, contextMap);
- }
- catch (Exception e) {
- log.error("Error while trying to display whitelist error!", e);
- throw new MacroExecutionException(e.getMessage());
- }
- }
- /**
- * Generates an invalid URL error message in HTML.
- * @param url
- * The URL.
- * @return
- * The generated HTML.
- */
- private String notFound(String url) {
- return RenderUtils.blockError(getText("whitelistedmacro.error.notfound", singletonList(url)), "");
- }
- /**
- * Generates a "insufficient permission" error message in HTML.
- * @param url
- * The URL.
- * @return
- * The generated HTML.
- */
- private String notPermitted(String url) {
- return RenderUtils.blockError(getText("whitelistedmacro.error.notpermitted", singletonList(url)), "");
- }
- /**
- * Generates a generic failure error message in HTML.
- * @param url
- * The URL.
- * @param statusMessage
- * Additional error detail. This should be the status message of the HTTP response.
- * @return
- * The generated HTML.
- */
- private String failed(String url, String statusMessage)
- {
- return RenderUtils.blockError(
- getText("whitelistedmacro.error.notpermitted", singletonList(url)),
- statusMessage
- );
- }
- /**
- * This will be called when the request is truly successful.
- * @param parameters
- * The macro parameters passed from {@link #execute(java.util.Map, String, com.atlassian.renderer.RenderContext)}.
- * @param renderContext
- * The rendering context passed from {@link #execute(java.util.Map, String, com.atlassian.renderer.RenderContext)}.
- * @param url
- * The user specified URL.
- * @param response
- * The response generated for the request.
- * @return
- * Implementations should return the HTML as if this method is {@link #execute(java.util.Map, String, com.atlassian.renderer.RenderContext)}
- * @throws MacroExecutionException
- * Implementations should throw this exception to indicate errors.
- *
- * @since 4.0.0
- */
- protected abstract String successfulResponse(Map<String, String> parameters, ConversionContext renderContext, String url, Response response) throws MacroExecutionException;
- @Override
- public String execute(Map<String, String> typeSafeMacroParams, String body, ConversionContext conversionContext) throws MacroExecutionException
- {
- // Check with the whitelist manager to see if the source URL is valid
- final String url = cleanupUrl(StringUtils.defaultString(typeSafeMacroParams.get("0"), StringUtils.defaultString(typeSafeMacroParams.get("url"))));
- if (StringUtils.isBlank(url)) {
- return RenderUtils.error(getText("whitelistedmacro.error.nourl"));
- }
- final URI uri = toURI(url);
- UserKey userKey = userManager.getRemoteUserKey();
- if (uri == null || !whitelist.isAllowed(uri, userKey)) {
- /* While it might be a good idea to move the code in the method below to another class sharable by HTTP retrieval macros (html-include, rss),
- * I still think that all this rendering should be done in a macro.
- * Also, after this refactoring, the only macros that can inherit from this are those in this plugin.
- * Therefore, if there are obvious benefits of moving the code in the method below to a separate class, it can be done easily.
- */
- return renderDeniedByWhiteListConfiguration(url);
- } else {
- final Optional<ReadOnlyApplicationLink> applicationLink = findApplicationLinkByUrl(url);
- try {
- final Request<?, Response> request = applicationLink.isPresent()
- ? applicationLink.get().createAuthenticatedRequestFactory().createRequest(Request.MethodType.GET, url)
- : requestFactory.createRequest(Request.MethodType.GET, url);
- return executeRequest(typeSafeMacroParams, conversionContext, url, request);
- } catch (Exception e) {
- throw new MacroExecutionException(e);
- }
- }
- }
- private String executeRequest(Map<String, String> typeSafeMacroParams, ConversionContext conversionContext, String url, Request<?, Response> request) throws ResponseException {
- Assert.notNull(request, "request must not be null");
- final AtomicReference<String> result = new AtomicReference<>();
- request.execute(response -> {
- if (response.getStatusCode() == 404) {
- result.set(notFound(url));
- } else if (response.getStatusCode() == 401 || response.getStatusCode() == 403) {
- result.set(notPermitted(url));
- } else if (response.getStatusCode() < 200 || response.getStatusCode() > 299) {
- result.set(failed(url, response.getStatusText()));
- } else {
- try {
- result.set(successfulResponse(typeSafeMacroParams, conversionContext, url, response));
- } catch (MacroExecutionException e) {
- throw new ResponseException(e);
- }
- }
- });
- return result.get();
- }
- private Optional<ReadOnlyApplicationLink> findApplicationLinkByUrl(String url) {
- final String lowerUrl = url.toLowerCase();
- final Stream<ReadOnlyApplicationLink> targetStream = StreamSupport.stream(applicationLinkService.getApplicationLinks().spliterator(), true);
- Predicate<ReadOnlyApplicationLink> filter = link -> {
- if (link == null) {
- return false;
- }
- final URI displayLinkUri = link.getDisplayUrl();
- if (displayLinkUri == null) {
- return false;
- }
- final String displayLinkUrl = displayLinkUri.toString();
- if (displayLinkUrl == null) {
- return false;
- }
- return displayLinkUrl.length() > 0 && lowerUrl.startsWith(displayLinkUrl.toLowerCase());
- };
- return targetStream.filter(filter).max(Comparator.comparingInt(o -> o.getDisplayUrl().toString().length()));
- }
- @Override
- public String execute(Map parameters, String body, RenderContext renderContext) throws MacroException {
- try {
- return execute(parameters, body, (ConversionContext) null);
- } catch (MacroExecutionException e) {
- throw new MacroException(e);
- }
- }
- private static URI toURI(final String str) {
- try {
- return new URI(str);
- } catch (URISyntaxException e) {
- return null;
- }
- }
- }