PageRenderTime 45ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/plugin/src/main/java/com/atlassian/confluence/plugins/macros/html/WhitelistedHttpRetrievalMacro.java

https://bitbucket.org/atlassian/confluence-html-macros
Java | 321 lines | 213 code | 34 blank | 74 comment | 26 complexity | 187e5648dce94bea138abb5ffed04770 MD5 | raw file
  1. package com.atlassian.confluence.plugins.macros.html;
  2. import com.atlassian.applinks.api.ReadOnlyApplicationLink;
  3. import com.atlassian.applinks.api.ReadOnlyApplicationLinkService;
  4. import com.atlassian.confluence.content.render.xhtml.ConversionContext;
  5. import com.atlassian.confluence.languages.LocaleManager;
  6. import com.atlassian.confluence.macro.Macro;
  7. import com.atlassian.confluence.macro.MacroExecutionException;
  8. import com.atlassian.confluence.plugin.services.VelocityHelperService;
  9. import com.atlassian.confluence.user.AuthenticatedUserThreadLocal;
  10. import com.atlassian.confluence.util.i18n.I18NBean;
  11. import com.atlassian.confluence.util.i18n.I18NBeanFactory;
  12. import com.atlassian.plugins.whitelist.OutboundWhitelist;
  13. import com.atlassian.renderer.RenderContext;
  14. import com.atlassian.renderer.TokenType;
  15. import com.atlassian.renderer.v2.RenderMode;
  16. import com.atlassian.renderer.v2.RenderUtils;
  17. import com.atlassian.renderer.v2.macro.BaseMacro;
  18. import com.atlassian.renderer.v2.macro.MacroException;
  19. import com.atlassian.sal.api.net.NonMarshallingRequestFactory;
  20. import com.atlassian.sal.api.net.Request;
  21. import com.atlassian.sal.api.net.Response;
  22. import com.atlassian.sal.api.net.ResponseException;
  23. import com.atlassian.sal.api.user.UserKey;
  24. import com.atlassian.sal.api.user.UserManager;
  25. import org.apache.commons.lang3.StringUtils;
  26. import org.slf4j.Logger;
  27. import org.slf4j.LoggerFactory;
  28. import org.springframework.util.Assert;
  29. import java.net.URI;
  30. import java.net.URISyntaxException;
  31. import java.util.Comparator;
  32. import java.util.List;
  33. import java.util.Map;
  34. import java.util.Optional;
  35. import java.util.concurrent.atomic.AtomicReference;
  36. import java.util.function.Predicate;
  37. import java.util.stream.Stream;
  38. import java.util.stream.StreamSupport;
  39. import static java.util.Collections.singletonList;
  40. /**
  41. * This class is an abstract class that provides the template for macro implementations
  42. * that renders external content (from HTTP sources) and supported for trusted apps.
  43. *
  44. * {@link #successfulResponse(java.util.Map, com.atlassian.confluence.content.render.xhtml.ConversionContext, String, com.atlassian.sal.api.net.Response)}
  45. */
  46. abstract class WhitelistedHttpRetrievalMacro extends BaseMacro implements Macro
  47. {
  48. private static final Logger log = LoggerFactory.getLogger(WhitelistedHttpRetrievalMacro.class);
  49. private static final String WHITELIST_ERROR_TEMPLATE = "com/atlassian/confluence/plugins/macros/html/whitelist-error.vm";
  50. private final LocaleManager localeManager;
  51. private final I18NBeanFactory i18NBeanFactory;
  52. private final NonMarshallingRequestFactory<Request<?, Response>> requestFactory;
  53. private final ReadOnlyApplicationLinkService applicationLinkService;
  54. private final OutboundWhitelist whitelist;
  55. private final UserManager userManager;
  56. private final VelocityHelperService velocityHelperService;
  57. protected WhitelistedHttpRetrievalMacro(
  58. LocaleManager localeManager,
  59. I18NBeanFactory i18NBeanFactory,
  60. NonMarshallingRequestFactory<Request<?, Response>> requestFactory,
  61. ReadOnlyApplicationLinkService applicationLinkService,
  62. OutboundWhitelist whitelist,
  63. UserManager userManager,
  64. VelocityHelperService velocityHelperService) {
  65. this.localeManager = localeManager;
  66. this.i18NBeanFactory = i18NBeanFactory;
  67. this.requestFactory = requestFactory;
  68. this.applicationLinkService = applicationLinkService;
  69. this.whitelist = whitelist;
  70. this.userManager = userManager;
  71. this.velocityHelperService = velocityHelperService;
  72. }
  73. protected String getText(String i18nKey, List<String> substitution) {
  74. return getI18nBean().getText(i18nKey, substitution);
  75. }
  76. private I18NBean getI18nBean() {
  77. return i18NBeanFactory.getI18NBean(localeManager.getLocale(AuthenticatedUserThreadLocal.get()));
  78. }
  79. protected String getText(String i18nKey) {
  80. return getI18nBean().getText(i18nKey);
  81. }
  82. @Override
  83. public TokenType getTokenType(Map parameters, String body, RenderContext context) {
  84. return TokenType.BLOCK;
  85. }
  86. @Override
  87. public final boolean hasBody() {
  88. return false;
  89. }
  90. @Override
  91. public final RenderMode getBodyRenderMode() {
  92. return RenderMode.NO_RENDER;
  93. }
  94. @Override
  95. public BodyType getBodyType() {
  96. return BodyType.NONE;
  97. }
  98. @Override
  99. public OutputType getOutputType() {
  100. return OutputType.BLOCK;
  101. }
  102. /**
  103. * Encodes common unsafe characters. It converts the following characters:
  104. * <ol>
  105. * <li><code>\(</code> to <code>%28</code></li>
  106. * <li><code>\)</code> to <code>%29</code></li>
  107. * <li><code>&amp;amp;</code> to <code>&amp;</code></li>
  108. * </ol>
  109. * @param url
  110. * The URL to be encoded
  111. * @return
  112. * The encoded URL.
  113. */
  114. private static String cleanupUrl(String url)
  115. {
  116. if (url.indexOf('(') > 0) {
  117. url = url.replaceAll("\\(", "%28");
  118. }
  119. if (url.indexOf(')') > 0) {
  120. url = url.replaceAll("\\)", "%29");
  121. }
  122. if (url.indexOf("&amp;") > 0) {
  123. url = url.replaceAll("&amp;", "&");
  124. }
  125. return url;
  126. }
  127. /**
  128. * Generates HTML showing white list failures
  129. * @param url
  130. * The URL denied by the white list.
  131. * @return
  132. * The generated HTML.
  133. * @throws MacroExecutionException
  134. * Thrown if there is any error in generating the HTML with the template {@link #WHITELIST_ERROR_TEMPLATE}
  135. */
  136. private String renderDeniedByWhiteListConfiguration(String url) throws MacroExecutionException {
  137. // Generate error html
  138. Map<String, Object> contextMap = velocityHelperService.createDefaultVelocityContext();
  139. contextMap.put("invalidURL", "true");
  140. contextMap.put("url", url);
  141. contextMap.put("remoteUser", AuthenticatedUserThreadLocal.get());
  142. try {
  143. return velocityHelperService.getRenderedTemplate(WHITELIST_ERROR_TEMPLATE, contextMap);
  144. }
  145. catch (Exception e) {
  146. log.error("Error while trying to display whitelist error!", e);
  147. throw new MacroExecutionException(e.getMessage());
  148. }
  149. }
  150. /**
  151. * Generates an invalid URL error message in HTML.
  152. * @param url
  153. * The URL.
  154. * @return
  155. * The generated HTML.
  156. */
  157. private String notFound(String url) {
  158. return RenderUtils.blockError(getText("whitelistedmacro.error.notfound", singletonList(url)), "");
  159. }
  160. /**
  161. * Generates a &quot;insufficient permission&quot; error message in HTML.
  162. * @param url
  163. * The URL.
  164. * @return
  165. * The generated HTML.
  166. */
  167. private String notPermitted(String url) {
  168. return RenderUtils.blockError(getText("whitelistedmacro.error.notpermitted", singletonList(url)), "");
  169. }
  170. /**
  171. * Generates a generic failure error message in HTML.
  172. * @param url
  173. * The URL.
  174. * @param statusMessage
  175. * Additional error detail. This should be the status message of the HTTP response.
  176. * @return
  177. * The generated HTML.
  178. */
  179. private String failed(String url, String statusMessage)
  180. {
  181. return RenderUtils.blockError(
  182. getText("whitelistedmacro.error.notpermitted", singletonList(url)),
  183. statusMessage
  184. );
  185. }
  186. /**
  187. * This will be called when the request is truly successful.
  188. * @param parameters
  189. * The macro parameters passed from {@link #execute(java.util.Map, String, com.atlassian.renderer.RenderContext)}.
  190. * @param renderContext
  191. * The rendering context passed from {@link #execute(java.util.Map, String, com.atlassian.renderer.RenderContext)}.
  192. * @param url
  193. * The user specified URL.
  194. * @param response
  195. * The response generated for the request.
  196. * @return
  197. * Implementations should return the HTML as if this method is {@link #execute(java.util.Map, String, com.atlassian.renderer.RenderContext)}
  198. * @throws MacroExecutionException
  199. * Implementations should throw this exception to indicate errors.
  200. *
  201. * @since 4.0.0
  202. */
  203. protected abstract String successfulResponse(Map<String, String> parameters, ConversionContext renderContext, String url, Response response) throws MacroExecutionException;
  204. @Override
  205. public String execute(Map<String, String> typeSafeMacroParams, String body, ConversionContext conversionContext) throws MacroExecutionException
  206. {
  207. // Check with the whitelist manager to see if the source URL is valid
  208. final String url = cleanupUrl(StringUtils.defaultString(typeSafeMacroParams.get("0"), StringUtils.defaultString(typeSafeMacroParams.get("url"))));
  209. if (StringUtils.isBlank(url)) {
  210. return RenderUtils.error(getText("whitelistedmacro.error.nourl"));
  211. }
  212. final URI uri = toURI(url);
  213. UserKey userKey = userManager.getRemoteUserKey();
  214. if (uri == null || !whitelist.isAllowed(uri, userKey)) {
  215. /* 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),
  216. * I still think that all this rendering should be done in a macro.
  217. * Also, after this refactoring, the only macros that can inherit from this are those in this plugin.
  218. * Therefore, if there are obvious benefits of moving the code in the method below to a separate class, it can be done easily.
  219. */
  220. return renderDeniedByWhiteListConfiguration(url);
  221. } else {
  222. final Optional<ReadOnlyApplicationLink> applicationLink = findApplicationLinkByUrl(url);
  223. try {
  224. final Request<?, Response> request = applicationLink.isPresent()
  225. ? applicationLink.get().createAuthenticatedRequestFactory().createRequest(Request.MethodType.GET, url)
  226. : requestFactory.createRequest(Request.MethodType.GET, url);
  227. return executeRequest(typeSafeMacroParams, conversionContext, url, request);
  228. } catch (Exception e) {
  229. throw new MacroExecutionException(e);
  230. }
  231. }
  232. }
  233. private String executeRequest(Map<String, String> typeSafeMacroParams, ConversionContext conversionContext, String url, Request<?, Response> request) throws ResponseException {
  234. Assert.notNull(request, "request must not be null");
  235. final AtomicReference<String> result = new AtomicReference<>();
  236. request.execute(response -> {
  237. if (response.getStatusCode() == 404) {
  238. result.set(notFound(url));
  239. } else if (response.getStatusCode() == 401 || response.getStatusCode() == 403) {
  240. result.set(notPermitted(url));
  241. } else if (response.getStatusCode() < 200 || response.getStatusCode() > 299) {
  242. result.set(failed(url, response.getStatusText()));
  243. } else {
  244. try {
  245. result.set(successfulResponse(typeSafeMacroParams, conversionContext, url, response));
  246. } catch (MacroExecutionException e) {
  247. throw new ResponseException(e);
  248. }
  249. }
  250. });
  251. return result.get();
  252. }
  253. private Optional<ReadOnlyApplicationLink> findApplicationLinkByUrl(String url) {
  254. final String lowerUrl = url.toLowerCase();
  255. final Stream<ReadOnlyApplicationLink> targetStream = StreamSupport.stream(applicationLinkService.getApplicationLinks().spliterator(), true);
  256. Predicate<ReadOnlyApplicationLink> filter = link -> {
  257. if (link == null) {
  258. return false;
  259. }
  260. final URI displayLinkUri = link.getDisplayUrl();
  261. if (displayLinkUri == null) {
  262. return false;
  263. }
  264. final String displayLinkUrl = displayLinkUri.toString();
  265. if (displayLinkUrl == null) {
  266. return false;
  267. }
  268. return displayLinkUrl.length() > 0 && lowerUrl.startsWith(displayLinkUrl.toLowerCase());
  269. };
  270. return targetStream.filter(filter).max(Comparator.comparingInt(o -> o.getDisplayUrl().toString().length()));
  271. }
  272. @Override
  273. public String execute(Map parameters, String body, RenderContext renderContext) throws MacroException {
  274. try {
  275. return execute(parameters, body, (ConversionContext) null);
  276. } catch (MacroExecutionException e) {
  277. throw new MacroException(e);
  278. }
  279. }
  280. private static URI toURI(final String str) {
  281. try {
  282. return new URI(str);
  283. } catch (URISyntaxException e) {
  284. return null;
  285. }
  286. }
  287. }