src/sbw.erl

% vim: ts=4 sw=4 et
% Simple Bridge
% Copyright (c) 2008-2010 Rusty Klophaus
% Copyright (c) 2013 Jesse Gumm
% See MIT-LICENSE for licensing information.

-module (sbw).
-include("simple_bridge.hrl").

%% REQUEST BRIDGE EXPORTS
-export([
    new/2,
    set_multipart/3,
    set_error/2,
    cache_post_params/1,
    protocol/1,
    host/1,
    path/1,
    uri/1,
    peer_ip/1,
    peer_port/1,
    get_peername/1,
    error/1,
    socket/1,
    protocol_version/1,
    request_body/1,
    request_method/1,

    headers/1,
    headers_list/1,
    headers_map/1,
    header/2,
    header_lower/2,

    cookies/1,
    cookie/2,

    query_params/1,
    query_param/2,
    query_param/3,

    post_params/1,
    post_param/2,
    post_param/3,
    post_param_group/2,
    post_param_group/3,

    param_group/2,
    param_group/3,
    query_param_group/2,
    query_param_group/3,

    params/1,
    param/2,
    param/3,

    deep_params/1,
    deep_param/2,

    deep_query_params/1,
    deep_query_param/2,

    deep_post_params/1,
    deep_post_param/2,

    post_files/1,
    recv_from_socket/3,
    insert_into/3
]).

%% RESPONSE BRIDGE EXPORTS
-export([
    set_status_code/2,
    get_status_code/1,

    set_header/3,
    get_response_header/2,
    clear_headers/1,
    set_cookie/3,
    set_cookie/4,
    set_cookie/5,
    clear_cookies/1,
    set_response_data/2,
    set_response_file/2,
    build_response/1,

    %% retained for backwards compatibility
    status_code/2,
    header/3,
    cookie/3,
    cookie/5,
    data/2,
    file/2
]).

%% TODO: Add Typespecs to all these

%% REQUEST WRAPPERS

new(Mod, Req) ->
    Bridge = #sbw{
        mod=Mod,
        req=Req
    },
    %% We don't cache the Post params here because the multipart parser might
    %% do it for us. See simple_bridge:make_nocatch/2
    Bridge2 = cache_headers(Bridge),
    Bridge3 = cache_query_params(Bridge2),
    cache_cookies(Bridge3).


set_multipart(PostParams, PostFiles, Wrapper) ->
    Wrapper#sbw{
        is_multipart=true,
        post_params=PostParams,
        post_files=PostFiles
    }.

%% PRECACHING HEADERS, POST PARAMS AND QUERY PARAMS
%% So we don't have to convert to and from binary/lists/atom for different
%% backends. We do it once per request.
%% Experimenting with caching the headers as maps
%% Something to consider here is that the normalize_headers function looks at binaries/lists 
cache_headers(Wrapper) ->
    Mod = Wrapper#sbw.mod,
    Req = Wrapper#sbw.req,
    FormattedHeaders = cache_headers_by_type(Req, Mod),
    Wrapper#sbw{headers=FormattedHeaders}.

cache_headers_by_type(Req, Mod) ->
    case Mod:native_header_type() of
        map ->
            cache_headers_map(Req, Mod);
        list ->
            cache_headers_list(Req, Mod)
    end.

cache_headers_map(Req, Mod) ->
    Headers = Mod:headers(Req),
    HeadersList = maps:to_list(Headers),
    normalize_headers(HeadersList).
    %error_logger:info_msg("Raw Header: ~p",[Headers]),
    %%% filter out undefineds, we don't care about them
    %Filtered = maps:filter(fun(_K, V) -> V =/= undefined end, Headers),
    %%% format the headers
    %maps:map(fun(K,V) -> normalize_header({K,V}) end, Filtered).

cache_headers_list(Req, Mod) ->
    Headers = Mod:headers(Req),
    normalize_headers(Headers).

normalize_headers(Headers) ->
    ListHeaders = [normalize_header(Header) || Header={_K,V} <- Headers, V =/= undefined],
    maps:from_list(ListHeaders).

cache_cookies(Wrapper) ->
    Mod = Wrapper#sbw.mod,
    Req = Wrapper#sbw.req,
    Wrapper#sbw{
        cookies=[normalize_header({K,V}) || {K,V} <- Mod:cookies(Req), V=/=undefined]
    }.

normalize_header({Key0, Val0}) ->
    Key = simple_bridge_util:binarize_header(Key0),
    Val = simple_bridge_util:to_binary(Val0),
    {Key, Val}.

cache_post_params(Wrapper) ->
    Mod = Wrapper#sbw.mod,
    Req = Wrapper#sbw.req,
    PostParams = Mod:post_params(Req),
    Wrapper#sbw{
        post_params=[normalize_param(Param) || Param <- PostParams]
    }.

cache_query_params(Wrapper) ->
    Mod = Wrapper#sbw.mod,
    Req = Wrapper#sbw.req,
    Wrapper#sbw{
      query_params=[normalize_param({K,V}) || {K,V} <- Mod:query_params(Req)]
    }.

normalize_param({K, V}) ->  
    {simple_bridge_util:to_binary(K), simple_bridge_util:to_binary(V)}.

set_error(Error, Wrapper) ->
    Wrapper#sbw{
        error=Error
    }.

get_peername(Wrapper) ->
    inet:peername(socket(Wrapper)).

error(Wrapper) ->
    Wrapper#sbw.error.

-define(PASSTHROUGH(FunctionName),
    FunctionName(Wrapper) -> 
        (Wrapper#sbw.mod):FunctionName(Wrapper#sbw.req)).

?PASSTHROUGH(protocol).
?PASSTHROUGH(host).
?PASSTHROUGH(path).
?PASSTHROUGH(uri).
?PASSTHROUGH(peer_ip).
?PASSTHROUGH(peer_port).
?PASSTHROUGH(protocol_version).
?PASSTHROUGH(socket).

request_body(Wrapper) ->
    Mod = Wrapper#sbw.mod,
    Req = Wrapper#sbw.req,
    Body = Mod:request_body(Req),
    simple_bridge_util:to_binary(Body).

request_method(Wrapper) ->
    Mod = Wrapper#sbw.mod,
    Req = Wrapper#sbw.req,
    case Mod:request_method(Req) of
        Method when is_binary(Method) ->
            list_to_atom(binary_to_list(Method));
        Method when is_list(Method) ->
            list_to_atom(Method);
        Method when is_atom(Method) ->
            Method
     end.


recv_from_socket(Length, Timeout, Wrapper) ->
    Mod = Wrapper#sbw.mod,
    Req = Wrapper#sbw.req,
    case erlang:function_exported(Mod, recv_from_socket, 3) of
        true ->  Mod:recv_from_socket(Length, Timeout, Req);
        false -> throw({not_supported, Mod, recv_from_socket})
    end.

%% FILES

post_files(Wrapper) ->
    Wrapper#sbw.post_files.

%% REQUEST HEADERS

headers(Wrapper) ->
    headers_map(Wrapper).

headers_map(Wrapper) ->
    Wrapper#sbw.headers.

headers_list(Wrapper) ->
    maps:to_list(Wrapper#sbw.headers).

header(Header, Wrapper) ->
    BinHeader = simple_bridge_util:binarize_header(Header),
    case maps:find(BinHeader, Wrapper#sbw.headers) of
        error -> undefined;
        {ok, Val} ->
            if  is_list(Header);
                is_atom(Header)   -> binary_to_list(Val);
                is_binary(Header) -> Val
            end
    end.

header_lower(Header, Wrapper) ->
    case header(Header, Wrapper) of
        undefined ->
            undefined;
        Other when is_binary(Header) ->
            list_to_binary(string:to_lower(Other));
        Other when is_atom(Header); is_list(Header) ->
            string:to_lower(Other)
    end.

%% REQUEST COOKIES

cookies(Wrapper) ->
    Wrapper#sbw.cookies.

cookie(Cookie, Wrapper) ->
    BinCookie = list_to_binary(string:to_lower(simple_bridge_util:to_list(Cookie))),
    case lists:keyfind(BinCookie, 1, Wrapper#sbw.cookies) of
        false -> undefined;
        {_, Val} ->
            if  is_list(Cookie);
                is_atom(Cookie)   -> binary_to_list(Val);
                is_binary(Cookie) -> Val
            end
    end.

%% PARAM GROUPS

param_group(Param, Wrapper) ->
     param_group(Param, [], Wrapper).

param_group(Param, DefaultValue, Wrapper) ->
    query_param_group(Param, DefaultValue, Wrapper) ++ post_param_group(Param, DefaultValue, Wrapper).

query_param_group(Param, DefaultValue, Wrapper) ->
    find_param_group(Param, DefaultValue, query_params(Wrapper)).

query_param_group(Param, Wrapper) ->
    query_param_group(Param, [], Wrapper).

post_param_group(Param, Wrapper) ->
    post_param_group(Param, [], Wrapper).

post_param_group(Param, DefaultValue, Wrapper) ->
    find_param_group(Param, DefaultValue, post_params(Wrapper)).

%% QUERY PARAMS

query_params(Wrapper) ->
    Wrapper#sbw.query_params.

query_param(Param, Wrapper) ->
    query_param(Param, undefined, Wrapper).

query_param(Param, DefaultValue, Wrapper) ->
    find_param(Param, DefaultValue, query_params(Wrapper)).

%% POST PARAMS

post_params(Wrapper) ->
    Wrapper#sbw.post_params.

post_param(Param, Wrapper) ->
    post_param(Param, undefined, Wrapper).

post_param(Param, DefaultValue, Wrapper) ->
    find_param(Param, DefaultValue, post_params(Wrapper)).

%% AGNOSTIC PARAMS

params(Wrapper) ->
    post_params(Wrapper) ++ query_params(Wrapper).

param(Param, Wrapper) ->
    param(Param, undefined, Wrapper).

param(Param, DefaultValue, Wrapper) ->
    case post_param(Param, Wrapper) of
        undefined -> query_param(Param, DefaultValue, Wrapper);
        V -> V
    end.

%% FIND PARAM

find_param(Param, Default, ParamList) when is_binary(Param) ->
    case lists:keyfind(Param, 1, ParamList) of
        {_, Val} -> Val;
        false -> Default
    end;
find_param(Param, Default, ParamList) when is_atom(Param); is_list(Param) ->
    Param1 = simple_bridge_util:to_binary(Param),
    simple_bridge_util:maybe_to_list(find_param(Param1, Default, ParamList)).


find_param_group(Param, Default, ParamList) when is_binary(Param) ->
    case [V || {K,V} <- ParamList, K=:=Param] of
        [] -> Default;
        L -> L
    end;
find_param_group(Param, Default, ParamList) when is_list(Param); is_atom(Param) ->
    Param1 = simple_bridge_util:to_binary(Param),
    ResultList = find_param_group(Param1, Default, ParamList),
    [simple_bridge_util:to_list(V) || V <- ResultList].

%% DEEP PARAM STUFF

deep_params(Wrapper) ->
    Params = params(Wrapper),
    parse_deep_post_params(Params, []).

deep_param(Path, Wrapper) ->
    find_deep_post_param(Path, deep_params(Wrapper)).

deep_query_params(Wrapper) ->
    Params = query_params(Wrapper),
    parse_deep_post_params(Params, []).

deep_query_param(Path, Wrapper) ->
    find_deep_post_param(Path, deep_query_params(Wrapper)).

deep_post_params(Wrapper) ->
    Params = post_params(Wrapper),
    parse_deep_post_params(Params, []).

deep_post_param(Path, Wrapper) ->
    find_deep_post_param(Path, deep_post_params(Wrapper)).

find_deep_post_param([], Params) ->
    Params;
find_deep_post_param([Index|Rest], Params) when is_integer(Index) ->
    find_deep_post_param(Rest, lists:nth(Index, Params));
find_deep_post_param([Index|Rest], Params) when is_list(Index) ->
    find_deep_post_param(Rest, proplists:get_value(Index, Params)).

parse_deep_post_params([], Acc) ->
    Acc;
parse_deep_post_params([{Key, Value}|Rest], Acc) ->
    case re:run(Key, "^(\\w+)(?:\\[([\\w-\\[\\]]*)\\])?$", [{capture, all_but_first, list}]) of
        {match, [Key]} ->
            parse_deep_post_params(Rest, [{Key, Value}|Acc]);
        {match, [KeyName, Path]} ->
            PathList = re:split(Path, "\\]\\[", [{return, list}]),
            parse_deep_post_params(Rest, insert_into(Acc, [KeyName|PathList], Value));
        Other ->
            error_logger:warning_msg("Unable to parse key: ~p. Returned: ~p", [Key, Other]),
            parse_deep_post_params(Rest, Acc)
    end.

insert_into(_List, [], Value) ->
    Value;
insert_into(undefined, PathList, Value) ->
    insert_into([], PathList, Value);
insert_into(N, PathList, Value) when is_integer(N) ->
    insert_into([], PathList, Value);
insert_into(List, [ThisKey|Rest], Value) ->
    case catch list_to_integer(ThisKey) of
        {'EXIT', _} ->
            ExistingVal = proplists:get_value(ThisKey, List),
            [{ThisKey, insert_into(ExistingVal, Rest, Value)}|
                proplists:delete(ThisKey, List)];
        N when N < erlang:length(List) ->
            ExistingVal = lists:nth(N+1, List),
            lists:sublist(List, N) ++ [insert_into(ExistingVal, Rest, Value)|
                lists:nthtail(N+1, List)];
        N when N >= erlang:length(List) ->
            List ++ lists:reverse([insert_into(undefined, Rest, Value)|
                    lists:seq(0, N - erlang:length(List) - 1)])
    end.

%% RESPONSE WRAPPERS

set_status_code(StatusCode, Wrapper) ->
    update_response(fun(Res) ->
        Res#response{status_code=StatusCode}
    end, Wrapper).

get_status_code(Wrapper) ->
    (Wrapper#sbw.response)#response.status_code.

set_header(Name0, Value, Wrapper) ->
    Name = simple_bridge_util:binarize_header(Name0),
    update_response(fun(Res) ->
        Header = #header { name=Name, value=Value },
        Headers = Res#response.headers,
        Headers1 = [X || X <- Headers, X#header.name /= Name orelse X#header.name =:= <<"set-cookie">>],
        Headers2 = [Header|Headers1],
        Res#response{headers=Headers2}
    end, Wrapper).

get_response_header(Name0, Wrapper) ->
    Name = simple_bridge_util:binarize_header(Name0),
    Res = Wrapper#sbw.response,
    Headers = [H#header.value || H <- Res#response.headers, H#header.name == Name],
    case Headers of
        [] -> undefined;
        [V] -> V
    end.

clear_headers(Wrapper) ->
    update_response(fun(Res) ->
        Res#response{headers=[]}
    end, Wrapper).

set_cookie(Name, Value, Wrapper) ->
    set_cookie(Name, Value, [], Wrapper).

set_cookie(Name, Value, Options, Wrapper) ->
    update_response(fun(Res) ->
        Cookie = #cookie { name=Name,
                           value=Value,
                           domain=proplists:get_value(domain, Options),
                           path=proplists:get_value(path, Options, "/"),
                           max_age=proplists:get_value(max_age, Options, 3600),
                           secure=proplists:get_value(secure, Options, false),
                           http_only=proplists:get_value(http_only, Options, false),
                           same_site=proplists:get_value(same_site, Options, lax)
                         },
        Cookies = Res#response.cookies,
        Cookies1 = [X || X <- Cookies, X#cookie.name /= Name],
        Cookies2 = [Cookie|Cookies1],
        Res#response{cookies=Cookies2}
    end, Wrapper).

set_cookie(Name, Value, Path, MinutesToLive, Wrapper) ->
    set_cookie(Name, Value, [{path, Path}, {max_age, MinutesToLive*60}], Wrapper).

clear_cookies(Wrapper) ->
    update_response(fun(Res) ->
        Res#response{cookies=[]}
    end, Wrapper).

set_response_data(Data, Wrapper) ->
    update_response(fun(Res) ->
        Res#response{data={data, Data}}
    end, Wrapper).

set_response_file(Path, Wrapper) ->
    update_response(fun(Res) ->
        Res#response{data={file,Path}}
    end, Wrapper).

build_response(Wrapper) ->
    Mod = Wrapper#sbw.mod,
    Req = Wrapper#sbw.req,
    Res = Wrapper#sbw.response,
    Mod:build_response(Req,Res).

update_response(Fun, Wrapper) ->
    NewRes = Fun(Wrapper#sbw.response),
    Wrapper#sbw{response=NewRes}.


%% DEPRECATED RESPONSE WRAPPERS

status_code(StatusCode, Wrapper) ->
    set_status_code(StatusCode, Wrapper).

header(Name, Value, Wrapper) ->
    set_header(Name, Value, Wrapper).

cookie(Name, Value, Wrapper) ->
    set_cookie(Name, Value, Wrapper).

cookie(Name, Value, Path, MinutesToLive, Wrapper) ->
    set_cookie(Name, Value, Path, MinutesToLive, Wrapper).

data(Data, Wrapper) ->
    set_response_data(Data, Wrapper).

file(File, Wrapper) ->
    set_response_file(File, Wrapper).