%% esaml - SAML for erlang
%%
%% Copyright (c) 2013, Alex Wilson and the University of Queensland
%% All rights reserved.
%%
%% Distributed subject to the terms of the 2-clause BSD license, see
%% the LICENSE file in the root of the distribution.
%% @doc XML canonocialisation for xmerl
%%
%% Functions for performing XML canonicalisation (C14n), as specified
%% at http://www.w3.org/TR/xml-c14n .
%%
%% These routines work on xmerl data structures (see the xmerl user guide
%% for details).
-module(xmerl_c14n).
-export([c14n/3, c14n/2, c14n/1, xml_safe_string/2, xml_safe_string/1, canon_name/1]).
-include_lib("xmerl/include/xmerl.hrl").
-include_lib("public_key/include/public_key.hrl").
%% @doc Returns the canonical namespace-URI-prefix-resolved version of an XML name.
%% @private
-spec canon_name(Prefix :: string(), Name :: string() | atom(), Nsp :: #xmlNamespace{}) -> string().
canon_name(Ns, Name, Nsp) ->
NsPartRaw = case Ns of
empty -> Nsp#xmlNamespace.default;
[] ->
if Nsp == [] -> 'urn:oasis:names:tc:SAML:2.0:assertion';
true -> Nsp#xmlNamespace.default
end;
_ ->
case proplists:get_value(Ns, Nsp#xmlNamespace.nodes) of
undefined ->
error({ns_not_found, Ns, Nsp});
Uri -> atom_to_list(Uri)
end
end,
NsPart = if is_atom(NsPartRaw) -> atom_to_list(NsPartRaw); true -> NsPartRaw end,
NamePart = if is_atom(Name) -> atom_to_list(Name); true -> Name end,
lists:flatten([NsPart | NamePart]).
%% @doc Returns the canonical URI name of an XML element or attribute.
%% @private
-spec canon_name(#xmlElement{} | #xmlAttribute{}) -> string().
canon_name(#xmlAttribute{name = Name, nsinfo = Exp, namespace = Nsp}) ->
case Exp of
{Ns, Nme} -> canon_name(Ns, Nme, Nsp);
_ -> canon_name([], Name, Nsp)
end;
canon_name(#xmlElement{name = Name, nsinfo = Exp, namespace = Nsp}) ->
case Exp of
{Ns, Nme} -> canon_name(Ns, Nme, Nsp);
_ -> canon_name([], Name, Nsp)
end.
%% @doc Compares two XML attributes for c14n purposes
-spec attr_lte(A :: #xmlAttribute{}, B :: #xmlAttribute{}) -> true | false.
attr_lte(AttrA, AttrB) ->
A = canon_name(AttrA), B = canon_name(AttrB),
PrefixedA = case AttrA#xmlAttribute.nsinfo of {_, _} -> true; _ -> false end,
PrefixedB = case AttrB#xmlAttribute.nsinfo of {_, _} -> true; _ -> false end,
if (PrefixedA) andalso (not PrefixedB) ->
false;
(not PrefixedA) andalso (PrefixedB) ->
true;
true ->
A =< B
end.
%% @doc Cleans out all namespace definitions from an attribute list and returns it sorted.
%% @private
-spec clean_sort_attrs(Attrs :: [#xmlAttribute{}]) -> [#xmlAttribute{}].
clean_sort_attrs(Attrs) ->
lists:sort(fun(A,B) ->
attr_lte(A, B)
end, lists:filter(fun(Attr) ->
case Attr#xmlAttribute.nsinfo of
{"xmlns", _} -> false;
_ -> case Attr#xmlAttribute.name of
'xmlns' -> false;
_ -> true
end
end
end, Attrs)).
%% @doc Returns the list of namespace prefixes "needed" by an element in canonical form
%% @private
-spec needed_ns(Elem :: #xmlElement{}, InclNs :: [string()]) -> [string()].
needed_ns(#xmlElement{nsinfo = NsInfo, attributes = Attrs}, InclNs) ->
NeededNs1 = case NsInfo of
{Nas, _} -> [Nas];
_ -> []
end,
% show through namespaces that apply at the bottom level? this part of the spec is retarded
%KidElems = [K || K <- Kids, element(1, K) =:= xmlElement],
NeededNs2 = NeededNs1, %case KidElems of
%[] -> [K || {K,V} <- E#xmlElement.namespace#xmlNamespace.nodes];
%_ -> NeededNs1
%end,
lists:foldl(fun(Attr, Needed) ->
case Attr#xmlAttribute.nsinfo of
{"xmlns", Prefix} ->
case lists:member(Prefix, InclNs) of
true -> [Prefix | Needed];
_ -> Needed
end;
{Ns, _Name} ->
case lists:member(Ns, Needed) of
true -> Needed;
_ -> [Ns | Needed]
end;
_ -> Needed
end
end, NeededNs2, Attrs).
%% @doc Make xml ok to eat, in a non-quoted situation.
%% @private
-spec xml_safe_string(term()) -> string().
xml_safe_string(Term) -> xml_safe_string(Term, false).
%% @doc Make xml ok to eat
%% @private
-spec xml_safe_string(String :: term(), Quotes :: boolean()) -> string().
xml_safe_string(Atom, Quotes) when is_atom(Atom) -> xml_safe_string(atom_to_list(Atom), Quotes);
xml_safe_string(Bin, Quotes) when is_binary(Bin) -> xml_safe_string(binary_to_list(Bin), Quotes);
xml_safe_string([], _) -> [];
xml_safe_string(Str, Quotes) when is_list(Str) ->
[Next | Rest] = Str,
if
(not Quotes andalso ([Next] =:= "\n")) -> [Next | xml_safe_string(Rest, Quotes)];
(Next < 32) ->
lists:flatten(["" ++ integer_to_list(Next, 16) ++ ";" | xml_safe_string(Rest, Quotes)]);
(Quotes andalso ([Next] =:= "\"")) -> lists:flatten([""" | xml_safe_string(Rest, Quotes)]);
([Next] =:= "&") -> lists:flatten(["&" | xml_safe_string(Rest, Quotes)]);
([Next] =:= "<") -> lists:flatten(["<" | xml_safe_string(Rest, Quotes)]);
(not Quotes andalso ([Next] =:= ">")) -> lists:flatten([">" | xml_safe_string(Rest, Quotes)]);
true -> [Next | xml_safe_string(Rest, Quotes)]
end;
xml_safe_string(Term, Quotes) ->
xml_safe_string(io_lib:format("~p", [Term]), Quotes).
%% @doc Worker function for canonicalisation (c14n). It accumulates the canonical string data
%% for a given XML "thing" (element/attribute/whatever)
%% @private
-type xml_thing() :: #xmlDocument{} | #xmlElement{} | #xmlAttribute{} | #xmlPI{} | #xmlText{} | #xmlComment{}.
-spec c14n(XmlThing :: xml_thing(), KnownNs :: [{string(), string()}], ActiveNS :: [string()], Comments :: boolean(), InclNs :: [string()], Acc :: [string() | number()]) -> [string() | number()].
c14n(#xmlText{value = Text}, _KnownNS, _ActiveNS, _Comments, _InclNs, Acc) ->
[xml_safe_string(Text) | Acc];
c14n(#xmlComment{value = Text}, _KnownNS, _ActiveNS, true, _InclNs, Acc) ->
["-->", xml_safe_string(Text), "\n\n\n\n\n\n", [{namespace_conformant, true}, {document, true}]),
WithoutComments = "\nHello, world!\n",
WithoutComments = c14n(Doc, false),
WithComments = "\nHello, world!\n\n\n",
WithComments = c14n(Doc, true).
c14n_3_2_test() ->
{Doc, _} = xmerl_scan:string("\n \n A B \n \n A\n \n B\n A B \n C\n \n", [{namespace_conformant, true}, {document, true}]),
Target = "\n \n A B \n \n A\n \n B\n A B \n C\n \n",
Target = c14n(Doc, true).
c14n_3_3_test() ->
{Doc, _} = xmerl_scan:string("]>\n\n \n \n \n \n \n \n \n \n \n \n \n \n", [{namespace_conformant, true}, {document, true}]),
Target = "\n \n \n \n \n \n \n \n \n \n \n \n \n",
Target = c14n(Doc, true).
c14n_3_4_test() ->
{Doc, _} = xmerl_scan:string("\n\n]>\n\n First line
Second line\n 2\n \"0\" && value<\"10\" ?\"valid\":\"error\"]]>\n valid\n \n \n \n", [{namespace_conformant, true}, {document, true}]),
Target = "\n First line\n\nSecond line\n 2\n value>\"0\" && value<\"10\" ?\"valid\":\"error\"\n "0" && value<"10" ?"valid":"error"\">valid\n \n \n \n",
Target = c14n(Doc, true).
default_ns_test() ->
{Doc, _} = xmerl_scan:string("blah", [{namespace_conformant, true}]),
Target = "blah",
Target = c14n(Doc, true),
{Doc2, _} = xmerl_scan:string("foo", [{namespace_conformant, true}]),
Target2 = "foo",
Target2 = c14n(Doc2, true).
omit_default_ns_test() ->
{Doc, _} = xmerl_scan:string("", [{namespace_conformant, true}]),
Target = "",
Target = c14n(Doc, true).
c14n_inclns_test() ->
{Doc, []} = xmerl_scan:string("foo", [{namespace_conformant, true}]),
Target1 = "foo",
Target1 = c14n(Doc, false),
Target2 = "foo",
Target2 = c14n(Doc, false, ["bar"]).
-endif.