% 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_misc_handlers).

-export([
    handle_welcome_req/2,
    handle_favicon_req/2,
    handle_utils_dir_req/2,
    handle_all_dbs_req/1,
    handle_uuids_req/1,
    handle_config_req/1,
    handle_task_status_req/1,
    handle_file_req/2
]).

-include_lib("couch/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,
        last_chunk/1,
        end_json_response/1,
        start_chunked_response/3,
        send_error/4
    ]
).

% httpd global handlers

handle_welcome_req(#httpd{method = 'GET'} = Req, WelcomeMessage) ->
    send_json(Req, {
        [
            {couchdb, WelcomeMessage},
            {uuid, couch_server:get_uuid()},
            {version, list_to_binary(couch_server:get_version())}
        ] ++
            case config:get("vendor") of
                [] ->
                    [];
                Properties ->
                    [{vendor, {[{?l2b(K), ?l2b(V)} || {K, V} <- Properties]}}]
            end
    });
handle_welcome_req(Req, _) ->
    send_method_not_allowed(Req, "GET,HEAD").

handle_favicon_req(#httpd{method = 'GET'} = Req, DocumentRoot) ->
    {{Year, Month, Day}, Time} = erlang:universaltime(),
    OneYearFromNow = {{Year + 1, Month, Day}, Time},
    CachingHeaders = [
        %favicon should expire a year from now
        {"Cache-Control", "public, max-age=31536000"},
        {"Expires", couch_util:rfc1123_date(OneYearFromNow)}
    ],
    couch_httpd:serve_file(Req, "favicon.ico", DocumentRoot, CachingHeaders);
handle_favicon_req(Req, _) ->
    send_method_not_allowed(Req, "GET,HEAD").

handle_file_req(#httpd{method = 'GET'} = Req, Document) ->
    couch_httpd:serve_file(Req, filename:basename(Document), filename:dirname(Document));
handle_file_req(Req, _) ->
    send_method_not_allowed(Req, "GET,HEAD").

handle_utils_dir_req(Req, _) ->
    send_error(
        Req,
        410,
        <<"no_node_local_fauxton">>,
        ?l2b("The web interface is no longer available on the node-local port.")
    ).

handle_all_dbs_req(#httpd{method = 'GET'} = Req) ->
    {ok, DbNames} = couch_server:all_databases(),
    send_json(Req, DbNames);
handle_all_dbs_req(Req) ->
    send_method_not_allowed(Req, "GET,HEAD").

handle_task_status_req(#httpd{method = 'GET'} = Req) ->
    ok = couch_httpd:verify_is_server_admin(Req),
    % convert the list of prop lists to a list of json objects
    send_json(Req, [{Props} || Props <- lists:sort(couch_task_status:all())]);
handle_task_status_req(Req) ->
    send_method_not_allowed(Req, "GET,HEAD").

handle_uuids_req(#httpd{method = 'GET'} = Req) ->
    Max = config:get_integer("uuids", "max_count", 1000),
    Count =
        try list_to_integer(couch_httpd:qs_value(Req, "count", "1")) of
            N when N > Max ->
                throw({bad_request, <<"count parameter too large">>});
            N when N < 0 ->
                throw({bad_request, <<"count must be a positive integer">>});
            N ->
                N
        catch
            error:badarg ->
                throw({bad_request, <<"count must be a positive integer">>})
        end,
    UUIDs = [couch_uuids:new() || _ <- lists:seq(1, Count)],
    Etag = couch_httpd:make_etag(UUIDs),
    couch_httpd:etag_respond(Req, Etag, fun() ->
        CacheBustingHeaders = [
            {"Date", couch_util:rfc1123_date()},
            {"Cache-Control", "no-cache"},
            % Past date, ON PURPOSE!
            {"Expires", "Mon, 01 Jan 1990 00:00:00 GMT"},
            {"Pragma", "no-cache"},
            {"ETag", Etag}
        ],
        send_json(Req, 200, CacheBustingHeaders, {[{<<"uuids">>, UUIDs}]})
    end);
handle_uuids_req(Req) ->
    send_method_not_allowed(Req, "GET").

% Config request handler

% GET /_config/
% GET /_config
handle_config_req(#httpd{method = 'GET', path_parts = [_]} = Req) ->
    ok = couch_httpd:verify_is_server_admin(Req),
    Grouped = lists:foldl(
        fun({{Section, Key}, Value}, Acc) ->
            case dict:is_key(Section, Acc) of
                true ->
                    dict:append(Section, {list_to_binary(Key), list_to_binary(Value)}, Acc);
                false ->
                    dict:store(Section, [{list_to_binary(Key), list_to_binary(Value)}], Acc)
            end
        end,
        dict:new(),
        config:all()
    ),
    KVs = dict:fold(
        fun(Section, Values, Acc) ->
            [{list_to_binary(Section), {Values}} | Acc]
        end,
        [],
        Grouped
    ),
    send_json(Req, 200, {KVs});
% GET /_config/Section
handle_config_req(#httpd{method = 'GET', path_parts = [_, Section]} = Req) ->
    ok = couch_httpd:verify_is_server_admin(Req),
    KVs = [
        {list_to_binary(Key), list_to_binary(Value)}
     || {Key, Value} <- config:get(Section)
    ],
    send_json(Req, 200, {KVs});
% GET /_config/Section/Key
handle_config_req(#httpd{method = 'GET', path_parts = [_, Section, Key]} = Req) ->
    ok = couch_httpd:verify_is_server_admin(Req),
    case config:get(Section, Key, undefined) of
        undefined ->
            throw({not_found, unknown_config_value});
        Value ->
            send_json(Req, 200, list_to_binary(Value))
    end;
% POST /_config/_reload - Flushes unpersisted config values from RAM
handle_config_req(#httpd{method = 'POST', path_parts = [_, <<"_reload">>]} = Req) ->
    couch_httpd:validate_ctype(Req, "application/json"),
    _ = couch_httpd:body(Req),
    ok = couch_httpd:verify_is_server_admin(Req),
    ok = config:reload(),
    send_json(Req, 200, {[{ok, true}]});
% PUT or DELETE /_config/Section/Key
handle_config_req(#httpd{method = Method, path_parts = [_, Section, Key]} = Req) when
    (Method == 'PUT') or (Method == 'DELETE')
->
    ok = couch_httpd:verify_is_server_admin(Req),
    couch_util:check_config_blacklist(Section),
    Persist = couch_httpd:header_value(Req, "X-Couch-Persist") /= "false",
    case chttpd_util:get_chttpd_config("config_whitelist") of
        undefined ->
            % No whitelist; allow all changes.
            handle_approved_config_req(Req, Persist);
        WhitelistValue ->
            % Provide a failsafe to protect against inadvertently locking
            % onesself out of the config by supplying a syntactically-incorrect
            % Erlang term. To intentionally lock down the whitelist, supply a
            % well-formed list which does not include the whitelist config
            % variable itself.
            FallbackWhitelist = [{<<"chttpd">>, <<"config_whitelist">>}],

            Whitelist =
                case couch_util:parse_term(WhitelistValue) of
                    {ok, Value} when is_list(Value) ->
                        Value;
                    {ok, _NonListValue} ->
                        FallbackWhitelist;
                    {error, _} ->
                        [{WhitelistSection, WhitelistKey}] = FallbackWhitelist,
                        couch_log:error(
                            "Only whitelisting ~s/~s due to error"
                            " parsing: ~p",
                            [
                                WhitelistSection,
                                WhitelistKey,
                                WhitelistValue
                            ]
                        ),
                        FallbackWhitelist
                end,

            IsRequestedKeyVal = fun(Element) ->
                case Element of
                    {A, B} ->
                        % For readability, tuples may be used instead of binaries
                        % in the whitelist.
                        case {couch_util:to_binary(A), couch_util:to_binary(B)} of
                            {Section, Key} ->
                                true;
                            {Section, <<"*">>} ->
                                true;
                            _Else ->
                                false
                        end;
                    _Else ->
                        false
                end
            end,

            case lists:any(IsRequestedKeyVal, Whitelist) of
                true ->
                    % Allow modifying this whitelisted variable.
                    handle_approved_config_req(Req, Persist);
                _NotWhitelisted ->
                    % Disallow modifying this non-whitelisted variable.
                    send_error(
                        Req,
                        400,
                        <<"modification_not_allowed">>,
                        ?l2b("This config variable is read-only")
                    )
            end
    end;
handle_config_req(Req) ->
    send_method_not_allowed(Req, "GET,PUT,POST,DELETE").

% PUT /_config/Section/Key
% "value"
handle_approved_config_req(Req, Persist) ->
    Query = couch_httpd:qs(Req),
    UseRawValue =
        case lists:keyfind("raw", 1, Query) of
            % Not specified
            false -> false;
            % Specified with no value, i.e. "?raw" and "?raw="
            {"raw", ""} -> false;
            {"raw", "false"} -> false;
            {"raw", "true"} -> true;
            {"raw", InvalidValue} -> InvalidValue
        end,
    handle_approved_config_req(Req, Persist, UseRawValue).

handle_approved_config_req(
    #httpd{method = 'PUT', path_parts = [_, Section, Key]} = Req,
    Persist,
    UseRawValue
) when
    UseRawValue =:= false orelse UseRawValue =:= true
->
    RawValue = couch_httpd:json_body(Req),
    Value =
        case UseRawValue of
            true ->
                % Client requests no change to the provided value.
                RawValue;
            false ->
                % Pre-process the value as necessary.
                case Section of
                    <<"admins">> ->
                        couch_passwords:hash_admin_password(RawValue);
                    _ ->
                        couch_util:trim(RawValue)
                end
        end,
    OldValue = config:get(Section, Key, ""),
    case config:set(Section, Key, ?b2l(Value), Persist) of
        ok ->
            send_json(Req, 200, list_to_binary(OldValue));
        Error ->
            throw(Error)
    end;
handle_approved_config_req(#httpd{method = 'PUT'} = Req, _Persist, UseRawValue) ->
    Err = io_lib:format("Bad value for 'raw' option: ~s", [UseRawValue]),
    send_json(Req, 400, {[{error, ?l2b(Err)}]});
% DELETE /_config/Section/Key
handle_approved_config_req(
    #httpd{method = 'DELETE', path_parts = [_, Section, Key]} = Req,
    Persist,
    _UseRawValue
) ->
    case config:get(Section, Key, undefined) of
        undefined ->
            throw({not_found, unknown_config_value});
        OldValue ->
            config:delete(Section, Key, Persist),
            send_json(Req, 200, list_to_binary(OldValue))
    end.
