blob: cf81b33a0874699fa753d57fe8b124bb9ac4d1a0 [file] [log] [blame]
% Licensed under the Apache License, Version 2.0 (the "License"); you may not
% use this file except in compliance with the License. You may obtain a copy of
% the License at
%
% http://www.apache.org/licenses/LICENSE-2.0
%
% Unless required by applicable law or agreed to in writing, software
% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
% License for the specific language governing permissions and limitations under
% the License.
-module(couch_mrview_http).
-export([
handle_all_docs_req/2,
handle_view_req/3,
handle_temp_view_req/2,
handle_info_req/3,
handle_compact_req/3,
handle_cleanup_req/2,
parse_qs/2
]).
-include_lib("couch/include/couch_db.hrl").
-include_lib("couch_mrview/include/couch_mrview.hrl").
-record(vacc, {
db,
req,
resp,
prepend,
etag
}).
handle_all_docs_req(#httpd{method='GET'}=Req, Db) ->
all_docs_req(Req, Db, undefined);
handle_all_docs_req(#httpd{method='POST'}=Req, Db) ->
Keys = get_view_keys(couch_httpd:json_body_obj(Req)),
all_docs_req(Req, Db, Keys);
handle_all_docs_req(Req, _Db) ->
couch_httpd:send_method_not_allowed(Req, "GET,POST,HEAD").
handle_view_req(#httpd{method='GET'}=Req, Db, DDoc) ->
[_, _, _, _, ViewName] = Req#httpd.path_parts,
couch_stats_collector:increment({httpd, view_reads}),
design_doc_view(Req, Db, DDoc, ViewName, undefined);
handle_view_req(#httpd{method='POST'}=Req, Db, DDoc) ->
[_, _, _, _, ViewName] = Req#httpd.path_parts,
Keys = get_view_keys(couch_httpd:json_body_obj(Req)),
couch_stats_collector:increment({httpd, view_reads}),
design_doc_view(Req, Db, DDoc, ViewName, Keys);
handle_view_req(Req, _Db, _DDoc) ->
couch_httpd:send_method_not_allowed(Req, "GET,POST,HEAD").
handle_temp_view_req(#httpd{method='POST'}=Req, Db) ->
couch_httpd:validate_ctype(Req, "application/json"),
ok = couch_db:check_is_admin(Db),
{Body} = couch_httpd:json_body_obj(Req),
DDoc = couch_mrview_util:temp_view_to_ddoc({Body}),
Keys = get_view_keys({Body}),
couch_stats_collector:increment({httpd, temporary_view_reads}),
design_doc_view(Req, Db, DDoc, <<"temp">>, Keys);
handle_temp_view_req(Req, _Db) ->
couch_httpd:send_method_not_allowed(Req, "POST").
handle_info_req(#httpd{method='GET'}=Req, Db, DDoc) ->
[_, _, Name, _] = Req#httpd.path_parts,
{ok, Info} = couch_mrview:get_info(Db, DDoc),
couch_httpd:send_json(Req, 200, {[
{name, Name},
{view_index, {Info}}
]});
handle_info_req(Req, _Db, _DDoc) ->
couch_httpd:send_method_not_allowed(Req, "GET").
handle_compact_req(#httpd{method='POST'}=Req, Db, DDoc) ->
ok = couch_db:check_is_admin(Db),
couch_httpd:validate_ctype(Req, "application/json"),
ok = couch_mrview:compact(Db, DDoc),
couch_httpd:send_json(Req, 202, {[{ok, true}]});
handle_compact_req(Req, _Db, _DDoc) ->
couch_httpd:send_method_not_allowd(Req, "POST").
handle_cleanup_req(#httpd{method='POST'}=Req, Db) ->
ok = couch_db:check_is_admin(Db),
couch_httpd:validate_ctype(Req, "application/json"),
ok = couch_mrview:cleanup(Db),
couch_httpd:send_json(Req, 202, {[{ok, true}]});
handle_cleanup_req(Req, _Db) ->
couch_httpd:send_method_not_allowed(Req, "POST").
all_docs_req(Req, Db, Keys) ->
case couch_db:is_system_db(Db) of
true ->
case (catch couch_db:check_is_admin(Db)) of
ok ->
do_all_docs_req(Req, Db, Keys);
_ ->
throw({forbidden, <<"Only admins can access _all_docs",
" of system databases.">>})
end;
false ->
do_all_docs_req(Req, Db, Keys)
end.
do_all_docs_req(Req, Db, Keys) ->
Args0 = parse_qs(Req, Keys),
ETagFun = fun(Sig, Acc0) ->
ETag = couch_httpd:make_etag(Sig),
case couch_httpd:etag_match(Req, ETag) of
true -> throw({etag_match, ETag});
false -> {ok, Acc0#vacc{etag=ETag}}
end
end,
Args = Args0#mrargs{preflight_fun=ETagFun},
{ok, Resp} = couch_httpd:etag_maybe(Req, fun() ->
VAcc0 = #vacc{db=Db, req=Req},
couch_mrview:query_all_docs(Db, Args, fun view_cb/2, VAcc0)
end),
case is_record(Resp, vacc) of
true -> {ok, Resp#vacc.resp};
_ -> {ok, Resp}
end.
design_doc_view(Req, Db, DDoc, ViewName, Keys) ->
Args0 = parse_qs(Req, Keys),
ETagFun = fun(Sig, Acc0) ->
ETag = couch_httpd:make_etag(Sig),
case couch_httpd:etag_match(Req, ETag) of
true -> throw({etag_match, ETag});
false -> {ok, Acc0#vacc{etag=ETag}}
end
end,
Args = Args0#mrargs{preflight_fun=ETagFun},
{ok, Resp} = couch_httpd:etag_maybe(Req, fun() ->
VAcc0 = #vacc{db=Db, req=Req},
couch_mrview:query_view(Db, DDoc, ViewName, Args, fun view_cb/2, VAcc0)
end),
case is_record(Resp, vacc) of
true -> {ok, Resp#vacc.resp};
_ -> {ok, Resp}
end.
view_cb({meta, Meta}, #vacc{resp=undefined}=Acc) ->
Headers = [{"ETag", Acc#vacc.etag}],
{ok, Resp} = couch_httpd:start_json_response(Acc#vacc.req, 200, Headers),
% Map function starting
Parts = case couch_util:get_value(total, Meta) of
undefined -> [];
Total -> [io_lib:format("\"total_rows\":~p", [Total])]
end ++ case couch_util:get_value(offset, Meta) of
undefined -> [];
Offset -> [io_lib:format("\"offset\":~p", [Offset])]
end ++ case couch_util:get_value(update_seq, Meta) of
undefined -> [];
UpdateSeq -> [io_lib:format("\"update_seq\":~p", [UpdateSeq])]
end ++ ["\"rows\":["],
Chunk = lists:flatten("{" ++ string:join(Parts, ",") ++ "\r\n"),
couch_httpd:send_chunk(Resp, Chunk),
{ok, Acc#vacc{resp=Resp, prepend=""}};
view_cb({row, Row}, #vacc{resp=undefined}=Acc) ->
% Reduce function starting
Headers = [{"ETag", Acc#vacc.etag}],
{ok, Resp} = couch_httpd:start_json_response(Acc#vacc.req, 200, Headers),
couch_httpd:send_chunk(Resp, ["{\"rows\":[\r\n", row_to_json(Row)]),
{ok, #vacc{resp=Resp, prepend=",\r\n"}};
view_cb({row, Row}, Acc) ->
% Adding another row
couch_httpd:send_chunk(Acc#vacc.resp, [Acc#vacc.prepend, row_to_json(Row)]),
{ok, Acc#vacc{prepend=",\r\n"}};
view_cb(complete, #vacc{resp=undefined}=Acc) ->
% Nothing in view
{ok, Resp} = couch_httpd:send_json(Acc#vacc.req, 200, {[{rows, []}]}),
{ok, Acc#vacc{resp=Resp}};
view_cb(complete, Acc) ->
% Finish view output
couch_httpd:send_chunk(Acc#vacc.resp, "\r\n]}"),
couch_httpd:end_json_response(Acc#vacc.resp),
{ok, Acc}.
row_to_json(Row) ->
Id = couch_util:get_value(id, Row),
row_to_json(Id, Row).
row_to_json(error, Row) ->
% Special case for _all_docs request with KEYS to
% match prior behavior.
Key = couch_util:get_value(key, Row),
Val = couch_util:get_value(value, Row),
Obj = {[{key, Key}, {error, Val}]},
?JSON_ENCODE(Obj);
row_to_json(Id0, Row) ->
Id = case Id0 of
undefined -> [];
Id0 -> [{id, Id0}]
end,
Key = couch_util:get_value(key, Row, null),
Val = couch_util:get_value(value, Row),
Doc = case couch_util:get_value(doc, Row) of
undefined -> [];
Doc0 -> [{doc, Doc0}]
end,
Obj = {Id ++ [{key, Key}, {value, Val}] ++ Doc},
?JSON_ENCODE(Obj).
get_view_keys({Props}) ->
case couch_util:get_value(<<"keys">>, Props) of
undefined ->
?LOG_DEBUG("POST with no keys member.", []),
undefined;
Keys when is_list(Keys) ->
Keys;
_ ->
throw({bad_request, "`keys` member must be a array."})
end.
parse_qs(Req, Keys) ->
Args = #mrargs{keys=Keys},
lists:foldl(fun({K, V}, Acc) ->
parse_qs(K, V, Acc)
end, Args, couch_httpd:qs(Req)).
parse_qs(Key, Val, Args) ->
case Key of
"" ->
Args;
"reduce" ->
Args#mrargs{reduce=parse_boolean(Val)};
"key" ->
JsonKey = ?JSON_DECODE(Val),
Args#mrargs{start_key=JsonKey, end_key=JsonKey};
"keys" ->
Args#mrargs{keys=?JSON_DECODE(Val)};
"startkey" ->
Args#mrargs{start_key=?JSON_DECODE(Val)};
"start_key" ->
Args#mrargs{start_key=?JSON_DECODE(Val)};
"startkey_docid" ->
Args#mrargs{start_key_docid=list_to_binary(Val)};
"start_key_doc_id" ->
Args#mrargs{start_key_docid=list_to_binary(Val)};
"endkey" ->
Args#mrargs{end_key=?JSON_DECODE(Val)};
"end_key" ->
Args#mrargs{end_key=?JSON_DECODE(Val)};
"endkey_docid" ->
Args#mrargs{end_key_docid=list_to_binary(Val)};
"end_key_doc_id" ->
Args#mrargs{end_key_docid=list_to_binary(Val)};
"limit" ->
Args#mrargs{limit=parse_pos_int(Val)};
"count" ->
throw({query_parse_error, <<"QS param `count` is not `limit`">>});
"stale" when Val == "ok" ->
Args#mrargs{stale=ok};
"stale" when Val == "update_after" ->
Args#mrargs{stale=update_after};
"stale" ->
throw({query_parse_error, <<"Invalid value for `stale`.">>});
"descending" ->
case parse_boolean(Val) of
true -> Args#mrargs{direction=rev};
_ -> Args#mrargs{direction=fwd}
end;
"skip" ->
Args#mrargs{skip=parse_pos_int(Val)};
"group" ->
case parse_boolean(Val) of
true -> Args#mrargs{group_level=exact};
_ -> Args#mrargs{group_level=0}
end;
"group_level" ->
Args#mrargs{group_level=parse_pos_int(Val)};
"inclusive_end" ->
Args#mrargs{inclusive_end=parse_boolean(Val)};
"include_docs" ->
Args#mrargs{include_docs=parse_boolean(Val)};
"update_seq" ->
Args#mrargs{update_seq=parse_boolean(Val)};
"conflicts" ->
Args#mrargs{conflicts=parse_boolean(Val)};
"list" ->
Args#mrargs{list=list_to_binary(Val)};
"callback" ->
Args#mrargs{callback=list_to_binary(Val)};
_ ->
BKey = list_to_binary(Key),
BVal = list_to_binary(Val),
Args#mrargs{extra=[{BKey, BVal} | Args#mrargs.extra]}
end.
parse_boolean(Val) ->
case string:to_lower(Val) of
"true" -> true;
"false" -> false;
_ ->
Msg = io_lib:format("Invalid boolean parameter: ~p", [Val]),
throw({query_parse_error, ?l2b(Msg)})
end.
parse_int(Val) ->
case (catch list_to_integer(Val)) of
IntVal when is_integer(IntVal) ->
IntVal;
_ ->
Msg = io_lib:format("Invalid value for integer: ~p", [Val]),
throw({query_parse_error, ?l2b(Msg)})
end.
parse_pos_int(Val) ->
case parse_int(Val) of
IntVal when IntVal >= 0 ->
IntVal;
_ ->
Fmt = "Invalid value for positive integer: ~p",
Msg = io_lib:format(Fmt, [Val]),
throw({query_parse_error, ?l2b(Msg)})
end.