1%%% -*- erlang -*-
2%%%
3%%% This file is part of couchbeam released under the MIT license.
4%%% See the NOTICE for more information.
5
6-module(couchbeam_util).
7
8-include_lib("hackney/include/hackney.hrl").
9-include_lib("hackney/include/hackney_lib.hrl").
10
11-export([dbname/1]).
12-export([encode_docid/1, encode_att_name/1]).
13-export([parse_options/1, parse_options/2]).
14-export([to_list/1, to_binary/1, to_integer/1, to_atom/1]).
15-export([encode_query/1, encode_query_value/2]).
16-export([oauth_header/3]).
17-export([propmerge/3, propmerge1/2]).
18-export([get_value/2, get_value/3]).
19-export([deprecated/3, shutdown_sync/1]).
20-export([start_app_deps/1, get_app_env/2]).
21-export([encode_docid1/1, encode_docid_noop/1]).
22-export([force_param/3]).
23-export([proxy_token/2, proxy_header/3]).
24
25-define(PROXY_AUTH_HEADERS,[
26    {username,<<"X-Auth-CouchDB-UserName">>},
27    {roles,<<"X-Auth-CouchDB-Roles">>},
28    {token,<<"X-Auth-CouchDB-Token">>}]).
29
30-define(ENCODE_DOCID_FUNC, encode_docid1).
31
32dbname(DbName) when is_list(DbName) ->
33    list_to_binary(DbName);
34dbname(DbName) when is_binary(DbName) ->
35    DbName;
36dbname(DbName) ->
37    erlang:error({illegal_database_name, DbName}).
38
39encode_att_name(Name) when is_binary(Name) ->
40    encode_att_name(xmerl_ucs:from_utf8(Name));
41encode_att_name(Name) ->
42    Parts = lists:foldl(fun(P, Att) ->
43               [xmerl_ucs:to_utf8(P)|Att]
44       end, [], string:tokens(Name, "/")),
45    lists:flatten(Parts).
46
47encode_docid(DocId) when is_list(DocId) ->
48    encode_docid(list_to_binary(DocId));
49encode_docid(DocId)->
50    ?ENCODE_DOCID_FUNC(DocId).
51
52encode_docid1(DocId) ->
53    case DocId of
54        << "_design/", Rest/binary >> ->
55            Rest1 = hackney_url:urlencode(Rest, [noplus]),
56            <<"_design/", Rest1/binary >>;
57        _ ->
58            hackney_url:urlencode(DocId, [noplus])
59    end.
60
61encode_docid_noop(DocId) ->
62    DocId.
63
64%% @doc Encode needed value of Query proplists in json
65encode_query([]) ->
66    [];
67encode_query(QSL) when is_list(QSL) ->
68    lists:foldl(fun({K, V}, Acc) ->
69        V1 = encode_query_value(K, V),
70        [{K, V1}|Acc]
71    end, [], QSL);
72encode_query(QSL) ->
73    QSL.
74
75%% @doc Encode value in JSON if needed depending on the key
76encode_query_value(K, V) when is_atom(K) ->
77    encode_query_value(atom_to_list(K), V);
78encode_query_value(K, V) when is_binary(K) ->
79    encode_query_value(binary_to_list(K), V);
80encode_query_value(_K, V) -> V.
81
82% build oauth header
83oauth_header(Url, Action, OauthProps) when is_binary(Url) ->
84    oauth_header(binary_to_list(Url),Action, OauthProps);
85oauth_header(Url, Action, OauthProps) ->
86    #hackney_url{qs=QS} = hackney_url:parse_url(Url),
87    QSL = [{binary_to_list(K), binary_to_list(V)} || {K,V} <-
88                                                     hackney_url:parse_qs(QS)],
89
90    % get oauth paramerers
91    ConsumerKey = to_list(get_value(consumer_key, OauthProps)),
92    Token = to_list(get_value(token, OauthProps)),
93    TokenSecret = to_list(get_value(token_secret, OauthProps)),
94    ConsumerSecret = to_list(get_value(consumer_secret, OauthProps)),
95    SignatureMethodStr = to_list(get_value(signature_method,
96            OauthProps, "HMAC-SHA1")),
97
98    SignatureMethodAtom = case SignatureMethodStr of
99        "PLAINTEXT" ->
100            plaintext;
101        "HMAC-SHA1" ->
102            hmac_sha1;
103        "RSA-SHA1" ->
104            rsa_sha1
105    end,
106    Consumer = {ConsumerKey, ConsumerSecret, SignatureMethodAtom},
107    Method = case Action of
108        delete -> "DELETE";
109        get -> "GET";
110        post -> "POST";
111        put -> "PUT";
112        head -> "HEAD"
113    end,
114    Params = oauth:sign(Method, Url, QSL, Consumer, Token, TokenSecret) -- QSL,
115
116    Realm = "OAuth " ++ oauth:header_params_encode(Params),
117    {<<"Authorization">>, list_to_binary(Realm)}.
118
119
120%% @doc merge 2 proplists. All the Key - Value pairs from both proplists
121%% are included in the new proplists. If a key occurs in both dictionaries
122%% then Fun is called with the key and both values to return a new
123%% value. This a wreapper around dict:merge
124propmerge(F, L1, L2) ->
125	dict:to_list(dict:merge(F, dict:from_list(L1), dict:from_list(L2))).
126
127%% @doc Update a proplist with values of the second. In case the same
128%% key is in 2 proplists, the value from the first are kept.
129propmerge1(L1, L2) ->
130    propmerge(fun(_, V1, _) -> V1 end, L1, L2).
131
132%% @doc replace a value in a proplist
133force_param(Key, Value, Options) ->
134    case couchbeam_util:get_value(Key, Options) of
135        undefined ->
136            [{Key, Value} | Options];
137        _ ->
138            lists:keystore(Key, 1, Options, {Key, Value})
139    end.
140
141%% @doc emulate proplists:get_value/2,3 but use faster lists:keyfind/3
142-spec get_value(Key :: term(), Prop :: [term()]) -> term().
143get_value(Key, Prop) ->
144    get_value(Key, Prop, undefined).
145
146-spec get_value(Key :: term(), Prop :: [term()], Default :: term()) -> term().
147get_value(Key, Prop, Default) ->
148    case lists:keyfind(Key, 1, Prop) of
149	false ->
150	    case lists:member(Key, Prop) of
151		true -> true;
152		false -> Default
153	    end;
154	{Key, V} -> % only return V if a two-tuple is found
155	    V;
156	Other when is_tuple(Other) -> % otherwise return the default
157	    Default
158    end.
159
160%% @doc make view options a list
161parse_options(Options) ->
162    parse_options(Options, []).
163
164parse_options([], Acc) ->
165    Acc;
166parse_options([V|Rest], Acc) when is_atom(V) ->
167    parse_options(Rest, [{atom_to_list(V), true}|Acc]);
168parse_options([{K,V}|Rest], Acc) when is_list(K) ->
169    parse_options(Rest, [{K,V}|Acc]);
170parse_options([{K,V}|Rest], Acc) when is_binary(K) ->
171    parse_options(Rest, [{binary_to_list(K),V}|Acc]);
172parse_options([{K,V}|Rest], Acc) when is_atom(K) ->
173    parse_options(Rest, [{atom_to_list(K),V}|Acc]);
174parse_options(_,_) ->
175    fail.
176
177to_binary(V) when is_binary(V) ->
178    V;
179to_binary(V) when is_list(V) ->
180    try
181        list_to_binary(V)
182    catch
183        _ ->
184            list_to_binary(io_lib:format("~p", [V]))
185    end;
186to_binary(V) when is_atom(V) ->
187    list_to_binary(atom_to_list(V));
188to_binary(V) ->
189    V.
190
191to_integer(V) when is_integer(V) ->
192    V;
193to_integer(V) when is_list(V) ->
194    erlang:list_to_integer(V);
195to_integer(V) when is_binary(V) ->
196    erlang:list_to_integer(binary_to_list(V)).
197
198to_list(V) when is_list(V) ->
199    V;
200to_list(V) when is_binary(V) ->
201    binary_to_list(V);
202to_list(V) when is_atom(V) ->
203    atom_to_list(V);
204to_list(V) ->
205    V.
206
207to_atom(V) when is_atom(V) ->
208    V;
209to_atom(V) when is_list(V) ->
210    list_to_atom(V);
211to_atom(V) when is_binary(V) ->
212    list_to_atom(binary_to_list(V));
213to_atom(V) ->
214    list_to_atom(lists:flatten(io_lib:format("~p", [V]))).
215
216deprecated(Old, New, When) ->
217    io:format(
218      <<
219        "WARNING: function deprecated~n"
220        "Function '~p' has been deprecated~n"
221        "in favor of '~p'.~n"
222        "'~p' will be removed ~s.~n~n"
223      >>, [Old, New, Old, When]).
224
225shutdown_sync(Pid) when not is_pid(Pid)->
226    ok;
227shutdown_sync(Pid) ->
228    MRef = erlang:monitor(process, Pid),
229    try
230        catch unlink(Pid),
231        catch exit(Pid, shutdown),
232        receive
233        {'DOWN', MRef, _, _, _} ->
234            ok
235        end
236    after
237        erlang:demonitor(MRef, [flush])
238    end.
239
240%% @spec start_app_deps(App :: atom()) -> ok
241%% @doc Start depedent applications of App.
242start_app_deps(App) ->
243    {ok, DepApps} = application:get_key(App, applications),
244    [ensure_started(A) || A <- DepApps],
245    ok.
246
247%% @spec ensure_started(Application :: atom()) -> ok
248%% @doc Start the named application if not already started.
249ensure_started(App) ->
250    case application:start(App) of
251	ok ->
252	    ok;
253	{error, {already_started, App}} ->
254	    ok
255    end.
256
257get_app_env(Env, Default) ->
258    case application:get_env(couchbeam, Env) of
259        {ok, Val} -> Val;
260        undefined -> Default
261    end.
262
263proxy_header(UserName,Roles,Secret) ->
264    proxy_header(UserName,Roles,Secret,?PROXY_AUTH_HEADERS).
265
266proxy_header(UserName,Roles,Secret,HeaderNames) ->
267    proxy_header_token(UserName,Roles,proxy_token(Secret,UserName),HeaderNames).
268
269proxy_header_token(UserName,Roles,Token,L) ->
270[
271    {hgv(username,L), UserName},
272    {hgv(roles,L), Roles},
273    {hgv(token,L), Token}
274].
275
276hgv(N,L) ->
277    get_value(N,L,get_value(N,?PROXY_AUTH_HEADERS)).
278
279proxy_token(Secret,UserName) ->
280    hackney_bstr:to_hex(hmac(sha, Secret, UserName)).
281
282hmac(Alg, Key, Data) ->
283    case {Alg, erlang:function_exported(crypto, hmac, 3)} of
284        {_, true} ->
285            crypto:hmac(Alg, Key, Data);
286        {sha, false} ->
287            crypto:sha_mac(Key, Data);
288        {Alg, false} ->
289            throw({unsupported, Alg})
290    end.
291