tohokuaikiのチラシの裏

技術的ネタとか。

Confluenceのweb-panelプラグインにて、VelocityテンプレートにHTMLエスケープされないでテンプレート変数をアサインしてみる

Confluenceでプラグイン開発してると、web-panelでテンプレートを使うのだけど、これにアサインする変数はすべてhtmlエスケープされる。「"」だと、「"」となる。たいがいの場合はこれでいいのだけど、JSONをアサインする際に困った。

アサインするのは、ContextProviderです。
atlassian-plugin.xml

  <web-panel key="foo-panel" location="atl.header">
    <resource name="view" type="velocity" location="templates/foo.vm"/>
    <context-provider class="jp.junoe.confluence.plugins.FooContextProvider"/>
  </web-panel>

でFooContextProvider::getContextMap()をごにょぼにょする。

そこでいくつか方法を試してみた。

結論から言うと、まだ成功してない。だけ見たい方は最後に。

jsonatorを使ってみた

テンプレート変数には、$jsonatorというのがある。このあたりを見てなんとなく
https://developer.atlassian.com/display/CONFDEV/Generating+JSON+output+in+Confluence+with+Jsonator
https://docs.atlassian.com/atlassian-confluence/latest/com/atlassian/confluence/json/jsonator/Jsonator.html

$jsonator.convert($obj).serialize()

ってテンプレートに書いてみた。$objは

        Map<String, String> bean = new HashMap<String, String>();
        bean.put("history", "my history");
        context.put("obj", bean);

という感じ。

結果

JSONに展開してくれたのだけど、展開後のJSONをエスケープしてくれたのでダメだった。どうやら、出力の最後の最後でエスケープするらしい。テンプレート関数を使ってもダメ。

ちなみに、テンプレートで使えそうなGeneralUtilっていうのを使ってみる方法をこのとき見つけた。
https://docs.atlassian.com/atlassian-confluence/latest/index.html?com/atlassian/confluence/util/GeneralUtil.html

web-panel-rendererを使ってみる。

これはまだ途中。


デフォルトで与えられているテンプレートレンダラーを使うからよくない。
そこで、新しくレンダラーを作る。
レンダラーモジュールについてはこちら。
Web Panel Renderer Plugin Module - Confluence Development - Atlassian Developer Documentation

コマンドプロンプトからモジュールを追加する | ナレッジオンデマンド株式会社
を参考にしてweb-panel-rendererモジュール追加。
atlassian-plugin.xml

  <web-panel-renderer
	  name="Foo Property Web Panel Renderer"
	  i18n-name-key="foo-property-web-panel-renderer.name"
	  key="foo-property-web-panel-renderer"
	  class="jp.junoe.confluence.plugins.web.FooPropertyWebPanelRenderer">
	  <description key="foo-property-web-panel-renderer.description">
		  The Foo Property Web Panel Renderer Plugin</description>
  </web-panel-renderer>

って感じ。

で、肝心のクラスには ConfluenceAwareWebPanelRenderer をExtendsして作る。ConfluenceAwareWebPanelRenderer はConfluenceのVelocityのデフォルトレンダラー。

package jp.junoe.confluence.plugins.web;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.atlassian.confluence.renderer.radeox.macros.MacroUtils;
import com.atlassian.confluence.util.collections.CompositeMap;
import com.atlassian.confluence.util.velocity.VelocityUtils;
import com.atlassian.plugin.Plugin;
import com.atlassian.plugin.web.renderer.RendererException;
import com.atlassian.plugin.web.renderer.WebPanelRenderer;

import com.atlassian.confluence.plugin.web.renderer.ConfluenceAwareWebPanelRenderer;

import java.io.IOException; 
import java.io.Writer;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;

import org.apache.velocity.app.VelocityEngine

/**
 * Renders a web panel in the context of Confluence.
 * This makes core velocity macros and templates available for use in the velocity template
 */
public class FooPropertyWebPanelRenderer extends ConfluenceAwareWebPanelRenderer implements WebPanelRenderer 
{
    
    private static final Logger log = LoggerFactory.getLogger(FooPropertyWebPanelRenderer.class);
    
    @Override
    public String getResourceType()
    {
        return "foo-velocity";
    }

    @Override
    public void render(String templateName, Plugin plugin, Map<String, Object> context, Writer writer) throws RendererException, IOException
    {
        Map<String, Object> defaultContext = MacroUtils.defaultVelocityContext();
        Map<String, Object> map = CompositeMap.of(defaultContext, context);
        VelocityUtils.writeRenderedTemplate(writer, templateName, map);
    }

    @Override
    public String renderFragment(String fragment, Plugin plugin, Map<String, Object> context) throws RendererException
    {
        Map<String, Object> defaultContext = MacroUtils.defaultVelocityContext();
        Map<String, Object> map = CompositeMap.of(defaultContext, context);

        return VelocityUtils.getRenderedContent(fragment, map);
    }

    @Override
    public void renderFragment(Writer writer, String fragment, Plugin plugin, Map<String, Object> context)
        throws RendererException, IOException
    {
        Map<String, Object> defaultContext = MacroUtils.defaultVelocityContext();
        Map<String, Object> map = CompositeMap.of(defaultContext, context);

        VelocityUtils.writeRenderedContent(writer, fragment, map);
    }
    
    public String unescapeHTMLEntities(String str)
    {
        return str;
    }
}

getResourceType()で返す文字列が重要。
これをweb-panelモジュールのresourceタイプにする。foo-velocityをreturnしてるので

  <web-panel key="foo-panel" location="atl.header">
    <resource name="view" type="foo-velocity" location="templates/panel.vm"/>
    <context-provider class="jp.junoe.confluence.plugins.FooContextProvider"/>
  </web-panel>

となる。

これでRenderer自体をとることができた。

で、どうするか?

Velocityを
VelocityEngine v = VelocityUtils.getVelocityEngine();
で取得することができるので、ここにRawで変数を与えていけばいいんじゃないかと思っている。

が、実際についてはまだ。

同じ質問を見つけた

AtlassianのQ&Aサイトで
How to prevent velocity escape html - Atlassian Answers
というのを見つけた。

@HtmlSafeというのを使って宣言するといいらしい。

ただ、まだやっていない。

disableAntiXSS()

その質問に言及のあった
https://developer.atlassian.com/display/CONFDEV/Enabling+XSS+Protection+in+Plugins
を見ると、テンプレートで#disableAntiXSS()を実行するといいらしい。ただ、これをやってみてもダメだった。

変数にHtmlというpostfixを付けるのが正解

Enabling XSS Protection in Plugins - Confluence Development - Atlassian Developer Documentationの「How do I opt out of Anti-XSS protection?」のセクション

あるいは、先ほどの質問の Damian Nowak さんの回答。

単にVelocityテンプレートで

#set($successMessageHtml = $successMessage)

というようにHtmlを最後につけた変数にすればいいだけという。。。

他にもいくつか方法があるのだけど、これが一番簡単だった。