Allow limiting maximum document body size

Configuration is via the `couchdb.max_document_size`. In the past that
was implemented as a maximum http request body size and this finally implements
it by actually checking a document's body size.

COUCHDB-2992
diff --git a/src/couch_doc.erl b/src/couch_doc.erl
index a913eee..381ad4b 100644
--- a/src/couch_doc.erl
+++ b/src/couch_doc.erl
@@ -13,7 +13,7 @@
 -module(couch_doc).
 
 -export([to_doc_info/1,to_doc_info_path/1,parse_rev/1,parse_revs/1,rev_to_str/1,revs_to_strs/1]).
--export([from_json_obj/1,to_json_obj/2,has_stubs/1, merge_stubs/2]).
+-export([from_json_obj/1, from_json_obj_validate/1, to_json_obj/2,has_stubs/1, merge_stubs/2]).
 -export([validate_docid/1, get_validate_doc_fun/1]).
 -export([doc_from_multi_part_stream/2, doc_from_multi_part_stream/3]).
 -export([doc_to_multi_part_stream/5, len_doc_to_multi_part_stream/4]).
@@ -124,6 +124,16 @@
         ++ to_json_attachments(Doc#doc.atts, Options)
     }.
 
+from_json_obj_validate(EJson) ->
+    MaxSize = config:get_integer("couchdb", "max_document_size", 4294967296),
+    Doc = from_json_obj(EJson),
+    case erlang:external_size(Doc#doc.body) =< MaxSize of
+        true ->
+             Doc;
+        false ->
+            throw({request_entity_too_large, Doc#doc.id})
+    end.
+
 from_json_obj({Props}) ->
     transfer_fields(Props, #doc{body=[]});
 
@@ -414,7 +424,7 @@
     {{started_open_doc_revs, NewRef}, Parser, _ParserRef} ->
         restart_open_doc_revs(Parser, Ref, NewRef);
     {{doc_bytes, Ref, DocBytes}, Parser, ParserRef} ->
-        Doc = from_json_obj(?JSON_DECODE(DocBytes)),
+        Doc = from_json_obj_validate(?JSON_DECODE(DocBytes)),
         erlang:put(mochiweb_request_recv, true),
         % we'll send the Parser process ID to the remote nodes so they can
         % retrieve their own copies of the attachment data
diff --git a/src/couch_httpd_db.erl b/src/couch_httpd_db.erl
index 3793a06..e1af1bf 100644
--- a/src/couch_httpd_db.erl
+++ b/src/couch_httpd_db.erl
@@ -256,7 +256,7 @@
 
 db_req(#httpd{method='POST',path_parts=[_DbName]}=Req, Db) ->
     couch_httpd:validate_ctype(Req, "application/json"),
-    Doc = couch_doc:from_json_obj(couch_httpd:json_body(Req)),
+    Doc = couch_doc:from_json_obj_validate(couch_httpd:json_body(Req)),
     validate_attachment_names(Doc),
     Doc2 = case Doc#doc.id of
         <<"">> ->
@@ -319,7 +319,7 @@
         true ->
             Docs = lists:map(
                 fun({ObjProps} = JsonObj) ->
-                    Doc = couch_doc:from_json_obj(JsonObj),
+                    Doc = couch_doc:from_json_obj_validate(JsonObj),
                     validate_attachment_names(Doc),
                     Id = case Doc#doc.id of
                         <<>> -> couch_uuids:new();
@@ -353,7 +353,7 @@
             end;
         false ->
             Docs = lists:map(fun(JsonObj) ->
-                    Doc = couch_doc:from_json_obj(JsonObj),
+                    Doc = couch_doc:from_json_obj_validate(JsonObj),
                     validate_attachment_names(Doc),
                     Doc
                 end, DocsArray),
@@ -809,7 +809,7 @@
     end,
     Doc#doc{id=DocId, revs=Revs2};
 couch_doc_from_req(Req, DocId, Json) ->
-    couch_doc_from_req(Req, DocId, couch_doc:from_json_obj(Json)).
+    couch_doc_from_req(Req, DocId, couch_doc:from_json_obj_validate(Json)).
 
 % Useful for debugging
 % couch_doc_open(Db, DocId) ->
diff --git a/test/couch_doc_json_tests.erl b/test/couch_doc_json_tests.erl
index 9003d06..ce099d1 100644
--- a/test/couch_doc_json_tests.erl
+++ b/test/couch_doc_json_tests.erl
@@ -38,6 +38,8 @@
     ok;
 mock(config) ->
     meck:new(config, [passthrough]),
+    meck:expect(config, get_integer,
+        fun("couchdb", "max_document_size", 4294967296) -> 1024 end),
     meck:expect(config, get, fun(_, _) -> undefined end),
     meck:expect(config, get, fun(_, _, Default) -> Default end),
     ok.
@@ -165,7 +167,7 @@
     ],
     lists:map(
         fun({EJson, Expect, Msg}) ->
-            {Msg, ?_assertMatch(Expect, couch_doc:from_json_obj(EJson))}
+            {Msg, ?_assertMatch(Expect, couch_doc:from_json_obj_validate(EJson))}
         end,
         Cases).
 
@@ -230,16 +232,29 @@
             {[{<<"_something">>, 5}]},
             {doc_validation, <<"Bad special document member: _something">>},
             "Underscore prefix fields are reserved."
+        },
+        {
+            fun() ->
+                {[
+                    {<<"_id">>, <<"large_doc">>},
+                    {<<"x">> , << <<"x">> || _ <- lists:seq(1,1025) >>}
+                ]}
+            end,
+            {request_entity_too_large, <<"large_doc">>},
+            "Document too large."
         }
     ],
 
     lists:map(fun
+        ({Fun, Expect, Msg}) when is_function(Fun, 0) ->
+            Error = (catch couch_doc:from_json_obj_validate(Fun())),
+            {Msg, ?_assertMatch(Expect, Error)};
         ({EJson, Expect, Msg}) ->
-            Error = (catch couch_doc:from_json_obj(EJson)),
+            Error = (catch couch_doc:from_json_obj_validate(EJson)),
             {Msg, ?_assertMatch(Expect, Error)};
         ({EJson, Msg}) ->
             try
-                couch_doc:from_json_obj(EJson),
+                couch_doc:from_json_obj_validate(EJson),
                 {"Conversion failed to raise an exception", ?_assert(false)}
             catch
                 _:_ -> {Msg, ?_assert(true)}