summaryrefslogtreecommitdiff
path: root/src/couchdb/couch_httpd_form.erl
blob: f4fa2c182de502988107e953959b24de42cff12b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
% 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_httpd_form).
    
-export([handle_form_req/2]).


-include("couch_db.hrl").

-import(couch_httpd,
    [send_json/2,send_json/3,send_json/4,send_method_not_allowed/2,
    start_json_response/2,send_chunk/2,end_json_response/1,
    start_chunked_response/3, send_error/4]).
    
handle_form_req(#httpd{method='GET',path_parts=[_, _, DesignName, FormName, Docid]}=Req, Db) ->
    DesignId = <<"_design/", DesignName/binary>>,
    % Anyway we can dry up this error handling?
    case (catch couch_httpd_db:couch_doc_open(Db, DesignId, [], [])) of
    {not_found, missing} ->
        throw({not_found, missing_design_doc});
    {not_found, deleted} ->
        throw({not_found, deleted_design_doc});
    DesignDoc ->
        #doc{body={Props}} = DesignDoc,
        Lang = proplists:get_value(<<"language">>, Props, <<"javascript">>),
        case proplists:get_value(<<"forms">>, Props, nil) of
        {Forms} ->
            case proplists:get_value(FormName, Forms, nil) of
            nil ->
                throw({not_found, missing_form});
            FormSrc ->
                case (catch couch_httpd_db:couch_doc_open(Db, Docid, [], [])) of
                {not_found, missing} ->
                    throw({not_found, missing});
                {not_found, deleted} ->
                    throw({not_found, deleted});
                Doc ->
                    % ok we have everythign we need. let's make it happen.
                    send_form_response(Lang, FormSrc, Doc, Req, Db)
                end
            end;
        nil ->
            throw({not_found, missing_form})
        end
    end;

handle_form_req(#httpd{method='GET'}=Req, _Db) ->
    send_error(Req, 404, <<"form_error">>, <<"Invalid path.">>);

handle_form_req(Req, _Db) ->
    send_method_not_allowed(Req, "GET,HEAD").


send_form_response(Lang, FormSrc, #doc{revs=[DocRev|_]}=Doc, #httpd{mochi_req=MReq}=Req, Db) ->
    % make a term with etag-effecting Req components, but not always changing ones.
    Headers = MReq:get(headers),
    Hlist = mochiweb_headers:to_list(Headers),
    Accept = proplists:get_value('Accept', Hlist),
    <<SigInt:128/integer>> = erlang:md5(term_to_binary({Lang, FormSrc, DocRev, Accept})),
    CurrentEtag = list_to_binary("\"" ++ lists:flatten(io_lib:format("form_~.36B",[SigInt])) ++ "\""),
    EtagsToMatch = string:tokens(
                couch_httpd:header_value(Req, "If-None-Match", ""), ", "),
    % We know our etag now                
    case lists:member(binary_to_list(CurrentEtag), EtagsToMatch) of
    true ->
        % the client has this in their cache.
        couch_httpd:send_response(Req, 304, [{"Etag", CurrentEtag}], <<>>);
    false ->
        % Run the external form renderer.
        {JsonResponse} = couch_query_servers:render_doc_form(Lang, FormSrc, Doc, Req, Db),
        % Here we embark on the delicate task of replacing or creating the  
        % headers on the JsonResponse object. We need to control the Etag and 
        % Vary headers. If the external function controls the Etag, we'd have to 
        % run it to check for a match, which sort of defeats the purpose.
        JsonResponse2 = case proplists:get_value(<<"headers">>, JsonResponse, nil) of
        nil ->
            % no JSON headers
            % add our Etag and Vary headers to the response
            [{<<"headers">>, {[{<<"Etag">>, CurrentEtag}, {<<"Vary">>, <<"Accept">>}]}} | JsonResponse];
        {JsonHeaders} ->
            [case Field of
            {<<"headers">>, {JsonHeaders}} -> % add our headers
                JsonHeadersEtagged = set_or_replace_header({<<"Etag">>, CurrentEtag}, JsonHeaders),
                JsonHeadersVaried = set_or_replace_header({<<"Vary">>, <<"Accept">>}, JsonHeadersEtagged),
                {<<"headers">>, {JsonHeadersVaried}};
            _ -> % skip non-header fields
                Field
            end || Field <- JsonResponse]
        end,
        couch_httpd_external:send_external_response(Req, {JsonResponse2})    
    end.

set_or_replace_header(H, L) ->
    set_or_replace_header(H, L, []).

set_or_replace_header({Key, NewValue}, [{Key, _OldVal} | Headers], Acc) ->
    % drop matching keys
    set_or_replace_header({Key, NewValue}, Headers, Acc);
set_or_replace_header({Key, NewValue}, [{OtherKey, OtherVal} | Headers], Acc) ->
    % something else is next, leave it alone.
    set_or_replace_header({Key, NewValue}, Headers, [{OtherKey, OtherVal} | Acc]);
set_or_replace_header({Key, NewValue}, [], Acc) ->
    % end of list, add ours
    [{Key, NewValue}|Acc].