123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445 |
- %% Copyright (c) 2019-2023, Loïc Hoguin <essen@ninenines.eu>
- %%
- %% Permission to use, copy, modify, and/or distribute this software for any
- %% purpose with or without fee is hereby granted, provided that the above
- %% copyright notice and this permission notice appear in all copies.
- %%
- %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- -module(cow_link).
- -compile({no_auto_import, [link/1]}).
- -export([parse_link/1]).
- -export([resolve_link/2]).
- -export([resolve_link/3]).
- -export([link/1]).
- -include("cow_inline.hrl").
- -include("cow_parse.hrl").
- -type link() :: #{
- target := binary(),
- rel := binary(),
- attributes := [{binary(), binary()}]
- }.
- -export_type([link/0]).
- -type resolve_opts() :: #{
- allow_anchor => boolean()
- }.
- -type uri() :: uri_string:uri_map() | uri_string:uri_string() | undefined.
- %% Parse a link header.
- %% This function returns the URI target from the header directly.
- %% Relative URIs must then be resolved as per RFC3986 5. In some
- %% cases it might not be possible to resolve URIs, for example when
- %% the link header is returned with a 404 status code.
- -spec parse_link(binary()) -> [link()].
- parse_link(Link) ->
- before_target(Link, []).
- before_target(<<>>, Acc) -> lists:reverse(Acc);
- before_target(<<$<,R/bits>>, Acc) -> target(R, Acc, <<>>);
- before_target(<<C,R/bits>>, Acc) when ?IS_WS(C) -> before_target(R, Acc).
- target(<<$>,R/bits>>, Acc, T) -> param_sep(R, Acc, T, []);
- target(<<C,R/bits>>, Acc, T) -> target(R, Acc, <<T/binary, C>>).
- param_sep(<<>>, Acc, T, P) -> lists:reverse(acc_link(Acc, T, P));
- param_sep(<<$,,R/bits>>, Acc, T, P) -> before_target(R, acc_link(Acc, T, P));
- param_sep(<<$;,R/bits>>, Acc, T, P) -> before_param(R, Acc, T, P);
- param_sep(<<C,R/bits>>, Acc, T, P) when ?IS_WS(C) -> param_sep(R, Acc, T, P).
- before_param(<<C,R/bits>>, Acc, T, P) when ?IS_WS(C) -> before_param(R, Acc, T, P);
- before_param(<<C,R/bits>>, Acc, T, P) when ?IS_TOKEN(C) -> ?LOWER(param, R, Acc, T, P, <<>>).
- param(<<$=,$",R/bits>>, Acc, T, P, K) -> quoted(R, Acc, T, P, K, <<>>);
- param(<<$=,C,R/bits>>, Acc, T, P, K) when ?IS_TOKEN(C) -> value(R, Acc, T, P, K, <<C>>);
- param(<<C,R/bits>>, Acc, T, P, K) when ?IS_TOKEN(C) -> ?LOWER(param, R, Acc, T, P, K).
- quoted(<<$",R/bits>>, Acc, T, P, K, V) -> param_sep(R, Acc, T, [{K, V}|P]);
- quoted(<<$\\,C,R/bits>>, Acc, T, P, K, V) when ?IS_VCHAR_OBS(C) -> quoted(R, Acc, T, P, K, <<V/binary,C>>);
- quoted(<<C,R/bits>>, Acc, T, P, K, V) when ?IS_VCHAR_OBS(C) -> quoted(R, Acc, T, P, K, <<V/binary,C>>).
- value(<<C,R/bits>>, Acc, T, P, K, V) when ?IS_TOKEN(C) -> value(R, Acc, T, P, K, <<V/binary,C>>);
- value(R, Acc, T, P, K, V) -> param_sep(R, Acc, T, [{K, V}|P]).
- acc_link(Acc, Target, Params0) ->
- Params1 = lists:reverse(Params0),
- %% The rel parameter MUST be present. (RFC8288 3.3)
- {value, {_, Rel}, Params2} = lists:keytake(<<"rel">>, 1, Params1),
- %% Occurrences after the first MUST be ignored by parsers.
- Params = filter_out_duplicates(Params2, #{}),
- [#{
- target => Target,
- rel => ?LOWER(Rel),
- attributes => Params
- }|Acc].
- %% This function removes duplicates for attributes that don't allow them.
- filter_out_duplicates([], _) ->
- [];
- %% The "rel" is mandatory and was already removed from params.
- filter_out_duplicates([{<<"rel">>, _}|Tail], State) ->
- filter_out_duplicates(Tail, State);
- filter_out_duplicates([{<<"anchor">>, _}|Tail], State=#{anchor := true}) ->
- filter_out_duplicates(Tail, State);
- filter_out_duplicates([{<<"media">>, _}|Tail], State=#{media := true}) ->
- filter_out_duplicates(Tail, State);
- filter_out_duplicates([{<<"title">>, _}|Tail], State=#{title := true}) ->
- filter_out_duplicates(Tail, State);
- filter_out_duplicates([{<<"title*">>, _}|Tail], State=#{title_star := true}) ->
- filter_out_duplicates(Tail, State);
- filter_out_duplicates([{<<"type">>, _}|Tail], State=#{type := true}) ->
- filter_out_duplicates(Tail, State);
- filter_out_duplicates([Tuple={<<"anchor">>, _}|Tail], State) ->
- [Tuple|filter_out_duplicates(Tail, State#{anchor => true})];
- filter_out_duplicates([Tuple={<<"media">>, _}|Tail], State) ->
- [Tuple|filter_out_duplicates(Tail, State#{media => true})];
- filter_out_duplicates([Tuple={<<"title">>, _}|Tail], State) ->
- [Tuple|filter_out_duplicates(Tail, State#{title => true})];
- filter_out_duplicates([Tuple={<<"title*">>, _}|Tail], State) ->
- [Tuple|filter_out_duplicates(Tail, State#{title_star => true})];
- filter_out_duplicates([Tuple={<<"type">>, _}|Tail], State) ->
- [Tuple|filter_out_duplicates(Tail, State#{type => true})];
- filter_out_duplicates([Tuple|Tail], State) ->
- [Tuple|filter_out_duplicates(Tail, State)].
- -ifdef(TEST).
- parse_link_test_() ->
- Tests = [
- {<<>>, []},
- {<<" ">>, []},
- %% Examples from the RFC.
- {<<"<http://example.com/TheBook/chapter2>; rel=\"previous\"; title=\"previous chapter\"">>, [
- #{
- target => <<"http://example.com/TheBook/chapter2">>,
- rel => <<"previous">>,
- attributes => [
- {<<"title">>, <<"previous chapter">>}
- ]
- }
- ]},
- {<<"</>; rel=\"http://example.net/foo\"">>, [
- #{
- target => <<"/">>,
- rel => <<"http://example.net/foo">>,
- attributes => []
- }
- ]},
- {<<"</terms>; rel=\"copyright\"; anchor=\"#foo\"">>, [
- #{
- target => <<"/terms">>,
- rel => <<"copyright">>,
- attributes => [
- {<<"anchor">>, <<"#foo">>}
- ]
- }
- ]},
- % {<<"</TheBook/chapter2>; rel=\"previous\"; title*=UTF-8'de'letztes%20Kapitel, "
- % "</TheBook/chapter4>; rel=\"next\"; title*=UTF-8'de'n%c3%a4chstes%20Kapitel">>, [
- % %% @todo
- % ]}
- {<<"<http://example.org/>; rel=\"start http://example.net/relation/other\"">>, [
- #{
- target => <<"http://example.org/">>,
- rel => <<"start http://example.net/relation/other">>,
- attributes => []
- }
- ]},
- {<<"<https://example.org/>; rel=\"start\", "
- "<https://example.org/index>; rel=\"index\"">>, [
- #{
- target => <<"https://example.org/">>,
- rel => <<"start">>,
- attributes => []
- },
- #{
- target => <<"https://example.org/index">>,
- rel => <<"index">>,
- attributes => []
- }
- ]},
- %% Relation types are case insensitive.
- {<<"</>; rel=\"SELF\"">>, [
- #{
- target => <<"/">>,
- rel => <<"self">>,
- attributes => []
- }
- ]},
- {<<"</>; rel=\"HTTP://EXAMPLE.NET/FOO\"">>, [
- #{
- target => <<"/">>,
- rel => <<"http://example.net/foo">>,
- attributes => []
- }
- ]},
- %% Attribute names are case insensitive.
- {<<"</terms>; REL=\"copyright\"; ANCHOR=\"#foo\"">>, [
- #{
- target => <<"/terms">>,
- rel => <<"copyright">>,
- attributes => [
- {<<"anchor">>, <<"#foo">>}
- ]
- }
- ]}
- ],
- [{V, fun() -> R = parse_link(V) end} || {V, R} <- Tests].
- -endif.
- %% Resolve a link based on the context URI and options.
- -spec resolve_link(Link, uri()) -> Link | false when Link::link().
- resolve_link(Link, ContextURI) ->
- resolve_link(Link, ContextURI, #{}).
- -spec resolve_link(Link, uri(), resolve_opts()) -> Link | false when Link::link().
- %% When we do not have a context URI we only succeed when the target URI is absolute.
- %% The target URI will only be normalized in that case.
- resolve_link(Link=#{target := TargetURI}, undefined, _) ->
- case uri_string:parse(TargetURI) of
- URIMap = #{scheme := _} ->
- Link#{target => uri_string:normalize(URIMap)};
- _ ->
- false
- end;
- resolve_link(Link=#{attributes := Params}, ContextURI, Opts) ->
- AllowAnchor = maps:get(allow_anchor, Opts, true),
- case lists:keyfind(<<"anchor">>, 1, Params) of
- false ->
- do_resolve_link(Link, ContextURI);
- {_, Anchor} when AllowAnchor ->
- do_resolve_link(Link, resolve(Anchor, ContextURI));
- _ ->
- false
- end.
- do_resolve_link(Link=#{target := TargetURI}, ContextURI) ->
- Link#{target => uri_string:recompose(resolve(TargetURI, ContextURI))}.
- -ifdef(TEST).
- resolve_link_test_() ->
- Tests = [
- %% No context URI available.
- {#{target => <<"http://a/b/./c">>}, undefined, #{},
- #{target => <<"http://a/b/c">>}},
- {#{target => <<"a/b/./c">>}, undefined, #{},
- false},
- %% Context URI available, allow_anchor => true.
- {#{target => <<"http://a/b">>, attributes => []}, <<"http://a/c">>, #{},
- #{target => <<"http://a/b">>, attributes => []}},
- {#{target => <<"b">>, attributes => []}, <<"http://a/c">>, #{},
- #{target => <<"http://a/b">>, attributes => []}},
- {#{target => <<"b">>, attributes => [{<<"anchor">>, <<"#frag">>}]}, <<"http://a/c">>, #{},
- #{target => <<"http://a/b">>, attributes => [{<<"anchor">>, <<"#frag">>}]}},
- {#{target => <<"b">>, attributes => [{<<"anchor">>, <<"d/e">>}]}, <<"http://a/c">>, #{},
- #{target => <<"http://a/d/b">>, attributes => [{<<"anchor">>, <<"d/e">>}]}},
- %% Context URI available, allow_anchor => false.
- {#{target => <<"http://a/b">>, attributes => []}, <<"http://a/c">>, #{allow_anchor => false},
- #{target => <<"http://a/b">>, attributes => []}},
- {#{target => <<"b">>, attributes => []}, <<"http://a/c">>, #{allow_anchor => false},
- #{target => <<"http://a/b">>, attributes => []}},
- {#{target => <<"b">>, attributes => [{<<"anchor">>, <<"#frag">>}]},
- <<"http://a/c">>, #{allow_anchor => false}, false},
- {#{target => <<"b">>, attributes => [{<<"anchor">>, <<"d/e">>}]},
- <<"http://a/c">>, #{allow_anchor => false}, false}
- ],
- [{iolist_to_binary(io_lib:format("~0p", [L])),
- fun() -> R = resolve_link(L, C, O) end} || {L, C, O, R} <- Tests].
- -endif.
- %% @todo This function has been added to Erlang/OTP 22.3 as uri_string:resolve/2,3.
- resolve(URI, BaseURI) ->
- case resolve1(ensure_map_uri(URI), BaseURI) of
- TargetURI = #{path := Path0} ->
- %% We remove dot segments. Normalizing the entire URI
- %% will sometimes add an extra slash we don't want.
- #{path := Path} = uri_string:normalize(#{path => Path0}, [return_map]),
- TargetURI#{path => Path};
- TargetURI ->
- TargetURI
- end.
- resolve1(URI=#{scheme := _}, _) ->
- URI;
- resolve1(URI=#{host := _}, BaseURI) ->
- #{scheme := Scheme} = ensure_map_uri(BaseURI),
- URI#{scheme => Scheme};
- resolve1(URI=#{path := <<>>}, BaseURI0) ->
- BaseURI = ensure_map_uri(BaseURI0),
- Keys = case maps:is_key(query, URI) of
- true -> [scheme, host, port, path];
- false -> [scheme, host, port, path, query]
- end,
- maps:merge(URI, maps:with(Keys, BaseURI));
- resolve1(URI=#{path := <<"/",_/bits>>}, BaseURI0) ->
- BaseURI = ensure_map_uri(BaseURI0),
- maps:merge(URI, maps:with([scheme, host, port], BaseURI));
- resolve1(URI=#{path := Path}, BaseURI0) ->
- BaseURI = ensure_map_uri(BaseURI0),
- maps:merge(
- URI#{path := merge_paths(Path, BaseURI)},
- maps:with([scheme, host, port], BaseURI)).
- merge_paths(Path, #{host := _, path := <<>>}) ->
- <<$/, Path/binary>>;
- merge_paths(Path, #{path := BasePath0}) ->
- case string:split(BasePath0, <<$/>>, trailing) of
- [BasePath, _] -> <<BasePath/binary, $/, Path/binary>>;
- [_] -> <<$/, Path/binary>>
- end.
- ensure_map_uri(URI) when is_map(URI) -> URI;
- ensure_map_uri(URI) -> uri_string:parse(iolist_to_binary(URI)).
- -ifdef(TEST).
- resolve_test_() ->
- Tests = [
- %% 5.4.1. Normal Examples
- {<<"g:h">>, <<"g:h">>},
- {<<"g">>, <<"http://a/b/c/g">>},
- {<<"./g">>, <<"http://a/b/c/g">>},
- {<<"g/">>, <<"http://a/b/c/g/">>},
- {<<"/g">>, <<"http://a/g">>},
- {<<"//g">>, <<"http://g">>},
- {<<"?y">>, <<"http://a/b/c/d;p?y">>},
- {<<"g?y">>, <<"http://a/b/c/g?y">>},
- {<<"#s">>, <<"http://a/b/c/d;p?q#s">>},
- {<<"g#s">>, <<"http://a/b/c/g#s">>},
- {<<"g?y#s">>, <<"http://a/b/c/g?y#s">>},
- {<<";x">>, <<"http://a/b/c/;x">>},
- {<<"g;x">>, <<"http://a/b/c/g;x">>},
- {<<"g;x?y#s">>, <<"http://a/b/c/g;x?y#s">>},
- {<<"">>, <<"http://a/b/c/d;p?q">>},
- {<<".">>, <<"http://a/b/c/">>},
- {<<"./">>, <<"http://a/b/c/">>},
- {<<"..">>, <<"http://a/b/">>},
- {<<"../">>, <<"http://a/b/">>},
- {<<"../g">>, <<"http://a/b/g">>},
- {<<"../..">>, <<"http://a/">>},
- {<<"../../">>, <<"http://a/">>},
- {<<"../../g">>, <<"http://a/g">>},
- %% 5.4.2. Abnormal Examples
- {<<"../../../g">>, <<"http://a/g">>},
- {<<"../../../../g">>, <<"http://a/g">>},
- {<<"/./g">>, <<"http://a/g">>},
- {<<"/../g">>, <<"http://a/g">>},
- {<<"g.">>, <<"http://a/b/c/g.">>},
- {<<".g">>, <<"http://a/b/c/.g">>},
- {<<"g..">>, <<"http://a/b/c/g..">>},
- {<<"..g">>, <<"http://a/b/c/..g">>},
- {<<"./../g">>, <<"http://a/b/g">>},
- {<<"./g/.">>, <<"http://a/b/c/g/">>},
- {<<"g/./h">>, <<"http://a/b/c/g/h">>},
- {<<"g/../h">>, <<"http://a/b/c/h">>},
- {<<"g;x=1/./y">>, <<"http://a/b/c/g;x=1/y">>},
- {<<"g;x=1/../y">>, <<"http://a/b/c/y">>},
- {<<"g?y/./x">>, <<"http://a/b/c/g?y/./x">>},
- {<<"g?y/../x">>, <<"http://a/b/c/g?y/../x">>},
- {<<"g#s/./x">>, <<"http://a/b/c/g#s/./x">>},
- {<<"g#s/../x">>, <<"http://a/b/c/g#s/../x">>},
- {<<"http:g">>, <<"http:g">>} %% for strict parsers
- ],
- [{V, fun() -> R = uri_string:recompose(resolve(V, <<"http://a/b/c/d;p?q">>)) end} || {V, R} <- Tests].
- -endif.
- %% Build a link header.
- -spec link([#{
- target := binary(),
- rel := binary(),
- attributes := [{binary(), binary()}]
- }]) -> iodata().
- link(Links) ->
- lists:join(<<", ">>, [do_link(Link) || Link <- Links]).
- do_link(#{target := TargetURI, rel := Rel, attributes := Params}) ->
- [
- $<, TargetURI, <<">"
- "; rel=\"">>, Rel, $",
- [[<<"; ">>, Key, <<"=\"">>, escape(iolist_to_binary(Value), <<>>), $"]
- || {Key, Value} <- Params]
- ].
- escape(<<>>, Acc) -> Acc;
- escape(<<$\\,R/bits>>, Acc) -> escape(R, <<Acc/binary,$\\,$\\>>);
- escape(<<$\",R/bits>>, Acc) -> escape(R, <<Acc/binary,$\\,$\">>);
- escape(<<C,R/bits>>, Acc) -> escape(R, <<Acc/binary,C>>).
- -ifdef(TEST).
- link_test_() ->
- Tests = [
- {<<>>, []},
- %% Examples from the RFC.
- {<<"<http://example.com/TheBook/chapter2>; rel=\"previous\"; title=\"previous chapter\"">>, [
- #{
- target => <<"http://example.com/TheBook/chapter2">>,
- rel => <<"previous">>,
- attributes => [
- {<<"title">>, <<"previous chapter">>}
- ]
- }
- ]},
- {<<"</>; rel=\"http://example.net/foo\"">>, [
- #{
- target => <<"/">>,
- rel => <<"http://example.net/foo">>,
- attributes => []
- }
- ]},
- {<<"</terms>; rel=\"copyright\"; anchor=\"#foo\"">>, [
- #{
- target => <<"/terms">>,
- rel => <<"copyright">>,
- attributes => [
- {<<"anchor">>, <<"#foo">>}
- ]
- }
- ]},
- % {<<"</TheBook/chapter2>; rel=\"previous\"; title*=UTF-8'de'letztes%20Kapitel, "
- % "</TheBook/chapter4>; rel=\"next\"; title*=UTF-8'de'n%c3%a4chstes%20Kapitel">>, [
- % %% @todo
- % ]}
- {<<"<http://example.org/>; rel=\"start http://example.net/relation/other\"">>, [
- #{
- target => <<"http://example.org/">>,
- rel => <<"start http://example.net/relation/other">>,
- attributes => []
- }
- ]},
- {<<"<https://example.org/>; rel=\"start\", "
- "<https://example.org/index>; rel=\"index\"">>, [
- #{
- target => <<"https://example.org/">>,
- rel => <<"start">>,
- attributes => []
- },
- #{
- target => <<"https://example.org/index">>,
- rel => <<"index">>,
- attributes => []
- }
- ]},
- {<<"</>; rel=\"previous\"; quoted=\"name=\\\"value\\\"\"">>, [
- #{
- target => <<"/">>,
- rel => <<"previous">>,
- attributes => [
- {<<"quoted">>, <<"name=\"value\"">>}
- ]
- }
- ]}
- ],
- [{iolist_to_binary(io_lib:format("~0p", [V])),
- fun() -> R = iolist_to_binary(link(V)) end} || {R, V} <- Tests].
- -endif.
|