Просмотр исходного кода

Merge pull request #215 from brigadier/master

type interpolation and escaping in translations with parameters

close #213
Andreas Stenius 9 лет назад
Родитель
Сommit
f4cef9ca3e
3 измененных файлов с 105 добавлено и 13 удалено
  1. 7 4
      src/erlydtl_beam_compiler.erl
  2. 17 9
      src/erlydtl_runtime.erl
  3. 81 0
      test/erlydtl_translation_tests.erl

+ 7 - 4
src/erlydtl_beam_compiler.erl

@@ -832,11 +832,11 @@ blocktrans_ast(Args, Contents, PluralContents, TreeWalker) ->
 
     #dtl_context{
       trans_fun = TFun,
-      trans_locales = TLocales } = TreeWalker3#treewalker.context,
+      trans_locales = TLocales, auto_escape = AutoEscape } = TreeWalker3#treewalker.context,
     if TFun =:= none; PluralContents =/= undefined ->
             %% translate in runtime
             {FinalAst, FinalTW} = blocktrans_runtime_ast(
-                                    {DefaultAst, MergedInfo}, SourceText, Contents, Context,
+                                    {DefaultAst, MergedInfo}, SourceText, Contents, Context, AutoEscape,
                                     plural_contents(PluralContents, Count, TreeWalker3)),
             {FinalAst, restore_scope(TreeWalker1, FinalTW)};
        is_function(TFun, 2) ->
@@ -862,7 +862,7 @@ blocktrans_ast(Args, Contents, PluralContents, TreeWalker) ->
             empty_ast(?ERR({translation_fun, TFun}, TreeWalker3))
     end.
 
-blocktrans_runtime_ast({DefaultAst, Info}, SourceText, Contents, Context, {Plural, TreeWalker}) ->
+blocktrans_runtime_ast({DefaultAst, Info}, SourceText, Contents, Context, AutoEscape, {Plural, TreeWalker}) ->
     %% Contents is flat - only strings and '{{var}}' allowed.
     %% build sorted list (orddict) of pre-resolved variables to pass to runtime translation function
     USortedVariables = lists:usort(fun({variable, {identifier, _, A}},
@@ -878,13 +878,14 @@ blocktrans_runtime_ast({DefaultAst, Info}, SourceText, Contents, Context, {Plura
     VarListAst = erl_syntax:list(VarAsts),
     BlockTransAst = ?Q(["begin",
                         "  case erlydtl_runtime:translate_block(",
-                        "         _@phrase, _@locale,",
+                        "         _@phrase, _@locale, _@auto_escape, ",
                         "         _@VarListAst, _TranslationFun) of",
                         "    default -> _@DefaultAst;",
                         "    Text -> Text",
                         "  end",
                         "end"],
                        [{phrase, phrase_ast(SourceText, Plural)},
+                        {auto_escape, autoescape_ast(AutoEscape)},
                         {locale, phrase_locale_ast(Context)}]),
     {{BlockTransAst, merge_count_info(Info, Plural)}, TreeWalker1}.
 
@@ -908,6 +909,8 @@ phrase_ast(Text, {Contents, {CountAst, _CountInfo}}) ->
          [merl:term(erlydtl_unparser:unparse(Contents)),
           CountAst])
       ]).
+autoescape_ast([]) -> autoescape_ast([on]);
+autoescape_ast([V | _]) -> erl_syntax:atom(V == on).
 
 phrase_locale_ast(undefined) -> merl:var('_CurrentLocale');
 phrase_locale_ast(Context) -> erl_syntax:tuple([merl:var('_CurrentLocale'), merl:term(Context)]).

+ 17 - 9
src/erlydtl_runtime.erl

@@ -220,12 +220,12 @@ do_translate(Phrase, Locale, TranslationFun)
 %%  * Each interpolation variable should exist
 %%    (String="{{a}}", Variables=[{"b", "b-val"}] will fall)
 %%  * Orddict keys should be string(), not binary()
--spec translate_block(phrase(), locale(), orddict:orddict(), none | translate_fun()) -> iodata().
-translate_block(Phrase, Locale, Variables, TranslationFun) ->
+-spec translate_block(phrase(), locale(), atom(), orddict:orddict(), none | translate_fun()) -> iodata().
+translate_block(Phrase, Locale, AutoEscape, Variables, TranslationFun) ->
     case translate(Phrase, Locale, TranslationFun, default) of
         default -> default;
         Translated ->
-            try interpolate_variables(Translated, Variables)
+            try interpolate_variables(Translated, Variables, AutoEscape)
             catch
                 {no_close_var, T} ->
                     io:format(standard_error, "Warning: template translation: variable not closed: \"~s\"~n", [T]),
@@ -234,13 +234,13 @@ translate_block(Phrase, Locale, Variables, TranslationFun) ->
             end
     end.
 
-interpolate_variables(Tpl, []) ->
+interpolate_variables(Tpl, [], _) ->
     Tpl;
-interpolate_variables(Tpl, Variables) ->
+interpolate_variables(Tpl, Variables, AutoEscape) ->
     BTpl = iolist_to_binary(Tpl),
-    interpolate_variables1(BTpl, Variables).
+    interpolate_variables1(BTpl, Variables, AutoEscape).
 
-interpolate_variables1(Tpl, Vars) ->
+interpolate_variables1(Tpl, Vars, AutoEscape) ->
     %% pre-compile binary patterns?
     case binary:split(Tpl, <<"{{">>) of
         [Tpl]=NoVars -> NoVars; %% need to enclose in list due to list tail call below..
@@ -249,11 +249,19 @@ interpolate_variables1(Tpl, Vars) ->
                 [_] -> throw({no_close_var, Tpl});
                 [Var, Post1] ->
                     Var1 = string:strip(binary_to_list(Var)),
-                    Value = orddict:fetch(Var1, Vars),
-                    [Pre, Value | interpolate_variables1(Post1, Vars)]
+                    Value = cast(orddict:fetch(Var1, Vars), AutoEscape),
+                    [Pre, Value | interpolate_variables1(Post1, Vars, AutoEscape)]
             end
     end.
 
+cast(V, _) when is_integer(V); is_float(V) ->
+    erlydtl_filters:format_number(V);
+cast(V, true) when is_binary(V); is_list(V) ->
+    erlydtl_filters:force_escape(V);
+cast(V, false) when is_binary(V); is_list(V) ->
+    V;
+cast(V, AutoEscape) ->
+    cast(io_lib:format("~p", [V]), AutoEscape).
 
 are_equal(Arg1, Arg2) when Arg1 =:= Arg2 ->
     true;

+ 81 - 0
test/erlydtl_translation_tests.erl

@@ -0,0 +1,81 @@
+-module(erlydtl_translation_tests).
+
+-include_lib("eunit/include/eunit.hrl").
+
+all_sources_parser_test_() ->
+    [{Title, [test_fun(Test) || Test <- Tests]}
+     || {Title, Tests} <- test_defs()].
+
+test_fun({Name, Template, Variables, Options, Output}) ->
+    {Name, fun () ->
+                   Tokens = (catch compile_and_render(Template, Variables, Options)),
+                   ?assertMatch(Output, Tokens)
+           end}.
+
+
+
+compile_and_render(Template, Variables, Options) ->
+    {ok, test} = erlydtl:compile_template(Template, test),
+    {ok, R}  = test:render(Variables, Options),
+    iolist_to_binary(R).
+
+
+test_defs() ->
+    [
+        {"trans", [
+            {"simple", "{% trans \"hello\" %}", [], [], <<"hello">>},
+            {"with_fun", "{% trans \"text\" %}", [], [{translation_fun, fun(_ID, _L) -> "hola" end}], <<"hola">>},
+            {"with_fun_utf8", "{% trans \"text\" %}", [],
+                [{translation_fun, fun(_ID, _L) -> <<"привет"/utf8>> end}], <<"привет"/utf8>>}
+        ]},
+        {"blocktrans", [
+            {"simple", "{% blocktrans %} hello {% endblocktrans %}", [], [], <<" hello ">>},
+            {"with_fun", "{% blocktrans %} hello {% endblocktrans %}", [],
+                [{translation_fun, fun(_ID, _L) -> "hola" end}], <<"hola">>},
+    
+            {"s_param_no_fun", "{% blocktrans %} hello {{ p }} {% endblocktrans %}", [{p, "mundo"}],
+                [], <<" hello mundo ">>},
+    
+            {"s_param", "{% blocktrans %} hello {{ p }} {% endblocktrans %}", [{p, "mundo"}],
+                [{translation_fun, fun(_ID, _L) -> "hola {{ p }}" end}], <<"hola mundo">>},
+            {"b_param", "{% blocktrans %} hello {{ p }} {% endblocktrans %}", [{p, <<"mundo">>}],
+                [{translation_fun, fun(_ID, _L) -> "hola {{ p }}" end}], <<"hola mundo">>},
+            {"i_param", "{% blocktrans %} hello {{ p }} {% endblocktrans %}", [{p, 1}],
+                [{translation_fun, fun(_ID, _L) -> "hola {{ p }}" end}], <<"hola 1">>},
+            {"f_param", "{% blocktrans %} hello {{ p }} {% endblocktrans %}", [{p, 3.1415}],
+                [{translation_fun, fun(_ID, _L) -> "hola {{ p }}" end}], <<"hola 3.1415">>},
+    
+            {"b_xss", "{% blocktrans %} hello {{ p }} {% endblocktrans %}",
+                [{p, <<"<script>alert('pwnd');</script>">>}],
+                [{translation_fun, fun(_ID, _L) -> "hola {{ p }}" end}],
+                <<"hola &lt;script&gt;alert(&#039;pwnd&#039;);&lt;/script&gt;">>},
+            {"s_xss", "{% blocktrans %} hello {{ p }} {% endblocktrans %}",
+                [{p, "<script>alert('pwnd');</script>"}],
+                [{translation_fun, fun(_ID, _L) -> "hola {{ p }}" end}],
+                <<"hola &lt;script&gt;alert(&#039;pwnd&#039;);&lt;/script&gt;">>},
+    
+            {"b_autoecape_off",
+                "{% autoescape off %}{% blocktrans %} hello {{ p }} {% endblocktrans %}{% endautoescape %}",
+                [{p, <<"<script>alert('pwnd');</script>">>}],
+                [{translation_fun, fun(_ID, _L) -> "hola {{ p }}" end}],
+                <<"hola <script>alert('pwnd');</script>">>},
+            {"b_autoecape_nested",
+                "{% autoescape off %}{% autoescape on %}{% blocktrans %} hello {{ p }} {% endblocktrans %}{% endautoescape %}{% endautoescape %}",
+                [{p, <<"<script>alert('pwnd');</script>">>}],
+                [{translation_fun, fun(_ID, _L) -> "hola {{ p }}" end}],
+                <<"hola &lt;script&gt;alert(&#039;pwnd&#039;);&lt;/script&gt;">>},
+             {"term_hack_", "{% blocktrans %} hello {{ p }} {% endblocktrans %}",
+                [{p, {"<script>alert('pwnd');</script>"}}],
+                [{translation_fun, fun(_ID, _L) -> "hola {{ p }}" end}],
+                <<"hola {&quot;&lt;script&gt;alert(&#039;pwnd&#039;);&lt;/script&gt;&quot;}">>},
+            {"plural_2",
+                "{% blocktrans count counter=p %} hello world {% plural %} hello {{ p }} worlds {% endblocktrans %}",
+                [{p, 2}],
+                [{translation_fun, fun({" hello world ", {" hello {{ p }} worlds ", 2}}, _L)  ->
+                                        "hola {{ p }} mundos"
+                                   end}],
+                <<"hola 2 mundos">>}
+        ]}
+    ].
+
+