Merge branch 'remove-R16B03-1-build' of github.com:cloudant/couchdb-config
diff --git a/src/config.erl b/src/config.erl
index 9449b1e..640d7c2 100644
--- a/src/config.erl
+++ b/src/config.erl
@@ -27,9 +27,11 @@
 -export([set/3, set/4, set/5]).
 -export([delete/2, delete/3, delete/4]).
 
--export([get_integer/3, set_integer/3]).
--export([get_float/3, set_float/3]).
--export([get_boolean/3, set_boolean/3]).
+-export([get_integer/3, set_integer/3, set_integer/4]).
+-export([get_float/3, set_float/3, set_float/4]).
+-export([get_boolean/3, set_boolean/3, set_boolean/4]).
+
+-export([features/0, enable_feature/1, disable_feature/1]).
 
 -export([listen_for_changes/2]).
 -export([subscribe_for_changes/1]).
@@ -38,6 +40,13 @@
 -export([init/1, terminate/2, code_change/3]).
 -export([handle_call/3, handle_cast/2, handle_info/2]).
 
+-define(FEATURES, "features").
+
+-define(TIMEOUT, 30000).
+-define(INVALID_SECTION, <<"Invalid configuration section">>).
+-define(INVALID_KEY, <<"Invalid configuration key">>).
+-define(INVALID_VALUE, <<"Invalid configuration value">>).
+
 -record(config, {
     notify_funs=[],
     ini_files=undefined,
@@ -53,7 +62,7 @@
 
 
 reload() ->
-    gen_server:call(?MODULE, reload).
+    gen_server:call(?MODULE, reload, ?TIMEOUT).
 
 all() ->
     lists:sort(gen_server:call(?MODULE, all, infinity)).
@@ -66,9 +75,12 @@
             Default
     end.
 
-set_integer(Section, Key, Value) when is_integer(Value) ->
-    set(Section, Key, integer_to_list(Value));
-set_integer(_, _, _) ->
+set_integer(Section, Key, Value) ->
+    set_integer(Section, Key, Value, true).
+
+set_integer(Section, Key, Value, Persist) when is_integer(Value) ->
+    set(Section, Key, integer_to_list(Value), Persist);
+set_integer(_, _, _, _) ->
     error(badarg).
 
 to_integer(List) when is_list(List) ->
@@ -86,9 +98,12 @@
             Default
     end.
 
-set_float(Section, Key, Value) when is_float(Value) ->
-    set(Section, Key, float_to_list(Value));
-set_float(_, _, _) ->
+set_float(Section, Key, Value) ->
+    set_float(Section, Key, Value, true).
+
+set_float(Section, Key, Value, Persist) when is_float(Value) ->
+    set(Section, Key, float_to_list(Value), Persist);
+set_float(_, _, _, _) ->
     error(badarg).
 
 to_float(List) when is_list(List) ->
@@ -108,11 +123,14 @@
             Default
     end.
 
-set_boolean(Section, Key, true) ->
-    set(Section, Key, "true");
-set_boolean(Section, Key, false) ->
-    set(Section, Key, "false");
-set_boolean(_, _, _) ->
+set_boolean(Section, Key, Value) ->
+    set_boolean(Section, Key, Value, true).
+
+set_boolean(Section, Key, true, Persist) ->
+    set(Section, Key, "true", Persist);
+set_boolean(Section, Key, false, Persist) ->
+    set(Section, Key, "false", Persist);
+set_boolean(_, _, _, _) ->
     error(badarg).
 
 to_boolean(List) when is_list(List) ->
@@ -162,7 +180,8 @@
     ?MODULE:set(binary_to_list(Sec), binary_to_list(Key), Val, Persist, Reason);
 set(Section, Key, Value, Persist, Reason)
         when is_list(Section), is_list(Key), is_list(Value) ->
-    gen_server:call(?MODULE, {set, Section, Key, Value, Persist, Reason});
+    gen_server:call(?MODULE, {set, Section, Key, Value, Persist, Reason},
+        ?TIMEOUT);
 set(_Sec, _Key, _Val, _Persist, _Reason) ->
     error(badarg).
 
@@ -180,7 +199,20 @@
 delete(Sec, Key, Persist, Reason) when is_binary(Sec) and is_binary(Key) ->
     delete(binary_to_list(Sec), binary_to_list(Key), Persist, Reason);
 delete(Section, Key, Persist, Reason) when is_list(Section), is_list(Key) ->
-    gen_server:call(?MODULE, {delete, Section, Key, Persist, Reason}).
+    gen_server:call(?MODULE, {delete, Section, Key, Persist, Reason},
+        ?TIMEOUT).
+
+
+features() ->
+    lists:usort([list_to_atom(Key) || {Key, "true"} <- ?MODULE:get(?FEATURES)]).
+
+
+enable_feature(Feature) when is_atom(Feature) ->
+    ?MODULE:set(?FEATURES, atom_to_list(Feature), "true", false).
+
+
+disable_feature(Feature) when is_atom(Feature) ->
+    ?MODULE:delete(?FEATURES, atom_to_list(Feature), false).
 
 
 listen_for_changes(CallbackModule, InitialState) ->
@@ -212,20 +244,28 @@
     Resp = lists:sort((ets:tab2list(?MODULE))),
     {reply, Resp, Config};
 handle_call({set, Sec, Key, Val, Persist, Reason}, _From, Config) ->
-    true = ets:insert(?MODULE, {{Sec, Key}, Val}),
-    couch_log:notice("~p: [~s] ~s set to ~s for reason ~p",
-        [?MODULE, Sec, Key, Val, Reason]),
-    case {Persist, Config#config.write_filename} of
-        {true, undefined} ->
-            ok;
-        {true, FileName} ->
-            config_writer:save_to_file({{Sec, Key}, Val}, FileName);
-        _ ->
-            ok
-    end,
-    Event = {config_change, Sec, Key, Val, Persist},
-    gen_event:sync_notify(config_event, Event),
-    {reply, ok, Config};
+    case validate_config_update(Sec, Key, Val) of
+        {error, ValidationError} ->
+            couch_log:error("~p: [~s] ~s = '~s' rejected for reason ~p",
+                             [?MODULE, Sec, Key, Val, Reason]),
+            {reply, {error, ValidationError}, Config};
+        ok ->
+            true = ets:insert(?MODULE, {{Sec, Key}, Val}),
+            couch_log:notice("~p: [~s] ~s set to ~s for reason ~p",
+                             [?MODULE, Sec, Key, Val, Reason]),
+            case {Persist, Config#config.write_filename} of
+                {true, undefined} ->
+                    ok;
+                {true, FileName} ->
+                    config_writer:save_to_file({{Sec, Key}, Val}, FileName);
+                _ ->
+                    ok
+            end,
+            Event = {config_change, Sec, Key, Val, Persist},
+            gen_event:sync_notify(config_event, Event),
+            {reply, ok, Config}
+    end;
+
 handle_call({delete, Sec, Key, Persist, Reason}, _From, Config) ->
     true = ets:delete(?MODULE, {Sec,Key}),
     couch_log:notice("~p: [~s] ~s deleted for reason ~p",
@@ -360,6 +400,33 @@
             ok
     end.
 
+
+validate_config_update(Sec, Key, Val) ->
+    %% See https://erlang.org/doc/man/re.html &
+    %% https://pcre.org/original/doc/html/pcrepattern.html
+    %%
+    %%  only characters that are actually screen-visible are allowed
+    %%  tabs and spaces are allowed
+    %%  no  [ ] explicitly to avoid section header bypass
+    {ok, Forbidden} = re:compile("[\]\[]+", [dollar_endonly, unicode]),
+    %% Values are permitted [ ] characters as we use these in
+    %% places like mochiweb socket option lists
+    %% Values may also be empty to delete manual configuration
+    {ok, Printable} = re:compile("^[[:graph:]\t\s]*$",
+                               [dollar_endonly, unicode]),
+    case {re:run(Sec, Printable),
+          re:run(Sec, Forbidden),
+          re:run(Key, Printable),
+          re:run(Key, Forbidden),
+          re:run(Val, Printable)} of
+        {{match, _}, nomatch, {match, _}, nomatch, {match, _}} -> ok;
+        {nomatch, _,    _, _, _} -> {error, ?INVALID_SECTION};
+        {_, {match, _}, _, _, _} -> {error, ?INVALID_SECTION};
+        {_, _, nomatch,    _, _} -> {error, ?INVALID_KEY};
+        {_, _, _, {match, _}, _} -> {error, ?INVALID_KEY};
+        {_, _, _, _, nomatch}    -> {error, ?INVALID_VALUE}
+    end.
+
 -ifdef(TEST).
 -include_lib("eunit/include/eunit.hrl").
 
@@ -381,4 +448,37 @@
     ?assertEqual(0.0, to_float("+0.0")),
     ok.
 
+validation_test() ->
+    ?assertEqual(ok, validate_config_update("section", "key", "value")),
+    ?assertEqual(ok, validate_config_update("delete", "empty_value", "")),
+    ?assertEqual({error, ?INVALID_SECTION},
+        validate_config_update("sect[ion", "key", "value")),
+    ?assertEqual({error, ?INVALID_SECTION},
+        validate_config_update("sect]ion", "key", "value")),
+    ?assertEqual({error, ?INVALID_SECTION},
+        validate_config_update("section\n", "key", "value")),
+    ?assertEqual({error, ?INVALID_SECTION},
+        validate_config_update("section\r", "key", "value")),
+    ?assertEqual({error, ?INVALID_SECTION},
+        validate_config_update("section\r\n", "key", "value")),
+    ?assertEqual({error, ?INVALID_KEY},
+        validate_config_update("section", "key\n", "value")),
+    ?assertEqual({error, ?INVALID_KEY},
+        validate_config_update("section", "key\r", "value")),
+    ?assertEqual({error, ?INVALID_KEY},
+        validate_config_update("section", "key\r\n", "value")),
+    ?assertEqual({error,?INVALID_VALUE},
+        validate_config_update("section", "key", "value\n")),
+    ?assertEqual({error,?INVALID_VALUE},
+        validate_config_update("section", "key", "value\r")),
+    ?assertEqual({error,?INVALID_VALUE},
+        validate_config_update("section", "key", "value\r\n")),
+    ?assertEqual({error,?INVALID_KEY},
+        validate_config_update("section", "k[ey", "value")),
+    ?assertEqual({error,?INVALID_KEY},
+        validate_config_update("section", "[key", "value")),
+    ?assertEqual({error,?INVALID_KEY},
+        validate_config_update("section", "key]", "value")),
+    ok.
+
 -endif.
diff --git a/src/config_listener.erl b/src/config_listener.erl
index 22ce366..756609b 100644
--- a/src/config_listener.erl
+++ b/src/config_listener.erl
@@ -19,17 +19,27 @@
 -export([start/2]).
 -export([start/3]).
 
--export([behaviour_info/1]).
-
 %% Required gen_event interface
 -export([init/1, handle_event/2, handle_call/2, handle_info/2, terminate/2,
     code_change/3]).
 
-behaviour_info(callbacks) ->
-    [{handle_config_change,5},
-     {handle_config_terminate, 3}];
-behaviour_info(_) ->
-    undefined.
+
+-callback handle_config_change(
+    Sec :: string(),
+    Key :: string(),
+    Value :: string(),
+    Persist :: boolean(),
+    State :: term()
+) ->
+    {ok, term()} | remove_handler.
+
+-callback handle_config_terminate(
+    Subscriber :: pid(),
+    Reason :: term(),
+    State :: term()
+) ->
+    term().
+
 
 start(Module, State) ->
     start(Module, Module, State).
diff --git a/test/config_tests.erl b/test/config_tests.erl
index 8293f2e..7a5b56d 100644
--- a/test/config_tests.erl
+++ b/test/config_tests.erl
@@ -50,8 +50,6 @@
         FileName
     end).
 
--define(DEPS, [couch_stats, couch_log, config]).
-
 
 -define(T(F), {erlang:fun_to_list(F), F}).
 -define(FEXT(F), fun(_, _) -> F() end).
@@ -68,8 +66,12 @@
     setup(Chain ++ [?CONFIG_FIXTURE_TEMP]);
 
 setup(Chain) ->
+    meck:new(couch_log),
+    meck:expect(couch_log, error, fun(_, _) -> ok end),
+    meck:expect(couch_log, notice, fun(_, _) -> ok end),
+    meck:expect(couch_log, debug, fun(_, _) -> ok end),
     ok = application:set_env(config, ini_files, Chain),
-    test_util:start_applications(?DEPS).
+    test_util:start_applications([config]).
 
 
 setup_empty() ->
@@ -77,26 +79,29 @@
 
 
 setup_config_listener() ->
-    setup(),
-    spawn_config_listener().
+    Apps = setup(),
+    Pid = spawn_config_listener(),
+    {Apps, Pid}.
 
 setup_config_notifier(Subscription) ->
-    setup(),
-    spawn_config_notifier(Subscription).
+    Apps = setup(),
+    Pid = spawn_config_notifier(Subscription),
+    {Apps, Pid}.
 
 
-teardown(Pid) when is_pid(Pid) ->
+teardown({Apps, Pid}) when is_pid(Pid) ->
     catch exit(Pid, kill),
-    teardown(undefined);
+    teardown(Apps);
 
-teardown(_) ->
-    [application:stop(App) || App <- ?DEPS].
+teardown(Apps) when is_list(Apps) ->
+    meck:unload(),
+    test_util:stop_applications(Apps).
 
-teardown(_, Pid) when is_pid(Pid) ->
+teardown(_, {Apps, Pid}) when is_pid(Pid) ->
     catch exit(Pid, kill),
-    teardown(undefined);
-teardown(_, _) ->
-    teardown(undefined).
+    teardown(Apps);
+teardown(_, Apps) ->
+    teardown(Apps).
 
 
 handle_config_change("remove_handler", _Key, _Value, _Persist, {_Pid, _State}) ->
@@ -128,8 +133,6 @@
             fun teardown/1,
             [
                 fun should_load_all_configs/0,
-                fun should_locate_daemons_section/0,
-                fun should_locate_mrview_handler/0,
                 fun should_return_undefined_atom_on_missed_section/0,
                 fun should_return_undefined_atom_on_missed_option/0,
                 fun should_return_custom_default_value_on_missed_option/0,
@@ -172,6 +175,22 @@
     }.
 
 
+config_features_test_() ->
+    {
+        "Config features tests",
+        {
+            foreach,
+            fun setup/0,
+            fun teardown/1,
+            [
+                fun should_enable_features/0,
+                fun should_disable_features/0
+            ]
+        }
+    }.
+
+
+
 config_override_test_() ->
     {
         "Configs overide tests",
@@ -282,15 +301,6 @@
     ?assert(length(config:all()) > 0).
 
 
-should_locate_daemons_section() ->
-    ?assert(length(config:get("daemons")) > 0).
-
-
-should_locate_mrview_handler() ->
-    Expect = "{couch_mrview_http, handle_view_req}",
-    ?assertEqual(Expect, config:get("httpd_design_handlers", "_view")).
-
-
 should_return_undefined_atom_on_missed_section() ->
     ?assertEqual(undefined, config:get("foo", "bar")).
 
@@ -431,14 +441,14 @@
     end).
 
 
-should_handle_value_change(Pid) ->
+should_handle_value_change({_Apps, Pid}) ->
     ?_test(begin
         ?assertEqual(ok, config:set("httpd", "port", "80", false)),
         ?assertMatch({{"httpd", "port", "80", false}, _}, getmsg(Pid))
     end).
 
 
-should_pass_correct_state_to_handle_config_change(Pid) ->
+should_pass_correct_state_to_handle_config_change({_Apps, Pid}) ->
     ?_test(begin
         ?assertEqual(ok, config:set("update_state", "foo", "any", false)),
         ?assertMatch({_, undefined}, getmsg(Pid)),
@@ -447,7 +457,7 @@
     end).
 
 
-should_pass_correct_state_to_handle_config_terminate(Pid) ->
+should_pass_correct_state_to_handle_config_terminate({_Apps, Pid}) ->
     ?_test(begin
         ?assertEqual(ok, config:set("update_state", "foo", "any", false)),
         ?assertMatch({_, undefined}, getmsg(Pid)),
@@ -458,14 +468,14 @@
     end).
 
 
-should_pass_subscriber_pid_to_handle_config_terminate(Pid) ->
+should_pass_subscriber_pid_to_handle_config_terminate({_Apps, Pid}) ->
     ?_test(begin
         ?assertEqual(ok, config:set("remove_handler", "any", "any", false)),
         ?assertEqual({Pid, remove_handler, undefined}, getmsg(Pid))
     end).
 
 
-should_not_call_handle_config_after_related_process_death(Pid) ->
+should_not_call_handle_config_after_related_process_death({_Apps, Pid}) ->
     ?_test(begin
         ?assertEqual(ok, config:set("remove_handler", "any", "any", false)),
         ?assertEqual({Pid, remove_handler, undefined}, getmsg(Pid)),
@@ -478,7 +488,7 @@
     end).
 
 
-should_remove_handler_when_requested(Pid) ->
+should_remove_handler_when_requested({_Apps, Pid}) ->
     ?_test(begin
         ?assertEqual(1, n_handlers()),
         ?assertEqual(ok, config:set("remove_handler", "any", "any", false)),
@@ -487,7 +497,7 @@
     end).
 
 
-should_remove_handler_when_pid_exits(Pid) ->
+should_remove_handler_when_pid_exits({_Apps, Pid}) ->
     ?_test(begin
         ?assertEqual(1, n_handlers()),
 
@@ -516,7 +526,7 @@
     end).
 
 
-should_stop_monitor_on_error(Pid) ->
+should_stop_monitor_on_error({_Apps, Pid}) ->
     ?_test(begin
         ?assertEqual(1, n_handlers()),
 
@@ -542,27 +552,27 @@
         ?assertEqual(0, n_handlers())
     end).
 
-should_notify(Subscription, Pid) ->
+should_notify(Subscription, {_Apps, Pid}) ->
     {to_string(Subscription), ?_test(begin
         ?assertEqual(ok, config:set("section_foo", "key_bar", "any", false)),
         ?assertEqual({config_change,"section_foo", "key_bar", "any", false}, getmsg(Pid)),
         ok
     end)}.
 
-should_not_notify([{Section, _}] = Subscription, Pid) ->
+should_not_notify([{Section, _}] = Subscription, {_Apps, Pid}) ->
     {to_string(Subscription), ?_test(begin
         ?assertEqual(ok, config:set(Section, "any", "any", false)),
         ?assertError({timeout, config_msg}, getmsg(Pid)),
         ok
     end)};
-should_not_notify(Subscription, Pid) ->
+should_not_notify(Subscription, {_Apps, Pid}) ->
     {to_string(Subscription), ?_test(begin
         ?assertEqual(ok, config:set("any", "any", "any", false)),
         ?assertError({timeout, config_msg}, getmsg(Pid)),
         ok
     end)}.
 
-should_unsubscribe_when_subscriber_gone(_Subscription, Pid) ->
+should_unsubscribe_when_subscriber_gone(_Subscription, {_Apps, Pid}) ->
     ?_test(begin
         ?assertEqual(1, n_notifiers()),
 
@@ -601,6 +611,32 @@
     end).
 
 
+should_enable_features() ->
+    ?assertEqual([], config:features()),
+
+    ?assertEqual(ok, config:enable_feature(snek)),
+    ?assertEqual([snek], config:features()),
+
+    ?assertEqual(ok, config:enable_feature(snek)),
+    ?assertEqual([snek], config:features()),
+
+    ?assertEqual(ok, config:enable_feature(dogo)),
+    ?assertEqual([dogo, snek], config:features()).
+
+
+should_disable_features() ->
+    ?assertEqual([], config:features()),
+
+    config:enable_feature(snek),
+    ?assertEqual([snek], config:features()),
+
+    ?assertEqual(ok, config:disable_feature(snek)),
+    ?assertEqual([], config:features()),
+
+    ?assertEqual(ok, config:disable_feature(snek)),
+    ?assertEqual([], config:features()).
+
+
 spawn_config_listener() ->
     Self = self(),
     Pid = erlang:spawn(fun() ->