%%%----------------------------------------------------------------------
%%% File    : ejabberd_xmlrpc.erl
%%% Author  : Badlop <badlop@process-one.net>
%%% Purpose : XML-RPC server that frontends ejabberd commands
%%% Created : 21 Aug 2007 by Badlop <badlop@process-one.net>
%%%
%%%
%%% ejabberd, Copyright (C) 2002-2025   ProcessOne
%%%
%%% This program is free software; you can redistribute it and/or
%%% modify it under the terms of the GNU General Public License as
%%% published by the Free Software Foundation; either version 2 of the
%%% License, or (at your option) any later version.
%%%
%%% This program is distributed in the hope that it will be useful,
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
%%% General Public License for more details.
%%%
%%% You should have received a copy of the GNU General Public License along
%%% with this program; if not, write to the Free Software Foundation, Inc.,
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
%%%
%%%----------------------------------------------------------------------

%%% TODO: Remove support for plaintext password

%%% TODO: commands strings should be strings without ~n

-module(ejabberd_xmlrpc).
-behaviour(ejabberd_listener).

-author('badlop@process-one.net').

-export([start/3, start_link/3, handler/2, process/2, accept/1,
	 listen_options/0]).

-include("logger.hrl").
-include("ejabberd_http.hrl").
-include("mod_roster.hrl").

-include_lib("xmpp/include/xmpp.hrl").

-record(state,
	{auth = noauth        :: noauth | map(),
         get_auth = true      :: boolean(),
	 ip                   :: inet:ip_address()}).

%% -----------------------------
%% Listener interface
%% -----------------------------

start(SockMod, Socket, Opts) ->
    Opts1 = [{request_handlers, [{[], ?MODULE}]}|Opts],
    ejabberd_http:start(SockMod, Socket, Opts1).

start_link(SockMod, Socket, Opts) ->
    Opts1 = [{request_handlers, [{[], ?MODULE}]}|Opts],
    ejabberd_http:start_link(SockMod, Socket, Opts1).

accept(Pid) ->
    ejabberd_http:accept(Pid).

%% -----------------------------
%% HTTP interface
%% -----------------------------

process(_, #request{method = 'POST', data = Data, ip = {IP, _}}) ->
    GetAuth = true,
    State = #state{get_auth = GetAuth, ip = IP},
    case fxml_stream:parse_element(Data) of
	{error, _} ->
	    {400, [],
	     #xmlel{name = <<"h1">>, attrs = [],
		    children = [{xmlcdata, <<"Malformed XML">>}]}};
	El ->
	    case fxmlrpc:decode(El) of
		{error, _} = Err ->
		    ?ERROR_MSG("XML-RPC request ~ts failed with reason: ~p",
			       [Data, Err]),
		    {400, [],
		     #xmlel{name = <<"h1">>, attrs = [],
			    children = [{xmlcdata, <<"Malformed Request">>}]}};
		{ok, RPC} ->
		    ?DEBUG("Got XML-RPC request: ~p", [RPC]),
		    {false, Result} = handler(State, RPC),
		    XML = fxml:element_to_binary(fxmlrpc:encode(Result)),
		    {200, [{<<"Content-Type">>, <<"text/xml">>}],
		     <<"<?xml version=\"1.0\"?>", XML/binary>>}
	    end
    end;
process(_, _) ->
    {400, [],
     #xmlel{name = <<"h1">>, attrs = [],
	    children = [{xmlcdata, <<"400 Bad Request">>}]}}.

%% -----------------------------
%% Access verification
%% -----------------------------

-spec extract_auth([{user | server | token | password, binary()}]) ->
			  map() | {error, not_found | expired | invalid_auth}.
extract_auth(AuthList) ->
    ?DEBUG("AUTHLIST ~p", [AuthList]),
    try get_attrs([user, server, token], AuthList) of
        [U0, S0, T] ->
	    U = jid:nodeprep(U0),
	    S = jid:nameprep(S0),
	    case ejabberd_oauth:check_token(T) of
		{ok, {U, S}, Scope} ->
		    #{usr => {U, S, <<"">>}, oauth_scope => Scope, caller_server => S};
		{false, Reason} ->
		    {error, Reason};
		_ ->
		    {error, not_found}
	    end
    catch
	exit:{attribute_not_found, _, _} ->
            try get_attrs([user, server, password], AuthList) of
                [U0, S0, P] ->
		    U = jid:nodeprep(U0),
		    S = jid:nameprep(S0),
		    case ejabberd_auth:check_password(U, <<"">>, S, P) of
			true ->
			    #{usr => {U, S, <<"">>}, caller_server => S};
			false ->
			    {error, invalid_auth}
		    end
            catch
		exit:{attribute_not_found, Attr, _} ->
		    throw({error, missing_auth_arguments, Attr})
            end
    end.

%% -----------------------------
%% Handlers
%% -----------------------------

handler(#state{get_auth = true, auth = noauth, ip = IP} = State,
	{call, Method,
	 [{struct, AuthList} | Arguments] = AllArgs}) ->
    try extract_auth(AuthList) of
	{error, invalid_auth} ->
	    build_fault_response(-118,
				 "Invalid authentication data",
				 []);
	{error, not_found} ->
	    build_fault_response(-118,
				 "Invalid oauth token",
				 []);
	{error, expired} ->
	    build_fault_response(-118,
				 "Invalid oauth token",
				 []);
        Auth ->
            handler(State#state{get_auth = false, auth = Auth#{ip => IP, caller_module => ?MODULE}},
                    {call, Method, Arguments})
    catch
      {error, missing_auth_arguments, _Attr} ->
	  handler(State#state{get_auth = false,
			      auth = #{ip => IP, caller_module => ?MODULE}},
		  {call, Method, AllArgs})
    end;

%% .............................
%%  Debug

handler(_State, {call, echothis, [A]}) ->
    {false, {response, [A]}};

handler(_State,
	{call, echothisnew, [{struct, [{sentence, A}]}]}) ->
    {false, {response, [{struct, [{repeated, A}]}]}};

handler(_State,
	{call, multhis, [{struct, [{a, A}, {b, B}]}]}) ->
    {false, {response, [A * B]}};

handler(_State,
	{call, multhisnew, [{struct, [{a, A}, {b, B}]}]}) ->
    {false, {response, [{struct, [{mu, A * B}]}]}};

%% .............................
%% ejabberd commands

handler(State, {call, Command, []}) ->
    handler(State, {call, Command, [{struct, []}]});
handler(State,
	{call, Command, [{struct, AttrL}]}) ->
    {ArgsF, ArgsR, ResultF} = ejabberd_commands:get_command_format(Command, State#state.auth),
    try_do_command(State#state.auth, Command, AttrL, ArgsF, ArgsR, ResultF);
handler(_State, Payload) ->
    build_fault_response(-112, "Unknown call: ~p",
			 [Payload]).

%% -----------------------------
%% Command
%% -----------------------------

try_do_command(Auth, Command, AttrL, ArgsF, ArgsR, ResultF) ->
    try do_command(Auth, Command, AttrL, ArgsF, ArgsR, ResultF)
    of
      {command_result, ResultFormatted} ->
	  {false, {response, [ResultFormatted]}}
    catch
      exit:{duplicated_attribute, ExitAt, ExitAtL} ->
	  build_fault_response(-114,
			       "Attribute '~p' duplicated:~n~p",
			       [ExitAt, ExitAtL]);
      exit:{attribute_not_found, ExitAt, ExitAtL} ->
	  build_fault_response(-116,
			       "Required attribute '~p' not found:~n~p",
			       [ExitAt, ExitAtL]);
      exit:{additional_unused_args, ExitAtL} ->
	  build_fault_response(-120,
			       "The call provided additional unused "
				 "arguments:~n~p",
			       [ExitAtL]);
      exit:{invalid_arg_type, Arg, Type} ->
	  build_fault_response(-122,
			       "Parameter '~p' can't be coerced to type '~p'",
			       [Arg, Type]);
      Why ->
	  build_fault_response(-118,
			       "A problem '~p' occurred executing the "
				 "command ~p with arguments~n~p",
			       [Why, Command, AttrL])
    end.

build_fault_response(Code, ParseString, ParseArgs) ->
    FaultString = "Error " ++ integer_to_list(Code) ++ "\n"
        ++ lists:flatten(io_lib:format(ParseString, ParseArgs)),
    ?WARNING_MSG(FaultString, []),
    {false, {response, {fault, Code, list_to_binary(FaultString)}}}.

do_command(Auth, Command, AttrL, ArgsF, ArgsR,
	   ResultF) ->
    ArgsFormatted = format_args(rename_old_args(AttrL, ArgsR), ArgsF),
    Result = ejabberd_commands:execute_command2(Command, ArgsFormatted, Auth),
    ResultFormatted = format_result(Result, ResultF),
    {command_result, {struct, [ResultFormatted]}}.

rename_old_args(Args, []) ->
    Args;
rename_old_args(Args, [{OldName, NewName} | ArgsR]) ->
    Args2 = case lists:keytake(OldName, 1, Args) of
	{value, {OldName, Value}, ArgsTail} ->
	    [{NewName, Value} | ArgsTail];
	false ->
	    Args
    end,
    rename_old_args(Args2, ArgsR).

%%-----------------------------
%% Format arguments
%%-----------------------------

get_attrs(Attribute_names, L) ->
    [get_attr(A, L) || A <- Attribute_names].

get_attr(A, L) ->
    case lists:keysearch(A, 1, L) of
      {value, {A, Value}} -> Value;
      false ->
	  exit({attribute_not_found, A, L})
    end.

get_elem_delete(A, L) ->
    case proplists:get_all_values(A, L) of
      [Value] -> {Value, proplists:delete(A, L)};
      [_, _ | _] ->
	  exit({duplicated_attribute, A, L});
      [] ->
	  exit({attribute_not_found, A, L})
    end.

format_args(Args, ArgsFormat) ->
    {ArgsRemaining, R} = lists:foldl(fun ({ArgName,
					   ArgFormat},
					  {Args1, Res}) ->
					     {ArgValue, Args2} =
						 get_elem_delete(ArgName,
								 Args1),
					     Formatted = format_arg(ArgValue,
								    ArgFormat),
					     {Args2, Res ++ [Formatted]}
				     end,
				     {Args, []}, ArgsFormat),
    case ArgsRemaining of
      [] -> R;
      L when is_list(L) -> exit({additional_unused_args, L})
    end.

format_arg({array, Elements},
	   {list, {_ElementDefName, ElementDefFormat}})
    when is_list(Elements) ->
    lists:map(fun (ElementValue) ->
		      format_arg(ElementValue, ElementDefFormat)
	      end,
	      Elements);

format_arg({array, Elements},
	   {list, {ElementDefName, ElementDefFormat}})
    when is_list(Elements) ->
    lists:map(fun ({struct, [{ElementName, ElementValue}]}) when
                        ElementDefName == ElementName ->
		      format_arg(ElementValue, ElementDefFormat)
	      end,
	      Elements);
format_arg({array, [{struct, Elements}]},
	   {list, {ElementDefName, ElementDefFormat}})
    when is_list(Elements) ->
    lists:map(fun ({ElementName, ElementValue}) ->
		      true = ElementDefName == ElementName,
		      format_arg(ElementValue, ElementDefFormat)
	      end,
	      Elements);
%% Old ejabberd 23.10
format_arg({array, [{struct, Elements}]},
	   {tuple, ElementsDef})
    when is_list(Elements) ->
    FormattedList = format_args(Elements, ElementsDef),
    list_to_tuple(FormattedList);
%% New ejabberd 24.02
format_arg({struct, Elements},
	   {tuple, ElementsDef})
    when is_list(Elements) ->
    FormattedList = format_args(Elements, ElementsDef),
    list_to_tuple(FormattedList);
format_arg({array, Elements}, {list, ElementsDef})
    when is_list(Elements) and is_atom(ElementsDef) ->
    [format_arg(Element, ElementsDef)
     || Element <- Elements];
format_arg(Arg, integer) when is_integer(Arg) -> Arg;
format_arg(Arg, binary) when is_list(Arg) -> process_unicode_codepoints(Arg);
format_arg(Arg, binary) when is_binary(Arg) -> Arg;
format_arg(Arg, string) when is_list(Arg) -> Arg;
format_arg(Arg, string) when is_binary(Arg) -> binary_to_list(Arg);
format_arg(undefined, binary) -> <<>>;
format_arg(undefined, string) -> "";
format_arg(Arg, Format) ->
    ?ERROR_MSG("Don't know how to format Arg ~p for format ~p", [Arg, Format]),
    exit({invalid_arg_type, Arg, Format}).

process_unicode_codepoints(Str) ->
    iolist_to_binary(lists:map(fun(X) when X > 255 -> unicode:characters_to_binary([X]);
                                  (Y) -> Y
                               end, Str)).

%% -----------------------------
%% Result
%% -----------------------------

format_result(Code, {Name, rescode}) ->
    {Name, make_status(Code)};
format_result({_Code, Text}, {_Name, restuple}) ->
    {text, io_lib:format("~s", [Text])};
format_result({error, Error}, _) when is_list(Error) ->
    throw({error, lists:flatten(Error)});
format_result({error, Error}, _) ->
    throw({error, Error});
format_result({error, _Type, _Code, Error}, _) when is_list(Error) ->
    throw({error, lists:flatten(Error)});
format_result({error, _Type, _Code, Error}, _) ->
    throw({error, Error});
format_result(String, string) -> lists:flatten(String);
format_result(Atom, {Name, atom}) ->
     {Name, iolist_to_binary(atom_to_list(Atom))};
format_result(Int, {Name, integer}) ->
    {Name, Int};
format_result([A|_]=String, {Name, string}) when is_list(String) and is_integer(A) ->
    {Name, lists:flatten(String)};
format_result(Binary, {Name, string}) when is_binary(Binary) ->
    {Name, binary_to_list(Binary)};
format_result(Atom, {Name, string}) when is_atom(Atom) ->
    {Name, atom_to_list(Atom)};
format_result(Integer, {Name, string}) when is_integer(Integer) ->
    {Name, integer_to_list(Integer)};
format_result(Other, {Name, string}) ->
    {Name, io_lib:format("~p", [Other])};
format_result(String, {Name, binary}) when is_list(String) ->
    {Name, lists:flatten(String)};
format_result(Binary, {Name, binary}) when is_binary(Binary) ->
    {Name, binary_to_list(Binary)};


format_result(Els, {Name, {list, Def}}) ->
    FormattedList = [element(2, format_result(El, Def)) || El <- Els],
    {Name, {array, FormattedList}};


format_result(Tuple,
	      {Name, {tuple, Def}}) ->
    Els = lists:zip(tuple_to_list(Tuple), Def),
    FormattedList = [format_result(El, ElDef) || {El, ElDef} <- Els],
    {Name, {struct, FormattedList}};

format_result(404, {Name, _}) ->
    {struct, [{Name, make_status(not_found)}]}.

make_status(ok) -> 0;
make_status(true) -> 0;
make_status(false) -> 1;
make_status(error) -> 1;
make_status(_) -> 1.

listen_options() ->
    ?WARNING_MSG("It is deprecated defining ejabberd_xmlrpc as a listen module "
                 "in the ejabberd configuration. Support for that configuration"
                 " method may be removed in a future ejabberd release. You are "
                 "encouraged to define ejabberd_xmlrpc inside request_handlers "
                 "option of ejabberd_http listen module. See the ejabberd "
                 "documentation for details: _`/admin/configuration/listen/#ejabberd-xmlrpc|ejabberd_xmlrpc listener`_.", []),
    [].
