diff --git a/lib/phoenix_html.ex b/lib/phoenix_html.ex index e66a58d..a3dfcc6 100644 --- a/lib/phoenix_html.ex +++ b/lib/phoenix_html.ex @@ -371,6 +371,64 @@ defmodule Phoenix.HTML do defp javascript_escape(<<>>, acc), do: acc + @doc ~S""" + Escapes encoded JSON for safe insertion into an HTML script tag. + + The given JSON must already be encoded. The characters `<`, `>`, and `&` + are encoded as JSON Unicode escape sequences and the result is marked as + HTML safe. + + iex> json = ~s({"name":""}) + iex> json |> json_escape() |> safe_to_string() + "{\"name\":\"\\u003C/script\\u003E\"}" + + """ + @spec json_escape(binary) :: safe + def json_escape(data) when is_binary(data) do + {:safe, json_escape(data, 0, data, [])} + end + + escapes = [ + {?<, "\\u003C"}, + {?>, "\\u003E"}, + {?&, "\\u0026"} + ] + + for {match, insert} <- escapes do + defp json_escape(<>, skip, original, acc) do + json_escape(rest, skip + 1, original, [acc | unquote(insert)]) + end + end + + defp json_escape(<<_char, rest::bits>>, skip, original, acc) do + json_escape(rest, skip, original, acc, 1) + end + + defp json_escape(<<>>, 0, original, _acc) do + original + end + + defp json_escape(<<>>, _skip, _original, acc) do + acc + end + + for {match, insert} <- escapes do + defp json_escape(<>, skip, original, acc, len) do + part = binary_part(original, skip, len) + json_escape(rest, skip + len + 1, original, [acc, part | unquote(insert)], 0) + end + end + + defp json_escape(<<_char, rest::bits>>, skip, original, acc, len) do + json_escape(rest, skip, original, acc, len + 1) + end + + defp json_escape(<<>>, 0, original, _acc, _len), do: original + + defp json_escape(<<>>, skip, original, acc, len) do + [acc | binary_part(original, skip, len)] + end + @doc """ Escapes a string for use as a CSS identifier. diff --git a/test/phoenix_html_test.exs b/test/phoenix_html_test.exs index 285dbd7..7011b1d 100644 --- a/test/phoenix_html_test.exs +++ b/test/phoenix_html_test.exs @@ -21,6 +21,18 @@ defmodule Phoenix.HTMLTest do assert javascript_escape({:safe, ["'Single quote'"]}) == {:safe, "\\'Single quote\\'"} end + test "json_escape/1" do + assert json_escape("") == {:safe, ""} + + assert ~s({"data":"