diff options
| author | Pouriya <pouriya.jahanbakhsh@gmail.com> | 2022-01-21 03:14:28 +0330 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-01-21 03:14:28 +0330 | 
| commit | 03485f5e940326a9fdbcf2ecd1918b2772003a6a (patch) | |
| tree | 5ce253cdd24e11f0f2bd90babbbf0bc38e38c575 /src | |
| parent | Merge branch 'processone:master' into master (diff) | |
| parent | mod_pubsub: Allow for limiting item_expire value (diff) | |
Merge branch 'processone:master' into master
Diffstat (limited to 'src')
40 files changed, 1412 insertions, 495 deletions
| diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl index 0174cd7ff..9e72c7b36 100644 --- a/src/ejabberd_admin.erl +++ b/src/ejabberd_admin.erl @@ -120,7 +120,10 @@ get_commands_spec() ->  			module = init, function = restart,  			args = [], result = {res, rescode}},       #ejabberd_commands{name = reopen_log, tags = [logs], -			desc = "Reopen the log files", +			desc = "Reopen the log files after being renamed", +			longdesc = "This can be useful when an external tool is " +			"used for log rotation. See " +			"https://docs.ejabberd.im/admin/guide/troubleshooting/#log-files",  			policy = admin,  			module = ?MODULE, function = reopen_log,  			args = [], result = {res, rescode}}, @@ -178,6 +181,8 @@ get_commands_spec() ->  			result = {res, restuple}},       #ejabberd_commands{name = unregister, tags = [accounts],  			desc = "Unregister a user", +			longdesc = "This deletes the authentication and all the " +                        "data associated to the account (roster, vcard...).",                          policy = admin,  			module = ?MODULE, function = unregister,  			args_desc = ["Username", "Local vhost served by ejabberd"], @@ -345,31 +350,41 @@ get_commands_spec() ->  				{oldbackup, string}, {newbackup, string}],  			result = {res, restuple}},       #ejabberd_commands{name = backup, tags = [mnesia], -			desc = "Store the database to backup file", +			desc = "Backup the Mnesia database to a binary file",  			module = ?MODULE, function = backup_mnesia,  			args_desc = ["Full path for the destination backup file"],  			args_example = ["/var/lib/ejabberd/database.backup"],  			args = [{file, string}], result = {res, restuple}},       #ejabberd_commands{name = restore, tags = [mnesia], -			desc = "Restore the database from backup file", +			desc = "Restore the Mnesia database from a binary backup file", +			longdesc = "This restores immediately from a " +			"binary backup file the internal Mnesia " +			"database. This will consume a lot of memory if " +			"you have a large database, you may prefer " +			"'install_fallback'.",  			module = ?MODULE, function = restore_mnesia,  			args_desc = ["Full path to the backup file"],  			args_example = ["/var/lib/ejabberd/database.backup"],  			args = [{file, string}], result = {res, restuple}},       #ejabberd_commands{name = dump, tags = [mnesia], -			desc = "Dump the database to a text file", +			desc = "Dump the Mnesia database to a text file",  			module = ?MODULE, function = dump_mnesia,  			args_desc = ["Full path for the text file"],  			args_example = ["/var/lib/ejabberd/database.txt"],  			args = [{file, string}], result = {res, restuple}},       #ejabberd_commands{name = dump_table, tags = [mnesia], -			desc = "Dump a table to a text file", +			desc = "Dump a Mnesia table to a text file",  			module = ?MODULE, function = dump_table,  			args_desc = ["Full path for the text file", "Table name"],  			args_example = ["/var/lib/ejabberd/table-muc-registered.txt", "muc_registered"],  			args = [{file, string}, {table, string}], result = {res, restuple}},       #ejabberd_commands{name = load, tags = [mnesia], -			desc = "Restore the database from a text file", +			desc = "Restore Mnesia database from a text dump file", +			longdesc = "Restore immediately. This is not " +			"recommended for big databases, as it will " +			"consume much time, memory and processor. In " +			"that case it's preferable to use 'backup' and " +			"'install_fallback'.",  			module = ?MODULE, function = load_mnesia,  			args_desc = ["Full path to the text file"],  			args_example = ["/var/lib/ejabberd/database.txt"], @@ -385,7 +400,14 @@ get_commands_spec() ->  			args_example = ["roster"],  			args = [{table, string}], result = {res, string}},       #ejabberd_commands{name = install_fallback, tags = [mnesia], -			desc = "Install the database from a fallback file", +			desc = "Install Mnesia database from a binary backup file", +			longdesc = "The binary backup file is " +			"installed as fallback: it will be used to " +			"restore the database at the next ejabberd " +			"start. This means that, after running this " +			"command, you have to restart ejabberd. This " +			"command requires less memory than +			'restore'.",  			module = ?MODULE, function = install_fallback_mnesia,  			args_desc = ["Full path to the fallback file"],  			args_example = ["/var/lib/ejabberd/database.fallback"], diff --git a/src/ejabberd_auth_sql.erl b/src/ejabberd_auth_sql.erl index 1f7106c59..50cc1902e 100644 --- a/src/ejabberd_auth_sql.erl +++ b/src/ejabberd_auth_sql.erl @@ -299,6 +299,20 @@ export(_Server) ->                    ["username=%(LUser)s",                     "server_host=%(LServer)s",                     "password=%(Password)s"])]; +         (Host, {passwd, {LUser, LServer}, +                         {scram, StoredKey1, ServerKey, Salt, IterationCount}}) +            when LServer == Host -> +              Hash = sha, +              StoredKey = scram_hash_encode(Hash, StoredKey1), +              [?SQL("delete from users where username=%(LUser)s and %(LServer)H;"), +               ?SQL_INSERT( +                  "users", +                  ["username=%(LUser)s", +                   "server_host=%(LServer)s", +                   "password=%(StoredKey)s", +                   "serverkey=%(ServerKey)s", +                   "salt=%(Salt)s", +                   "iterationcount=%(IterationCount)d"])];           (Host, #passwd{us = {LUser, LServer}, password = #scram{} = Scram})              when LServer == Host ->  	      StoredKey = scram_hash_encode(Scram#scram.hash, Scram#scram.storedkey), diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl index c00b1469a..ddf0d3c59 100644 --- a/src/ejabberd_commands.erl +++ b/src/ejabberd_commands.erl @@ -94,6 +94,7 @@ get_commands_spec() ->                             result_example = ok},          #ejabberd_commands{name = gen_markdown_doc_for_tags, tags = [documentation],                             desc = "Generates markdown documentation for ejabberd_commands", +                           note = "added in 21.12",                             module = ejabberd_commands_doc, function = generate_tags_md,                             args = [{file, binary}],                             result = {res, rescode}, diff --git a/src/ejabberd_ctl.erl b/src/ejabberd_ctl.erl index 04e383d53..77595cd54 100644 --- a/src/ejabberd_ctl.erl +++ b/src/ejabberd_ctl.erl @@ -378,7 +378,11 @@ format_arg("", string) ->  format_arg(Arg, string) ->      NumChars = integer_to_list(length(Arg)),      Parse = "~" ++ NumChars ++ "c", -    format_arg2(Arg, Parse). +    format_arg2(Arg, Parse); +format_arg(Arg, Format) -> +    S = unicode:characters_to_binary(Arg, utf8), +    JSON = jiffy:decode(S), +    mod_http_api:format_arg(JSON, Format).  format_arg2(Arg, Parse)->      {ok, [Arg2], _RemainingArguments} = io_lib:fread(Parse, Arg), @@ -525,6 +529,7 @@ print_usage(Version) ->  print_usage(HelpMode, MaxC, ShCode, Version) ->      AllCommands =  	[ +	 {"help", ["[arguments]"], "Get help"},  	 {"status", [], "Get ejabberd status"},  	 {"stop", [], "Stop ejabberd"},  	 {"restart", [], "Restart ejabberd"}, diff --git a/src/ejabberd_piefxis.erl b/src/ejabberd_piefxis.erl index 8dff06837..d62efb300 100644 --- a/src/ejabberd_piefxis.erl +++ b/src/ejabberd_piefxis.erl @@ -24,17 +24,15 @@  %%%----------------------------------------------------------------------  %%% Not implemented: +%%% - PEP nodes export/import +%%% - message archives export/import  %%% - write mod_piefxis with ejabberdctl commands -%%% - Export from mod_offline_sql.erl -%%% - Export from mod_private_sql.erl -%%% - XEP-227: 6. Security Considerations  %%% - Other schemas of XInclude are not tested, and may not be imported correctly.  %%% - If a host has many users, split that host in XML files with 50 users each. -%%%% Headers  -module(ejabberd_piefxis). --protocol({xep, 227, '1.0'}). +-protocol({xep, 227, '1.1'}).  -export([import_file/1, export_server/1, export_host/2]). @@ -166,33 +164,66 @@ export_users([], _Server, _Fd) ->  export_user(User, Server, Fd) ->      Password = ejabberd_auth:get_password_s(User, Server),      LServer = jid:nameprep(Server), -    Pass = case ejabberd_auth:password_format(LServer) of -	       scram -> format_scram_password(Password); -	       _ -> Password +    {PassPlain, PassScram} = case ejabberd_auth:password_format(LServer) of +	       scram -> {[], [format_scram_password(Password)]}; +	       _ -> {[{<<"password">>, Password}], []}  	   end, -    Els = get_offline(User, Server) ++ +    Els = +        PassScram ++ +        get_offline(User, Server) ++          get_vcard(User, Server) ++          get_privacy(User, Server) ++          get_roster(User, Server) ++          get_private(User, Server),      print(Fd, fxml:element_to_binary(                  #xmlel{name = <<"user">>, -                       attrs = [{<<"name">>, User}, -                                {<<"password">>, Pass}], +                       attrs = [{<<"name">>, User} | PassPlain],                         children = Els})).  format_scram_password(#scram{hash = Hash, storedkey = StoredKey, serverkey = ServerKey,  			     salt = Salt, iterationcount = IterationCount}) -> -  StoredKeyB64 = base64:encode(StoredKey), -  ServerKeyB64 = base64:encode(ServerKey), -  SaltB64 = base64:encode(Salt), -  IterationCountBin = (integer_to_binary(IterationCount)), -  Hash2 = case Hash of -              sha -> <<>>; -              sha256 -> <<"sha256,">>; -              sha512 -> <<"sha512,">> -          end, -  <<"scram:", Hash2/binary, StoredKeyB64/binary, ",", ServerKeyB64/binary, ",", SaltB64/binary, ",", IterationCountBin/binary>>. +    StoredKeyB64 = base64:encode(StoredKey), +    ServerKeyB64 = base64:encode(ServerKey), +    SaltB64 = base64:encode(Salt), +    IterationCountBin = (integer_to_binary(IterationCount)), +    MechanismB = case Hash of +                     sha -> <<"SCRAM-SHA-1">>; +                     sha256 -> <<"SCRAM-SHA-256">>; +                     sha512 -> <<"SCRAM-SHA-512">> +                 end, +    Children = +        [ +         #xmlel{name = <<"iter-count">>, +                children = [{xmlcdata, IterationCountBin}]}, +         #xmlel{name = <<"salt">>, +                children = [{xmlcdata, SaltB64}]}, +         #xmlel{name = <<"server-key">>, +                children = [{xmlcdata, ServerKeyB64}]}, +         #xmlel{name = <<"stored-key">>, +                children = [{xmlcdata, StoredKeyB64}]} +        ], +    #xmlel{name = <<"scram-credentials">>, +           attrs = [{<<"xmlns">>, <<?NS_PIE/binary, "#scram">>}, +                    {<<"mechanism">>, MechanismB}], +           children = Children}. + +parse_scram_password(#xmlel{attrs = Attrs} = El) -> +    Hash = case fxml:get_attr_s(<<"mechanism">>, Attrs) of +               <<"SCRAM-SHA-1">> -> sha; +               <<"SCRAM-SHA-256">> -> sha256; +               <<"SCRAM-SHA-512">> -> sha512 +           end, +    StoredKeyB64 = fxml:get_path_s(El, [{elem, <<"stored-key">>}, cdata]), +    ServerKeyB64 = fxml:get_path_s(El, [{elem, <<"server-key">>}, cdata]), +    IterationCountBin = fxml:get_path_s(El, [{elem, <<"iter-count">>}, cdata]), +    SaltB64 = fxml:get_path_s(El, [{elem, <<"salt">>}, cdata]), +    #scram{ +       storedkey = base64:decode(StoredKeyB64), +       serverkey = base64:decode(ServerKeyB64), +       salt      = base64:decode(SaltB64), +       hash      = Hash, +       iterationcount = (binary_to_integer(IterationCountBin)) +      };  parse_scram_password(PassData) ->    Split = binary:split(PassData, <<",">>, [global]), @@ -214,26 +245,30 @@ parse_scram_password(PassData) ->  get_vcard(User, Server) ->      LUser = jid:nodeprep(User),      LServer = jid:nameprep(Server), -    case mod_vcard:get_vcard(LUser, LServer) of +    try mod_vcard:get_vcard(LUser, LServer) of  	error -> [];  	Els -> Els +    catch +        error:{module_not_loaded, _, _} -> []      end.  -spec get_offline(binary(), binary()) -> [xmlel()].  get_offline(User, Server) ->      LUser = jid:nodeprep(User),      LServer = jid:nameprep(Server), -    case mod_offline:get_offline_els(LUser, LServer) of +    try mod_offline:get_offline_els(LUser, LServer) of          [] ->              [];          Els ->              NewEls = lists:map(fun xmpp:encode/1, Els),              [#xmlel{name = <<"offline-messages">>, children = NewEls}] +    catch +        error:{module_not_loaded, _, _} -> []      end.  -spec get_privacy(binary(), binary()) -> [xmlel()].  get_privacy(User, Server) -> -    case mod_privacy:get_user_lists(User, Server) of +    try mod_privacy:get_user_lists(User, Server) of          {ok, #privacy{default = Default,                        lists = [_|_] = Lists}} ->              XLists = lists:map( @@ -246,12 +281,14 @@ get_privacy(User, Server) ->  	    [xmpp:encode(#privacy_query{default = Default, lists = XLists})];          _ ->              [] +    catch +        error:{module_not_loaded, _, _} -> []      end.  -spec get_roster(binary(), binary()) -> [xmlel()].  get_roster(User, Server) ->      JID = jid:make(User, Server), -    case mod_roster:get_roster(User, Server) of +    try mod_roster:get_roster(User, Server) of          [_|_] = Items ->              Subs =                  lists:flatmap( @@ -278,15 +315,19 @@ get_roster(User, Server) ->  	    [xmpp:encode(#roster_query{items = Rs}) | Subs];          _ ->              [] +    catch +        error:{module_not_loaded, _, _} -> []      end.  -spec get_private(binary(), binary()) -> [xmlel()].  get_private(User, Server) -> -    case mod_private:get_data(User, Server) of +    try mod_private:get_data(User, Server) of          [_|_] = Els ->  	    [xmpp:encode(#private{sub_els = Els})];          _ ->              [] +    catch +        error:{module_not_loaded, _, _} -> []      end.  process(#state{xml_stream_state = XMLStreamState, fd = Fd} = State) -> @@ -398,21 +439,10 @@ process_users([_|Els], State) ->  process_users([], State) ->      {ok, State}. -process_user(#xmlel{name = <<"user">>, attrs = Attrs, children = Els}, +process_user(#xmlel{name = <<"user">>, attrs = Attrs, children = Els} = El,               #state{server = LServer} = State) ->      Name = fxml:get_attr_s(<<"name">>, Attrs), -    Password = fxml:get_attr_s(<<"password">>, Attrs), -    PasswordFormat = ejabberd_auth:password_format(LServer), -    Pass = case PasswordFormat of -      scram -> -        case Password of -          <<"scram:", PassData/binary>> -> -            parse_scram_password(PassData); -          P -> P -        end; -      _ -> Password -    end, - +    Pass = process_password(El, LServer),      case jid:nodeprep(Name) of          error ->              stop("Invalid 'user': ~ts", [Name]); @@ -420,13 +450,29 @@ process_user(#xmlel{name = <<"user">>, attrs = Attrs, children = Els},              case ejabberd_auth:try_register(LUser, LServer, Pass) of                  ok ->                      process_user_els(Els, State#state{user = LUser}); -                {error, invalid_password} when (Password == <<>>) -> +                {error, invalid_password} when (Pass == <<>>) ->                      process_user_els(Els, State#state{user = LUser});                  {error, Err} ->                      stop("Failed to create user '~ts': ~p", [Name, Err])              end      end. +process_password(#xmlel{name = <<"user">>, attrs = Attrs} = El, LServer) -> +    {PassPlain, PassOldScram} = case fxml:get_attr_s(<<"password">>, Attrs) of +          <<"scram:", PassData/binary>> -> {<<"">>, PassData}; +          P -> {P, false} +    end, +    ScramCred = fxml:get_subtag(El, <<"scram-credentials">>), +    PasswordFormat = ejabberd_auth:password_format(LServer), +    case {PassPlain, PassOldScram, ScramCred, PasswordFormat} of +        {PassPlain, false, false, plain} -> PassPlain; +        {<<"">>, false, ScramCred, plain} -> parse_scram_password(ScramCred); +        {<<"">>, PassOldScram, false, plain} -> parse_scram_password(PassOldScram); +        {PassPlain, false, false, scram} -> PassPlain; +        {<<"">>, false, ScramCred, scram} -> parse_scram_password(ScramCred); +        {<<"">>, PassOldScram, false, scram} -> parse_scram_password(PassOldScram) +    end. +  process_user_els([#xmlel{} = El|Els], State) ->      case process_user_el(El, State) of          {ok, NewState} -> diff --git a/src/ejabberd_s2s.erl b/src/ejabberd_s2s.erl index 8057c9a35..04490071c 100644 --- a/src/ejabberd_s2s.erl +++ b/src/ejabberd_s2s.erl @@ -33,8 +33,8 @@  %% API  -export([start_link/0, stop/0, route/1, have_connection/1, -	 get_connections_pids/1, try_register/1, -	 remove_connection/2, start_connection/2, start_connection/3, +	 get_connections_pids/1, +	 start_connection/2, start_connection/3,  	 dirty_get_connections/0, allow_host/2,  	 incoming_s2s_number/0, outgoing_s2s_number/0,  	 stop_s2s_connections/0, @@ -64,7 +64,7 @@  %% once a server is temporary blocked, it stay blocked for 60 seconds --record(s2s, {fromto :: {binary(), binary()}, +-record(s2s, {fromto :: {binary(), binary()} | '_',                pid    :: pid()}).  -record(state, {}). @@ -112,24 +112,6 @@ is_temporarly_blocked(Host) ->  	    end      end. --spec remove_connection({binary(), binary()}, pid()) -> ok. -remove_connection({From, To} = FromTo, Pid) -> -    case mnesia:dirty_match_object(s2s, #s2s{fromto = FromTo, pid = Pid}) of -	[#s2s{pid = Pid}] -> -	    F = fun() -> -			mnesia:delete_object(#s2s{fromto = FromTo, pid = Pid}) -		end, -	    case mnesia:transaction(F) of -		{atomic, _} -> ok; -		{aborted, Reason} -> -		    ?ERROR_MSG("Failed to unregister s2s connection ~ts -> ~ts: " -			       "Mnesia failure: ~p", -			       [From, To, Reason]) -	    end; -	_ -> -	    ok -    end. -  -spec have_connection({binary(), binary()}) -> boolean().  have_connection(FromTo) ->      case catch mnesia:dirty_read(s2s, FromTo) of @@ -148,31 +130,6 @@ get_connections_pids(FromTo) ->  	    []      end. --spec try_register({binary(), binary()}) -> boolean(). -try_register({From, To} = FromTo) -> -    MaxS2SConnectionsNumber = max_s2s_connections_number(FromTo), -    MaxS2SConnectionsNumberPerNode = -	max_s2s_connections_number_per_node(FromTo), -    F = fun () -> -		L = mnesia:read({s2s, FromTo}), -		NeededConnections = needed_connections_number(L, -							      MaxS2SConnectionsNumber, -							      MaxS2SConnectionsNumberPerNode), -		if NeededConnections > 0 -> -			mnesia:write(#s2s{fromto = FromTo, pid = self()}), -			true; -		   true -> false -		end -	end, -    case mnesia:transaction(F) of -	{atomic, Res} -> Res; -	{aborted, Reason} -> -	    ?ERROR_MSG("Failed to register s2s connection ~ts -> ~ts: " -		       "Mnesia failure: ~p", -		       [From, To, Reason]), -	    false -    end. -  -spec dirty_get_connections() -> [{binary(), binary()}].  dirty_get_connections() ->      mnesia:dirty_all_keys(s2s). @@ -269,6 +226,8 @@ init([]) ->  	    {stop, Reason}      end. +handle_call({new_connection, Args}, _From, State) -> +    {reply, erlang:apply(fun new_connection_int/7, Args), State};  handle_call(Request, From, State) ->      ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),      {noreply, State}. @@ -289,6 +248,21 @@ handle_info({route, Packet}, State) ->  			misc:format_exception(2, Class, Reason, StackTrace)])      end,      {noreply, State}; +handle_info({'DOWN', _Ref, process, Pid, _Reason}, State) -> +    case mnesia:dirty_match_object(s2s, #s2s{fromto = '_', pid = Pid}) of +	[#s2s{pid = Pid, fromto = {From, To}} = Obj] -> +	    F = fun() -> mnesia:delete_object(Obj) end, +	    case mnesia:transaction(F) of +		{atomic, _} -> ok; +		{aborted, Reason} -> +		    ?ERROR_MSG("Failed to unregister s2s connection for pid ~p (~ts -> ~ts):" +			       "Mnesia failure: ~p", +			       [Pid, From, To, Reason]) +	    end, +	    {noreply, State}; +	_ -> +	    {noreply, State} +    end;  handle_info(Info, State) ->      ?WARNING_MSG("Unexpected info: ~p", [Info]),      {noreply, State}. @@ -458,6 +432,18 @@ open_several_connections(N, MyServer, Server, From,  		     integer(), integer(), [proplists:property()]) -> [pid()].  new_connection(MyServer, Server, From, FromTo,  	       MaxS2SConnectionsNumber, MaxS2SConnectionsNumberPerNode, Opts) -> +    case whereis(ejabberd_s2s) == self() of +	true -> +	    new_connection_int(MyServer, Server, From, FromTo, +			       MaxS2SConnectionsNumber, MaxS2SConnectionsNumberPerNode, Opts); +	false -> +	    gen_server:call(ejabberd_s2s, {new_connection, [MyServer, Server, From, FromTo, +							    MaxS2SConnectionsNumber, +							    MaxS2SConnectionsNumberPerNode, Opts]}) +    end. + +new_connection_int(MyServer, Server, From, FromTo, +	       MaxS2SConnectionsNumber, MaxS2SConnectionsNumberPerNode, Opts) ->      {ok, Pid} = ejabberd_s2s_out:start(MyServer, Server, Opts),      F = fun() ->  		L = mnesia:read({s2s, FromTo}), @@ -474,6 +460,7 @@ new_connection(MyServer, Server, From, FromTo,      case TRes of  	{atomic, Pid1} ->  	    if Pid1 == Pid -> +		    erlang:monitor(process, Pid),  		    ejabberd_s2s_out:connect(Pid);  	       true ->  		    ejabberd_s2s_out:stop_async(Pid) diff --git a/src/ejabberd_s2s_out.erl b/src/ejabberd_s2s_out.erl index d58396533..f057705ed 100644 --- a/src/ejabberd_s2s_out.erl +++ b/src/ejabberd_s2s_out.erl @@ -318,7 +318,6 @@ handle_info(Info, #{server_host := ServerHost} = State) ->  terminate(Reason, #{server := LServer,  		    remote_server := RServer} = State) -> -    ejabberd_s2s:remove_connection({LServer, RServer}, self()),      State1 = case Reason of  		 normal -> State;  		 _ -> State#{stop_reason => internal_failure} @@ -351,21 +350,12 @@ bounce_queue(State) ->        end, State).  -spec bounce_message_queue({binary(), binary()}, state()) -> state(). -bounce_message_queue({LServer, RServer} = FromTo, State) -> -    Pids = ejabberd_s2s:get_connections_pids(FromTo), -    case lists:member(self(), Pids) of -	true -> -	    ?WARNING_MSG("Outgoing s2s connection ~ts -> ~ts is supposed " -			 "to be unregistered, but pid ~p still presents " -			 "in 's2s' table", [LServer, RServer, self()]), -	    State; -	false -> -	    receive {route, Pkt} -> -		    State1 = bounce_packet(Pkt, State), -		    bounce_message_queue(FromTo, State1) -	    after 0 -> -		    State -	    end +bounce_message_queue(FromTo, State) -> +    receive {route, Pkt} -> +	    State1 = bounce_packet(Pkt, State), +	    bounce_message_queue(FromTo, State1) +    after 0 -> +	    State      end.  -spec bounce_packet(xmpp_element(), state()) -> state(). diff --git a/src/ejabberd_sql_pt.erl b/src/ejabberd_sql_pt.erl index 130228fe1..d131570f7 100644 --- a/src/ejabberd_sql_pt.erl +++ b/src/ejabberd_sql_pt.erl @@ -564,15 +564,23 @@ make_sql_upsert(Table, ParseRes, Pos) ->                             []                     end,      erl_syntax:fun_expr( -        [erl_syntax:clause( -            [erl_syntax:atom(pgsql), erl_syntax:variable("__Version")], -            [erl_syntax:infix_expr( -                erl_syntax:variable("__Version"), -                erl_syntax:operator('>='), -                erl_syntax:integer(90100))], -            [make_sql_upsert_pgsql901(Table, ParseRes), -             erl_syntax:atom(ok)])] ++ -            MySqlReplace ++ +      [erl_syntax:clause( +         [erl_syntax:atom(pgsql), erl_syntax:variable("__Version")], +         [erl_syntax:infix_expr( +            erl_syntax:variable("__Version"), +            erl_syntax:operator('>='), +            erl_syntax:integer(90500))], +         [make_sql_upsert_pgsql905(Table, ParseRes), +          erl_syntax:atom(ok)]), +       erl_syntax:clause( +         [erl_syntax:atom(pgsql), erl_syntax:variable("__Version")], +         [erl_syntax:infix_expr( +            erl_syntax:variable("__Version"), +            erl_syntax:operator('>='), +            erl_syntax:integer(90100))], +         [make_sql_upsert_pgsql901(Table, ParseRes), +          erl_syntax:atom(ok)])] ++ +          MySqlReplace ++              [erl_syntax:clause(                  [erl_syntax:underscore(), erl_syntax:underscore()],                  none, @@ -713,6 +721,57 @@ make_sql_upsert_pgsql901(Table, ParseRes0) ->        erl_syntax:atom(sql_query_t),        [Upsert]). +make_sql_upsert_pgsql905(Table, ParseRes0) -> +    ParseRes = lists:map( +        fun({"family", A2, A3}) -> {"\"family\"", A2, A3}; +           (Other) -> Other +        end, ParseRes0), +    Vals = +        lists:map( +          fun({_Field, _, ST}) -> +                  ST +          end, ParseRes), +    Fields = +        lists:map( +          fun({Field, _, _ST}) -> +                  #state{'query' = [{str, Field}]} +          end, ParseRes), +    SPairs = +        lists:flatmap( +          fun({_Field, key, _ST}) -> +                  []; +             ({_Field, {false}, _ST}) -> +                  []; +             ({Field, {true}, ST}) -> +                  [ST#state{ +                     'query' = [{str, Field}, {str, "="}] ++ ST#state.'query' +                    }] +          end, ParseRes), +    Set = join_states(SPairs, ", "), +    KeyFields = +        lists:flatmap( +          fun({Field, key, _ST}) -> +                  [#state{'query' = [{str, Field}]}]; +             ({_Field, _, _ST}) -> +                  [] +          end, ParseRes), +    State = +        concat_states( +          [#state{'query' = [{str, "INSERT INTO "}, {str, Table}, {str, "("}]}, +           join_states(Fields, ", "), +           #state{'query' = [{str, ") VALUES ("}]}, +           join_states(Vals, ", "), +           #state{'query' = [{str, ") ON CONFLICT ("}]}, +           join_states(KeyFields, ", "), +           #state{'query' = [{str, ") DO UPDATE SET "}]}, +           Set +          ]), +    Upsert = make_sql_query(State), +    erl_syntax:application( +      erl_syntax:atom(ejabberd_sql), +      erl_syntax:atom(sql_query_t), +      [Upsert]). +  check_upsert(ParseRes, Pos) ->      Set = diff --git a/src/ejd2sql.erl b/src/ejd2sql.erl index ad0cc5e88..427e13087 100644 --- a/src/ejd2sql.erl +++ b/src/ejd2sql.erl @@ -73,11 +73,16 @@ export(Server, Output) ->        end, Modules),      close_output(Output, IO). -export(Server, Output, Module1) -> -    Module = case Module1 of -		 mod_pubsub -> pubsub_db; -		 _ -> Module1 -	     end, +export(Server, Output, mod_mam = M1) -> +    MucServices = gen_mod:get_module_opt_hosts(Server, mod_muc), +    [export2(MucService, Output, M1, M1) || MucService <- MucServices], +    export2(Server, Output, M1, M1); +export(Server, Output, mod_pubsub = M1) -> +    export2(Server, Output, M1, pubsub_db); +export(Server, Output, M1) -> +    export2(Server, Output, M1, M1). + +export2(Server, Output, Module1, Module) ->      SQLMod = gen_mod:db_mod(sql, Module),      LServer = jid:nameprep(iolist_to_binary(Server)),      IO = prepare_output(Output), diff --git a/src/gen_pubsub_node.erl b/src/gen_pubsub_node.erl index 625e490fc..3f83fe48f 100644 --- a/src/gen_pubsub_node.erl +++ b/src/gen_pubsub_node.erl @@ -133,6 +133,10 @@      {result, {[itemId()], [itemId()]}  	}. +-callback remove_expired_items(NodeIdx :: nodeIdx(), +	Seconds :: infinity | non_neg_integer()) -> +    {result, [itemId()]}. +  -callback get_node_affiliations(NodeIdx :: nodeIdx()) ->      {result, [{ljid(), affiliation()}]}. diff --git a/src/mod_admin_update_sql.erl b/src/mod_admin_update_sql.erl index 4e932fe83..02beb4bf8 100644 --- a/src/mod_admin_update_sql.erl +++ b/src/mod_admin_update_sql.erl @@ -140,6 +140,7 @@ update_tables(State) ->      add_sh_column(State, "sr_group"),      add_pkey(State, "sr_group", ["server_host", "name"]), +    create_unique_index(State, "sr_group", "i_sr_group_sh_name", ["server_host", "name"]),      drop_sh_default(State, "sr_group"),      add_sh_column(State, "sr_user"), @@ -147,6 +148,7 @@ update_tables(State) ->      drop_index(State, "i_sr_user_jid"),      drop_index(State, "i_sr_user_grp"),      add_pkey(State, "sr_user", ["server_host", "jid", "grp"]), +    create_unique_index(State, "sr_user", "i_sr_user_sh_jid_grp", ["server_host", "jid", "grp"]),      create_index(State, "sr_user", "i_sr_user_sh_jid", ["server_host", "jid"]),      create_index(State, "sr_user", "i_sr_user_sh_grp", ["server_host", "grp"]),      drop_sh_default(State, "sr_user"), diff --git a/src/mod_caps.erl b/src/mod_caps.erl index c8f548169..bc48dac6f 100644 --- a/src/mod_caps.erl +++ b/src/mod_caps.erl @@ -49,7 +49,8 @@  	 handle_cast/2, terminate/2, code_change/3]).  -export([user_send_packet/1, user_receive_packet/1, -	 c2s_presence_in/2, mod_opt_type/1, mod_options/1, mod_doc/0]). +	 c2s_presence_in/2, c2s_copy_session/2, +	 mod_opt_type/1, mod_options/1, mod_doc/0]).  -include("logger.hrl"). @@ -274,6 +275,13 @@ c2s_presence_in(C2SState,  	    C2SState#{caps_resources => NewRs}      end. +-spec c2s_copy_session(ejabberd_c2s:state(), ejabberd_c2s:state()) +      -> ejabberd_c2s:state(). +c2s_copy_session(C2SState, #{caps_resources := Rs}) -> +    C2SState#{caps_resources => Rs}; +c2s_copy_session(C2SState, _) -> +    C2SState. +  -spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}].  depends(_Host, _Opts) ->      []. @@ -304,6 +312,8 @@ init([Host|_]) ->  		       caps_stream_features, 75),      ejabberd_hooks:add(s2s_in_post_auth_features, Host, ?MODULE,  		       caps_stream_features, 75), +    ejabberd_hooks:add(c2s_copy_session, Host, ?MODULE, +		       c2s_copy_session, 75),      ejabberd_hooks:add(disco_local_features, Host, ?MODULE,  		       disco_features, 75),      ejabberd_hooks:add(disco_local_identity, Host, ?MODULE, @@ -341,6 +351,8 @@ terminate(_Reason, State) ->  			  ?MODULE, caps_stream_features, 75),      ejabberd_hooks:delete(s2s_in_post_auth_features, Host,  			  ?MODULE, caps_stream_features, 75), +    ejabberd_hooks:delete(c2s_copy_session, Host, ?MODULE, +			  c2s_copy_session, 75),      ejabberd_hooks:delete(disco_local_features, Host,  			  ?MODULE, disco_features, 75),      ejabberd_hooks:delete(disco_local_identity, Host, diff --git a/src/mod_conversejs.erl b/src/mod_conversejs.erl new file mode 100644 index 000000000..8683d60ab --- /dev/null +++ b/src/mod_conversejs.erl @@ -0,0 +1,157 @@ +%%%---------------------------------------------------------------------- +%%% File    : mod_conversejs.erl +%%% Author  : Alexey Shchepin <alexey@process-one.net> +%%% Purpose : Serve simple page for Converse.js XMPP web browser client +%%% Created :  8 Nov 2021 by Alexey Shchepin <alexey@process-one.net> +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2021   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. +%%% +%%%---------------------------------------------------------------------- + +-module(mod_conversejs). + +-author('alexey@process-one.net'). + +-behaviour(gen_mod). + +-export([start/2, stop/1, reload/3, process/2, depends/2, +         mod_opt_type/1, mod_options/1, mod_doc/0]). + +-include_lib("xmpp/include/xmpp.hrl"). +-include("logger.hrl"). +-include("ejabberd_http.hrl"). +-include("translate.hrl"). +-include("ejabberd_web_admin.hrl"). + +start(_Host, _Opts) -> +    ok. + +stop(_Host) -> +    ok. + +reload(_Host, _NewOpts, _OldOpts) -> +    ok. + +depends(_Host, _Opts) -> +    []. + +process([], #request{method = 'GET'}) -> +    Host = ejabberd_config:get_myname(), +    Domain = gen_mod:get_module_opt(Host, ?MODULE, default_domain), +    Script = gen_mod:get_module_opt(Host, ?MODULE, conversejs_script), +    CSS = gen_mod:get_module_opt(Host, ?MODULE, conversejs_css), +    Init = [{<<"discover_connection_methods">>, false}, +            {<<"jid">>, Domain}, +            {<<"default_domain">>, Domain}, +            {<<"domain_placeholder">>, Domain}, +            {<<"view_mode">>, <<"fullscreen">>}], +    Init2 = +        case gen_mod:get_module_opt(Host, ?MODULE, websocket_url) of +            undefined -> Init; +            WSURL -> [{<<"websocket_url">>, WSURL} | Init] +        end, +    Init3 = +        case gen_mod:get_module_opt(Host, ?MODULE, bosh_service_url) of +            undefined -> Init2; +            BoshURL -> [{<<"bosh_service_url">>, BoshURL} | Init2] +        end, +    {200, [html], +     [<<"<!DOCTYPE html>">>, +      <<"<html>">>, +      <<"<head>">>, +      <<"<meta charset='utf-8'>">>, +      <<"<link rel='stylesheet' type='text/css' media='screen' href='">>, +      fxml:crypt(CSS), <<"'>">>, +      <<"<script src='">>, fxml:crypt(Script), <<"' charset='utf-8'></script>">>, +      <<"</head>">>, +      <<"<body>">>, +      <<"<script>">>, +      <<"converse.initialize(">>, jiffy:encode({Init3}), <<");">>, +      <<"</script>">>, +      <<"</body>">>, +      <<"</html>">>]}; +process(_, _) -> +    ejabberd_web:error(not_found). + +mod_opt_type(bosh_service_url) -> +    econf:either(undefined, econf:binary()); +mod_opt_type(websocket_url) -> +    econf:either(undefined, econf:binary()); +mod_opt_type(conversejs_script) -> +    econf:binary(); +mod_opt_type(conversejs_css) -> +    econf:binary(); +mod_opt_type(default_domain) -> +    econf:binary(). + +mod_options(_) -> +    [{bosh_service_url, undefined}, +     {websocket_url, undefined}, +     {default_domain, ejabberd_config:get_myname()}, +     {conversejs_script, <<"https://cdn.conversejs.org/dist/converse.min.js">>}, +     {conversejs_css, <<"https://cdn.conversejs.org/dist/converse.min.css">>}]. + +mod_doc() -> +    #{desc => +          [?T("This module serves a simple page for the " +              "https://conversejs.org/[Converse] XMPP web browser client."), "", +           ?T("This module is available since ejabberd 21.12."), "", +           ?T("To use this module, in addition to adding it to the 'modules' " +              "section, you must also enable it in 'listen' -> 'ejabberd_http' -> " +              "http://../listen-options/#request-handlers[request_handlers]."), "", +           ?T("You must also setup either the option 'websocket_url' or 'bosh_service_url'."), "", +           ?T("By default, the options 'conversejs_css' and 'conversejs_script'" +              " point to the public Converse.js client. Alternatively, you can" +              " host the client locally using _`mod_http_fileserver`_.") +          ], +     example => +         ["listen:", +          "  -", +          "    port: 5280", +          "    module: ejabberd_http", +          "    request_handlers:", +          "      /websocket: ejabberd_http_ws", +          "      /conversejs: mod_conversejs", +          "", +          "modules:", +          "  mod_conversejs:", +          "    websocket_url: \"ws://example.org:5280/websocket\""], +      opts => +          [{websocket_url, +            #{value => ?T("WebsocketURL"), +              desc => +                  ?T("A websocket URL to which Converse.js can connect to.")}}, +           {bosh_service_url, +            #{value => ?T("BoshURL"), +              desc => +                  ?T("BOSH service URL to which Converse.js can connect to.")}}, +           {default_domain, +            #{value => ?T("Domain"), +              desc => +                  ?T("Specify a domain to act as the default for user JIDs. " +                     "The default value is the first domain defined in the " +                     "ejabberd configuration file.")}}, +           {conversejs_script, +            #{value => ?T("URL"), +              desc => +                  ?T("Converse.js main script URL.")}}, +           {conversejs_css, +            #{value => ?T("URL"), +              desc => +                  ?T("Converse.js CSS URL.")}}] +     }. diff --git a/src/mod_conversejs_opt.erl b/src/mod_conversejs_opt.erl new file mode 100644 index 000000000..9e53978ea --- /dev/null +++ b/src/mod_conversejs_opt.erl @@ -0,0 +1,41 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_conversejs_opt). + +-export([bosh_service_url/1]). +-export([conversejs_css/1]). +-export([conversejs_script/1]). +-export([default_domain/1]). +-export([websocket_url/1]). + +-spec bosh_service_url(gen_mod:opts() | global | binary()) -> 'undefined' | binary(). +bosh_service_url(Opts) when is_map(Opts) -> +    gen_mod:get_opt(bosh_service_url, Opts); +bosh_service_url(Host) -> +    gen_mod:get_module_opt(Host, mod_conversejs, bosh_service_url). + +-spec conversejs_css(gen_mod:opts() | global | binary()) -> binary(). +conversejs_css(Opts) when is_map(Opts) -> +    gen_mod:get_opt(conversejs_css, Opts); +conversejs_css(Host) -> +    gen_mod:get_module_opt(Host, mod_conversejs, conversejs_css). + +-spec conversejs_script(gen_mod:opts() | global | binary()) -> binary(). +conversejs_script(Opts) when is_map(Opts) -> +    gen_mod:get_opt(conversejs_script, Opts); +conversejs_script(Host) -> +    gen_mod:get_module_opt(Host, mod_conversejs, conversejs_script). + +-spec default_domain(gen_mod:opts() | global | binary()) -> binary(). +default_domain(Opts) when is_map(Opts) -> +    gen_mod:get_opt(default_domain, Opts); +default_domain(Host) -> +    gen_mod:get_module_opt(Host, mod_conversejs, default_domain). + +-spec websocket_url(gen_mod:opts() | global | binary()) -> 'undefined' | binary(). +websocket_url(Opts) when is_map(Opts) -> +    gen_mod:get_opt(websocket_url, Opts); +websocket_url(Host) -> +    gen_mod:get_module_opt(Host, mod_conversejs, websocket_url). + diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl index 427833584..023df39ca 100644 --- a/src/mod_http_api.erl +++ b/src/mod_http_api.erl @@ -30,6 +30,7 @@  -behaviour(gen_mod).  -export([start/2, stop/1, reload/3, process/2, depends/2, +         format_arg/2,  	 mod_options/1, mod_doc/0]).  -include_lib("xmpp/include/xmpp.hrl"). diff --git a/src/mod_mam.erl b/src/mod_mam.erl index abb2333cc..9bf154f58 100644 --- a/src/mod_mam.erl +++ b/src/mod_mam.erl @@ -28,6 +28,7 @@  -protocol({xep, 313, '0.6.1'}).  -protocol({xep, 334, '0.2'}).  -protocol({xep, 359, '0.5.0'}). +-protocol({xep, 441, '0.2.0'}).  -behaviour(gen_mod). diff --git a/src/mod_mqtt_session.erl b/src/mod_mqtt_session.erl index ca025e3d2..e7737804e 100644 --- a/src/mod_mqtt_session.erl +++ b/src/mod_mqtt_session.erl @@ -1134,8 +1134,8 @@ is_expired(#publish{meta = Meta, properties = Props} = Pkt) ->  %%% Authentication  %%%===================================================================  -spec parse_credentials(connect()) -> {ok, jid:jid()} | {error, reason_code()}. -parse_credentials(#connect{client_id = <<>>}) -> -    parse_credentials(#connect{client_id = p1_rand:get_string()}); +parse_credentials(#connect{client_id = <<>>} = C) -> +    parse_credentials(C#connect{client_id = p1_rand:get_string()});  parse_credentials(#connect{username = <<>>, client_id = ClientID}) ->      Host = ejabberd_config:get_myname(),      JID = case jid:make(ClientID, Host) of diff --git a/src/mod_muc.erl b/src/mod_muc.erl index b2ebc5c61..72f386b00 100644 --- a/src/mod_muc.erl +++ b/src/mod_muc.erl @@ -69,6 +69,7 @@  	 get_online_rooms_by_user/3,  	 can_use_nick/4,  	 get_subscribed_rooms/2, +         remove_user/2,  	 procname/2,  	 route/1, unhibernate_room/3]). @@ -122,6 +123,8 @@  start(Host, Opts) ->      case mod_muc_sup:start(Host) of  	{ok, _} -> +            ejabberd_hooks:add(remove_user, Host, ?MODULE, +                               remove_user, 50),  	    MyHosts = gen_mod:get_opt_hosts(Opts),  	    Mod = gen_mod:db_mod(Opts, ?MODULE),  	    RMod = gen_mod:ram_db_mod(Opts, ?MODULE), @@ -133,6 +136,8 @@ start(Host, Opts) ->      end.  stop(Host) -> +    ejabberd_hooks:delete(remove_user, Host, ?MODULE, +                          remove_user, 50),      Proc = mod_muc_sup:procname(Host),      supervisor:terminate_child(ejabberd_gen_mod_sup, Proc),      supervisor:delete_child(ejabberd_gen_mod_sup, Proc). @@ -1122,6 +1127,32 @@ count_online_rooms(ServerHost, Host) ->      RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE),      RMod:count_online_rooms(ServerHost, Host). +-spec remove_user(binary(), binary()) -> ok. +remove_user(User, Server) -> +    LUser = jid:nodeprep(User), +    LServer = jid:nameprep(Server), +    Mod = gen_mod:db_mod(LServer, ?MODULE), +    case erlang:function_exported(Mod, remove_user, 2) of +	true -> +            Mod:remove_user(LUser, LServer); +        false -> +            ok +    end, +    JID = jid:make(User, Server), +    lists:foreach( +      fun(Host) -> +              lists:foreach( +                fun({_, _, Pid}) -> +                        mod_muc_room:change_item_async( +                          Pid, JID, affiliation, none, <<"User removed">>), +                        mod_muc_room:change_item_async( +                          Pid, JID, role, none, <<"User removed">>) +                end, +                get_online_rooms(LServer, Host)) +      end, +      gen_mod:get_module_opt_hosts(LServer, mod_muc)), +    ok. +  opts_to_binary(Opts) ->      lists:map(        fun({title, Title}) -> @@ -1225,6 +1256,8 @@ mod_opt_type(user_message_shaper) ->      econf:atom();  mod_opt_type(user_presence_shaper) ->      econf:atom(); +mod_opt_type(cleanup_affiliations_on_start) -> +    econf:bool();  mod_opt_type(default_room_options) ->      econf:options(        #{allow_change_subj => econf:bool(), @@ -1302,6 +1335,7 @@ mod_options(Host) ->       {preload_rooms, true},       {hibernation_timeout, infinity},       {vcard, undefined}, +     {cleanup_affiliations_on_start, false},       {default_room_options,        [{allow_change_subj,true},         {allow_private_messages,true}, @@ -1580,6 +1614,11 @@ mod_doc() ->                       "    -",                       "      work: true",                       "      street: Elm Street"]}]}}, +           {cleanup_affiliations_on_start, +            #{value => "true | false", +              desc => +                  ?T("Remove affiliations for non-existing local users on startup. " +                     "The default value is 'false'.")}},             {default_room_options,              #{value => ?T("Options"),                desc => diff --git a/src/mod_muc_admin.erl b/src/mod_muc_admin.erl index 2abeee45c..ac2d887fe 100644 --- a/src/mod_muc_admin.erl +++ b/src/mod_muc_admin.erl @@ -40,8 +40,11 @@  	 change_room_option/4, get_room_options/2,  	 set_room_affiliation/4, get_room_affiliations/2, get_room_affiliation/3,  	 web_menu_main/2, web_page_main/2, web_menu_host/3, -	 subscribe_room/4, unsubscribe_room/2, get_subscribers/2, -	 web_page_host/3, mod_options/1, get_commands_spec/0, find_hosts/1]). +	 subscribe_room/4, subscribe_room_many/3, +         unsubscribe_room/2, get_subscribers/2, +         web_page_host/3, +         mod_opt_type/1, mod_options/1, +         get_commands_spec/0, find_hosts/1]).  -include("logger.hrl").  -include_lib("xmpp/include/xmpp.hrl"). @@ -281,7 +284,7 @@ get_commands_spec() ->       #ejabberd_commands{name = send_direct_invitation, tags = [muc_room],  			desc = "Send a direct invitation to several destinations", -			longdesc = "Since ejabberd 20.10, this command is " +			longdesc = "Since ejabberd 20.12, this command is "                          "asynchronous: the API call may return before the "                          "server has send all the invitations.\n\n"                          "Password and Message can also be: none. " @@ -331,6 +334,26 @@ get_commands_spec() ->  			args = [{user, binary}, {nick, binary}, {room, binary},  				{nodes, binary}],  			result = {nodes, {list, {node, string}}}}, +     #ejabberd_commands{name = subscribe_room_many, tags = [muc_room], +			desc = "Subscribe several users to a MUC conference", +			longdesc = "This command accept up to 50 users at once (this is configurable with `subscribe_room_many_max_users` option)", +			module = ?MODULE, function = subscribe_room_many, +			args_desc = ["Users JIDs and nicks", +                                     "the room to subscribe", +                                     "nodes separated by commas: ,"], +			args_example = [[{"tom@localhost", "Tom"}, +                                         {"jerry@localhost", "Jerry"}], +                                        "room1@conference.localhost", +                                        "urn:xmpp:mucsub:nodes:messages,urn:xmpp:mucsub:nodes:affiliations"], +			args = [{users, {list, +                                         {user, {tuple, +                                                 [{jid, binary}, +                                                  {nick, binary} +                                                 ]}} +                                        }}, +                                {room, binary}, +				{nodes, binary}], +			result = {res, rescode}},       #ejabberd_commands{name = unsubscribe_room, tags = [muc_room],  			desc = "Unsubscribe from a MUC conference",  			module = ?MODULE, function = unsubscribe_room, @@ -710,7 +733,7 @@ create_room_with_opts(Name1, Host1, ServerHost1, CustomRoomOpts) ->  maybe_store_room(ServerHost, Host, Name, RoomOpts) ->      case proplists:get_bool(persistent, RoomOpts) of          true -> -            {atomic, ok} = mod_muc:store_room(ServerHost, Host, Name, RoomOpts), +            {atomic, _} = mod_muc:store_room(ServerHost, Host, Name, RoomOpts),              ok;          false ->              ok @@ -860,7 +883,14 @@ get_online_rooms(ServiceArg) ->  	   || {RoomName, RoomHost, Pid} <- mod_muc:get_online_rooms(Host)]        end, Hosts). -get_all_rooms(Host) -> +get_all_rooms(ServiceArg) -> +    Hosts = find_services(ServiceArg), +    lists:flatmap( +      fun(Host) -> +              get_all_rooms2(Host) +      end, Hosts). + +get_all_rooms2(Host) ->      ServerHost = ejabberd_router:host_of_route(Host),      OnlineRooms = get_online_rooms(Host),      OnlineMap = lists:foldl( @@ -1324,6 +1354,18 @@ subscribe_room(User, Nick, Room, Nodes) ->  	    throw({error, "Malformed room JID"})      end. +subscribe_room_many(Users, Room, Nodes) -> +    MaxUsers = mod_muc_admin_opt:subscribe_room_many_max_users(global), +    if +        length(Users) > MaxUsers -> +            throw({error, "Too many users in subscribe_room_many command"}); +        true -> +            lists:foreach( +              fun({User, Nick}) -> +                      subscribe_room(User, Nick, Room, Nodes) +              end, Users) +    end. +  unsubscribe_room(User, Room) ->      try jid:decode(Room) of  	#jid{luser = Name, lserver = Host} when Name /= <<"">> -> @@ -1406,11 +1448,22 @@ find_hosts(ServerHost) ->  	    []      end. -mod_options(_) -> []. +mod_opt_type(subscribe_room_many_max_users) -> +    econf:int(). + +mod_options(_) -> +    [{subscribe_room_many_max_users, 50}].  mod_doc() ->      #{desc =>  	  [?T("This module provides commands to administer local MUC "  	      "services and their MUC rooms. It also provides simple "  	      "WebAdmin pages to view the existing rooms."), "", -	   ?T("This module depends on _`mod_muc`_.")]}. +	   ?T("This module depends on _`mod_muc`_.")], +    opts => +          [{subscribe_room_many_max_users, +            #{value => ?T("Number"), +              desc => +                  ?T("How many users can be subscribed to a room at once using " +                     "the 'subscribe_room_many' command. " +                     "The default value is '50'.")}}]}. diff --git a/src/mod_muc_admin_opt.erl b/src/mod_muc_admin_opt.erl new file mode 100644 index 000000000..18ca64af7 --- /dev/null +++ b/src/mod_muc_admin_opt.erl @@ -0,0 +1,13 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_muc_admin_opt). + +-export([subscribe_room_many_max_users/1]). + +-spec subscribe_room_many_max_users(gen_mod:opts() | global | binary()) -> integer(). +subscribe_room_many_max_users(Opts) when is_map(Opts) -> +    gen_mod:get_opt(subscribe_room_many_max_users, Opts); +subscribe_room_many_max_users(Host) -> +    gen_mod:get_module_opt(Host, mod_muc_admin, subscribe_room_many_max_users). + diff --git a/src/mod_muc_opt.erl b/src/mod_muc_opt.erl index 760a5d7c8..4b9e8b806 100644 --- a/src/mod_muc_opt.erl +++ b/src/mod_muc_opt.erl @@ -9,6 +9,7 @@  -export([access_mam/1]).  -export([access_persistent/1]).  -export([access_register/1]). +-export([cleanup_affiliations_on_start/1]).  -export([db_type/1]).  -export([default_room_options/1]).  -export([hibernation_timeout/1]). @@ -73,6 +74,12 @@ access_register(Opts) when is_map(Opts) ->  access_register(Host) ->      gen_mod:get_module_opt(Host, mod_muc, access_register). +-spec cleanup_affiliations_on_start(gen_mod:opts() | global | binary()) -> boolean(). +cleanup_affiliations_on_start(Opts) when is_map(Opts) -> +    gen_mod:get_opt(cleanup_affiliations_on_start, Opts); +cleanup_affiliations_on_start(Host) -> +    gen_mod:get_module_opt(Host, mod_muc, cleanup_affiliations_on_start). +  -spec db_type(gen_mod:opts() | global | binary()) -> atom().  db_type(Opts) when is_map(Opts) ->      gen_mod:get_opt(db_type, Opts); diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl index 035e851fd..aaf3e8895 100644 --- a/src/mod_muc_room.erl +++ b/src/mod_muc_room.erl @@ -27,6 +27,8 @@  -author('alexey@process-one.net'). +-protocol({xep, 317, '0.1'}). +  -behaviour(p1_fsm).  %% External exports @@ -48,6 +50,7 @@  	 set_config/2,  	 get_state/1,  	 change_item/5, +	 change_item_async/5,  	 config_reloaded/1,  	 subscribe/4,  	 unsubscribe/2, @@ -76,6 +79,12 @@  -define(DEFAULT_MAX_USERS_PRESENCE,1000). +-define(MUC_HAT_ADD_CMD, <<"http://prosody.im/protocol/hats#add">>). +-define(MUC_HAT_REMOVE_CMD, <<"http://prosody.im/protocol/hats#remove">>). +-define(MUC_HAT_LIST_CMD, <<"p1:hats#list">>). +-define(MAX_HATS_USERS, 100). +-define(MAX_HATS_PER_USER, 10). +  %-define(DBGFSM, true).  -ifdef(DBGFSM). @@ -194,6 +203,11 @@ change_item(Pid, JID, Type, AffiliationOrRole, Reason) ->  	    {error, notfound}      end. +-spec change_item_async(pid(), jid(), affiliation | role, affiliation() | role(), binary()) -> ok. +change_item_async(Pid, JID, Type, AffiliationOrRole, Reason) -> +    p1_fsm:send_all_state_event( +      Pid, {process_item_change, {JID, Type, AffiliationOrRole, Reason}, undefined}). +  -spec get_state(pid()) -> {ok, state()} | {error, notfound | timeout}.  get_state(Pid) ->      try p1_fsm:sync_send_all_state_event(Pid, get_state) @@ -298,7 +312,8 @@ init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts, QueueType])  				  room_shaper = Shaper}),      add_to_log(room_existence, started, State),      ejabberd_hooks:run(start_room, ServerHost, [ServerHost, Room, Host]), -    {ok, normal_state, reset_hibernate_timer(State)}. +    State1 = cleanup_affiliations(State), +    {ok, normal_state, reset_hibernate_timer(State1)}.  normal_state({route, <<"">>,  	      #message{from = From, type = Type, lang = Lang} = Packet}, @@ -446,6 +461,8 @@ normal_state({route, <<"">>,  			       process_iq_mucsub(From, IQ, StateData);  			   #xcaptcha{} ->  			       process_iq_captcha(From, IQ, StateData); +			   #adhoc_command{} -> +			       process_iq_adhoc(From, IQ, StateData);  			   _ ->  			       Txt = ?T("The feature requested is not "  					"supported by the conference"), @@ -664,6 +681,16 @@ handle_event({set_affiliations, Affiliations},  	     StateName, StateData) ->      NewStateData = set_affiliations(Affiliations, StateData),      {next_state, StateName, NewStateData}; +handle_event({process_item_change, Item, UJID}, StateName, StateData) -> +    case process_item_change(Item, StateData, UJID) of +	{error, _} -> +            {next_state, StateName, StateData}; +        StateData -> +            {next_state, StateName, StateData}; +	NSD -> +	    store_room(NSD), +            {next_state, StateName, NSD} +    end;  handle_event(_Event, StateName, StateData) ->      {next_state, StateName, StateData}. @@ -712,6 +739,8 @@ handle_sync_event({process_item_change, Item, UJID}, _From, StateName, StateData      case process_item_change(Item, StateData, UJID) of  	{error, _} = Err ->  	    {reply, Err, StateName, StateData}; +        StateData -> +            {reply, {ok, StateData}, StateName, StateData};  	NSD ->  	    store_room(NSD),  	    {reply, {ok, NSD}, StateName, NSD} @@ -1405,6 +1434,12 @@ is_occupant_or_admin(JID, StateData) ->        _ -> false      end. +%% Check if the user is an admin or owner. +-spec is_admin(jid(), state()) -> boolean(). +is_admin(JID, StateData) -> +    FAffiliation = get_affiliation(JID, StateData), +    FAffiliation == admin orelse FAffiliation == owner. +  %% Decide the fate of the message and its sender  %% Returns: continue_delivery | forget_message | {expulse_sender, Reason}  -spec decide_fate_message(message(), jid(), state()) -> @@ -1602,7 +1637,7 @@ do_get_affiliation_fallback(JID, StateData) ->  -spec get_affiliations(state()) -> affiliations().  get_affiliations(#state{config = #config{persistent = false}} = StateData) -> -    get_affiliations_callback(StateData); +    get_affiliations_fallback(StateData);  get_affiliations(StateData) ->      Room = StateData#state.room,      Host = StateData#state.host, @@ -1610,13 +1645,13 @@ get_affiliations(StateData) ->      Mod = gen_mod:db_mod(ServerHost, mod_muc),      case Mod:get_affiliations(ServerHost, Room, Host) of  	{error, _} -> -	    get_affiliations_callback(StateData); +	    get_affiliations_fallback(StateData);  	{ok, Affiliations} ->  	    Affiliations      end. --spec get_affiliations_callback(state()) -> affiliations(). -get_affiliations_callback(StateData) -> +-spec get_affiliations_fallback(state()) -> affiliations(). +get_affiliations_fallback(StateData) ->      StateData#state.affiliations.  -spec get_service_affiliation(jid(), state()) -> owner | none. @@ -1935,7 +1970,7 @@ filter_presence(Presence) ->  		    XMLNS = xmpp:get_ns(El),  		    case catch binary:part(XMLNS, 0, size(?NS_MUC)) of  			?NS_MUC -> false; -			_ -> true +			_ -> XMLNS /= ?NS_HATS  		    end  	    end, xmpp:get_els(Presence)),      xmpp:set_els(Presence, Els). @@ -2485,9 +2520,10 @@ send_new_presence(NJID, Reason, IsInitialPresence, StateData, OldStateData) ->  	      Pres = if Presence == undefined -> #presence{};  			true -> Presence  		     end, -	      Packet = xmpp:set_subtag( -			 Pres, #muc_user{items = [Item], -					 status_codes = StatusCodes}), +              Packet = xmpp:set_subtag( +                         add_presence_hats(NJID, Pres, StateData), +                         #muc_user{items = [Item], +                                   status_codes = StatusCodes}),  	      send_wrapped(jid:replace_resource(StateData#state.jid, Nick),  			   Info#user.jid, Packet, Node1, StateData),  	      Type = xmpp:get_type(Packet), @@ -2536,7 +2572,9 @@ send_existing_presences1(ToJID, StateData) ->  				 false -> Item0  			     end,  		      Packet = xmpp:set_subtag( -				 Presence, #muc_user{items = [Item]}), +                                 add_presence_hats( +                                   FromJID, Presence, StateData), +                                 #muc_user{items = [Item]}),  		      send_wrapped(jid:replace_resource(StateData#state.jid, FromNick),  				   RealToJID, Packet, ?NS_MUCSUB_NODES_PRESENCE, StateData)  	      end @@ -3579,7 +3617,8 @@ get_config(Lang, StateData, From) ->  	 {allow_voice_requests, Config#config.allow_voice_requests},  	 {allow_subscription, Config#config.allow_subscription},  	 {voice_request_min_interval, Config#config.voice_request_min_interval}, -	 {pubsub, Config#config.pubsub}] +	 {pubsub, Config#config.pubsub}, +	 {enable_hats, Config#config.enable_hats}]  	++  	case ejabberd_captcha:is_feature_available() of  	    true -> @@ -3667,6 +3706,7 @@ set_config(Opts, Config, ServerHost, Lang) ->  	 ({maxusers, V}, C) -> C#config{max_users = V};  	 ({enablelogging, V}, C) -> C#config{logging = V};  	 ({pubsub, V}, C) -> C#config{pubsub = V}; +	 ({enable_hats, V}, C) -> C#config{enable_hats = V};  	 ({lang, L}, C) -> C#config{lang = L};  	 ({captcha_whitelist, Js}, C) ->  	      LJIDs = [jid:tolower(J) || J <- Js], @@ -3897,6 +3937,9 @@ set_opts([{Opt, Val} | Opts], StateData) ->  	    allow_subscription ->  		StateData#state{config =  				    (StateData#state.config)#config{allow_subscription = Val}}; +            enable_hats -> +                StateData#state{config = +                                    (StateData#state.config)#config{enable_hats = Val}};  	    lang ->  		StateData#state{config =  				    (StateData#state.config)#config{lang = Val}}; @@ -3927,6 +3970,11 @@ set_opts([{Opt, Val} | Opts], StateData) ->  			 end,  		  StateData#state{subject = Subj};  	    subject_author -> StateData#state{subject_author = Val}; +            hats_users -> +                  Hats = maps:from_list( +                           lists:map(fun({U, H}) -> {U, maps:from_list(H)} end, +                                     Val)), +                  StateData#state{hats_users = Hats};  	    _ -> StateData  	  end,      set_opts(Opts, NSD). @@ -3983,6 +4031,7 @@ make_opts(StateData) ->       ?MAKE_CONFIG_OPT(#config.vcard),       ?MAKE_CONFIG_OPT(#config.vcard_xupdate),       ?MAKE_CONFIG_OPT(#config.pubsub), +     ?MAKE_CONFIG_OPT(#config.enable_hats),       ?MAKE_CONFIG_OPT(#config.lang),       {captcha_whitelist,        (?SETS):to_list((StateData#state.config)#config.captcha_whitelist)}, @@ -3990,6 +4039,9 @@ make_opts(StateData) ->        maps:to_list(StateData#state.affiliations)},       {subject, StateData#state.subject},       {subject_author, StateData#state.subject_author}, +     {hats_users, +      lists:map(fun({U, H}) -> {U, maps:to_list(H)} end, +                maps:to_list(StateData#state.hats_users))},       {hibernation_time, erlang:system_time(microsecond)},       {subscribers, Subscribers}]. @@ -4080,6 +4132,7 @@ maybe_forget_room(StateData) ->  make_disco_info(_From, StateData) ->      Config = StateData#state.config,      Feats = [?NS_VCARD, ?NS_MUC, ?NS_DISCO_INFO, ?NS_DISCO_ITEMS, +             ?NS_COMMANDS,  	     ?CONFIG_OPT_TO_FEATURE((Config#config.public),  				    <<"muc_public">>, <<"muc_hidden">>),  	     ?CONFIG_OPT_TO_FEATURE((Config#config.persistent), @@ -4120,6 +4173,77 @@ process_iq_disco_info(From, #iq{type = get, lang = Lang,      Extras = iq_disco_info_extras(Lang, StateData, false),      {result, DiscoInfo#disco_info{xdata = [Extras]}};  process_iq_disco_info(From, #iq{type = get, lang = Lang, +				sub_els = [#disco_info{node = ?NS_COMMANDS}]}, +		      StateData) -> +    case (StateData#state.config)#config.enable_hats andalso +        is_admin(From, StateData) +    of +        true -> +            {result, +             #disco_info{ +                identities = [#identity{category = <<"automation">>, +                                        type = <<"command-list">>, +                                        name = translate:translate( +                                                 Lang, ?T("Commands"))}]}}; +        false -> +            Txt = ?T("Node not found"), +            {error, xmpp:err_item_not_found(Txt, Lang)} +    end; +process_iq_disco_info(From, #iq{type = get, lang = Lang, +				sub_els = [#disco_info{node = ?MUC_HAT_ADD_CMD}]}, +		      StateData) -> +    case (StateData#state.config)#config.enable_hats andalso +        is_admin(From, StateData) +    of +        true -> +            {result, +             #disco_info{ +                identities = [#identity{category = <<"automation">>, +                                        type = <<"command-node">>, +                                        name = translate:translate( +                                              Lang, ?T("Add a hat to a user"))}], +                features = [?NS_COMMANDS]}}; +        false -> +            Txt = ?T("Node not found"), +            {error, xmpp:err_item_not_found(Txt, Lang)} +    end; +process_iq_disco_info(From, #iq{type = get, lang = Lang, +				sub_els = [#disco_info{node = ?MUC_HAT_REMOVE_CMD}]}, +		      StateData) -> +    case (StateData#state.config)#config.enable_hats andalso +        is_admin(From, StateData) +    of +        true -> +            {result, +             #disco_info{ +                identities = [#identity{category = <<"automation">>, +                                        type = <<"command-node">>, +                                        name = translate:translate( +                                              Lang, ?T("Remove a hat from a user"))}], +                features = [?NS_COMMANDS]}}; +        false -> +            Txt = ?T("Node not found"), +            {error, xmpp:err_item_not_found(Txt, Lang)} +    end; +process_iq_disco_info(From, #iq{type = get, lang = Lang, +				sub_els = [#disco_info{node = ?MUC_HAT_LIST_CMD}]}, +		      StateData) -> +    case (StateData#state.config)#config.enable_hats andalso +        is_admin(From, StateData) +    of +        true -> +            {result, +             #disco_info{ +                identities = [#identity{category = <<"automation">>, +                                        type = <<"command-node">>, +                                        name = translate:translate( +                                              Lang, ?T("List users with hats"))}], +                features = [?NS_COMMANDS]}}; +        false -> +            Txt = ?T("Node not found"), +            {error, xmpp:err_item_not_found(Txt, Lang)} +    end; +process_iq_disco_info(From, #iq{type = get, lang = Lang,  				sub_els = [#disco_info{node = Node}]},  		      StateData) ->      try @@ -4199,6 +4323,46 @@ process_iq_disco_items(From, #iq{type = get, sub_els = [#disco_items{node = <<>>  		{result, #disco_items{}}  	  end      end; +process_iq_disco_items(From, #iq{type = get, lang = Lang, +                                 sub_els = [#disco_items{node = ?NS_COMMANDS}]}, +		       StateData) -> +    case (StateData#state.config)#config.enable_hats andalso +        is_admin(From, StateData) +    of +        true -> +            {result, +             #disco_items{ +                items = [#disco_item{jid = StateData#state.jid, +                                     node = ?MUC_HAT_ADD_CMD, +                                     name = translate:translate( +                                              Lang, ?T("Add a hat to a user"))}, +                         #disco_item{jid = StateData#state.jid, +                                     node = ?MUC_HAT_REMOVE_CMD, +                                     name = translate:translate( +                                              Lang, ?T("Remove a hat from a user"))}, +                         #disco_item{jid = StateData#state.jid, +                                     node = ?MUC_HAT_LIST_CMD, +                                     name = translate:translate( +                                              Lang, ?T("List users with hats"))}]}}; +        false -> +            Txt = ?T("Node not found"), +            {error, xmpp:err_item_not_found(Txt, Lang)} +    end; +process_iq_disco_items(From, #iq{type = get, lang = Lang, +                                 sub_els = [#disco_items{node = Node}]}, +		       StateData) +  when Node == ?MUC_HAT_ADD_CMD; +       Node == ?MUC_HAT_REMOVE_CMD; +       Node == ?MUC_HAT_LIST_CMD -> +    case (StateData#state.config)#config.enable_hats andalso +        is_admin(From, StateData) +    of +        true -> +            {result, #disco_items{}}; +        false -> +            Txt = ?T("Node not found"), +            {error, xmpp:err_item_not_found(Txt, Lang)} +    end;  process_iq_disco_items(_From, #iq{lang = Lang}, _StateData) ->      Txt = ?T("Node not found"),      {error, xmpp:err_item_not_found(Txt, Lang)}. @@ -4441,6 +4605,271 @@ get_mucroom_disco_items(StateData) ->  	       end, [], StateData#state.nicks),      #disco_items{items = Items}. +-spec process_iq_adhoc(jid(), iq(), state()) -> +			      {result, adhoc_command()} | +			      {result, adhoc_command(), state()} | +			      {error, stanza_error()}. +process_iq_adhoc(_From, #iq{type = get}, _StateData) -> +    {error, xmpp:err_bad_request()}; +process_iq_adhoc(From, #iq{type = set, lang = Lang1, +                           sub_els = [#adhoc_command{} = Request]}, +		 StateData) -> +    % Ad-Hoc Commands are used only for Hats here +    case (StateData#state.config)#config.enable_hats andalso +        is_admin(From, StateData) +    of +        true -> +            #adhoc_command{lang = Lang2, node = Node, +                           action = Action, xdata = XData} = Request, +            Lang = case Lang2 of +                       <<"">> -> Lang1; +                       _ -> Lang2 +                   end, +            case {Node, Action} of +                {_, cancel} -> +                    {result, +                     xmpp_util:make_adhoc_response( +                       Request, +                       #adhoc_command{status = canceled, lang = Lang, +                                      node = Node})}; +                {?MUC_HAT_ADD_CMD, execute} -> +                    Form = +                        #xdata{ +                           title = translate:translate( +                                     Lang, ?T("Add a hat to a user")), +                           type = form, +                           fields = +                               [#xdata_field{ +                                   type = 'jid-single', +                                   label = translate:translate(Lang, ?T("Jabber ID")), +                                   required = true, +                                   var = <<"jid">>}, +                                #xdata_field{ +                                   type = 'text-single', +                                   label = translate:translate(Lang, ?T("Hat title")), +                                   var = <<"hat_title">>}, +                                #xdata_field{ +                                   type = 'text-single', +                                   label = translate:translate(Lang, ?T("Hat URI")), +                                   required = true, +                                   var = <<"hat_uri">>} +                               ]}, +                    {result, +                     xmpp_util:make_adhoc_response( +                       Request, +                       #adhoc_command{ +                          status = executing, +                          xdata = Form})}; +                {?MUC_HAT_ADD_CMD, complete} when XData /= undefined -> +                    JID = try +                              jid:decode(hd(xmpp_util:get_xdata_values( +                                              <<"jid">>, XData))) +                          catch _:_ -> error +                          end, +                    URI = try +                              hd(xmpp_util:get_xdata_values( +                                   <<"hat_uri">>, XData)) +                          catch _:_ -> error +                          end, +                    Title = case xmpp_util:get_xdata_values( +                                   <<"hat_title">>, XData) of +                                [] -> <<"">>; +                                [T] -> T +                            end, +                    if +                        (JID /= error) and (URI /= error) -> +                            case add_hat(JID, URI, Title, StateData) of +                                {ok, NewStateData} -> +                                    store_room(NewStateData), +                                    send_update_presence( +                                      JID, NewStateData, StateData), +                                    {result, +                                     xmpp_util:make_adhoc_response( +                                       Request, +                                       #adhoc_command{status = completed}), +                                     NewStateData}; +                                {error, size_limit} -> +                                    Txt = ?T("Hats limit exceeded"), +                                    {error, xmpp:err_not_allowed(Txt, Lang)} +                            end; +                        true -> +                            {error, xmpp:err_bad_request()} +                    end; +                {?MUC_HAT_ADD_CMD, complete} -> +                    {error, xmpp:err_bad_request()}; +                {?MUC_HAT_ADD_CMD, _} -> +                    Txt = ?T("Incorrect value of 'action' attribute"), +                    {error, xmpp:err_bad_request(Txt, Lang)}; +                {?MUC_HAT_REMOVE_CMD, execute} -> +                    Form = +                        #xdata{ +                           title = translate:translate( +                                     Lang, ?T("Remove a hat from a user")), +                           type = form, +                           fields = +                               [#xdata_field{ +                                   type = 'jid-single', +                                   label = translate:translate(Lang, ?T("Jabber ID")), +                                   required = true, +                                   var = <<"jid">>}, +                                #xdata_field{ +                                   type = 'text-single', +                                   label = translate:translate(Lang, ?T("Hat URI")), +                                   required = true, +                                   var = <<"hat_uri">>} +                               ]}, +                    {result, +                     xmpp_util:make_adhoc_response( +                       Request, +                       #adhoc_command{ +                          status = executing, +                          xdata = Form})}; +                {?MUC_HAT_REMOVE_CMD, complete} when XData /= undefined -> +                    JID = try +                              jid:decode(hd(xmpp_util:get_xdata_values( +                                              <<"jid">>, XData))) +                          catch _:_ -> error +                          end, +                    URI = try +                              hd(xmpp_util:get_xdata_values( +                                   <<"hat_uri">>, XData)) +                          catch _:_ -> error +                          end, +                    if +                        (JID /= error) and (URI /= error) -> +                            NewStateData = del_hat(JID, URI, StateData), +                            store_room(NewStateData), +                            send_update_presence( +                              JID, NewStateData, StateData), +                            {result, +                             xmpp_util:make_adhoc_response( +                               Request, +                               #adhoc_command{status = completed}), +                             NewStateData}; +                        true -> +                            {error, xmpp:err_bad_request()} +                    end; +                {?MUC_HAT_REMOVE_CMD, complete} -> +                    {error, xmpp:err_bad_request()}; +                {?MUC_HAT_REMOVE_CMD, _} -> +                    Txt = ?T("Incorrect value of 'action' attribute"), +                    {error, xmpp:err_bad_request(Txt, Lang)}; +                {?MUC_HAT_LIST_CMD, execute} -> +                    Hats = get_all_hats(StateData), +                    Items = +                        lists:map( +                          fun({JID, URI, Title}) -> +                                  [#xdata_field{ +                                      var = <<"jid">>, +                                      values = [jid:encode(JID)]}, +                                   #xdata_field{ +                                      var = <<"hat_title">>, +                                      values = [URI]}, +                                   #xdata_field{ +                                      var = <<"hat_uri">>, +                                      values = [Title]}] +                          end, Hats), +                    Form = +                        #xdata{ +                           title = translate:translate( +                                     Lang, ?T("List of users with hats")), +                           type = result, +                           reported = +                               [#xdata_field{ +                                   label = translate:translate(Lang, ?T("Jabber ID")), +                                   var = <<"jid">>}, +                                #xdata_field{ +                                   label = translate:translate(Lang, ?T("Hat title")), +                                   var = <<"hat_title">>}, +                                #xdata_field{ +                                   label = translate:translate(Lang, ?T("Hat URI")), +                                   var = <<"hat_uri">>}], +                           items = Items}, +                    {result, +                     xmpp_util:make_adhoc_response( +                       Request, +                       #adhoc_command{ +                          status = completed, +                          xdata = Form})}; +                {?MUC_HAT_LIST_CMD, _} -> +                    Txt = ?T("Incorrect value of 'action' attribute"), +                    {error, xmpp:err_bad_request(Txt, Lang)}; +                _ -> +                    {error, xmpp:err_item_not_found()} +            end; +	_ -> +	    {error, xmpp:err_forbidden()} +    end. + +-spec add_hat(jid(), binary(), binary(), state()) -> +                     {ok, state()} | {error, size_limit}. +add_hat(JID, URI, Title, StateData) -> +    Hats = StateData#state.hats_users, +    LJID = jid:remove_resource(jid:tolower(JID)), +    UserHats = maps:get(LJID, Hats, #{}), +    UserHats2 = maps:put(URI, Title, UserHats), +    USize = maps:size(UserHats2), +    if +        USize =< ?MAX_HATS_PER_USER -> +            Hats2 = maps:put(LJID, UserHats2, Hats), +            Size = maps:size(Hats2), +            if +                Size =< ?MAX_HATS_USERS -> +                    {ok, StateData#state{hats_users = Hats2}}; +                true -> +                    {error, size_limit} +            end; +        true -> +            {error, size_limit} +    end. + +-spec del_hat(jid(), binary(), state()) -> state(). +del_hat(JID, URI, StateData) -> +    Hats = StateData#state.hats_users, +    LJID = jid:remove_resource(jid:tolower(JID)), +    UserHats = maps:get(LJID, Hats, #{}), +    UserHats2 = maps:remove(URI, UserHats), +    Hats2 = +        case maps:size(UserHats2) of +            0 -> +                maps:remove(LJID, Hats); +            _ -> +                maps:put(LJID, UserHats2, Hats) +        end, +    StateData#state{hats_users = Hats2}. + +-spec get_all_hats(state()) -> list({jid(), binary(), binary()}). +get_all_hats(StateData) -> +    lists:flatmap( +      fun({LJID, H}) -> +              JID = jid:make(LJID), +              lists:map(fun({URI, Title}) -> {JID, URI, Title} end, +                        maps:to_list(H)) +      end, +      maps:to_list(StateData#state.hats_users)). + +-spec add_presence_hats(jid(), #presence{}, state()) -> #presence{}. +add_presence_hats(JID, Pres, StateData) -> +    case (StateData#state.config)#config.enable_hats of +        true -> +            Hats = StateData#state.hats_users, +            LJID = jid:remove_resource(jid:tolower(JID)), +            UserHats = maps:get(LJID, Hats, #{}), +            case maps:size(UserHats) of +                0 -> Pres; +                _ -> +                    Items = +                        lists:map(fun({URI, Title}) -> +                                          #muc_hat{uri = URI, title = Title} +                                  end, +                                  maps:to_list(UserHats)), +                    xmpp:set_subtag(Pres, +                                    #muc_hats{hats = Items}) +            end; +        false -> +            Pres +    end. +  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%  % Voice request support @@ -4687,7 +5116,7 @@ send_subscriptions_change_notifications(From, Nick, Type, State) ->  			id = p1_rand:get_string(),  			sub_els = [Payload1]}]}}]},  	ejabberd_router_multicast:route_multicast(State#state.jid, State#state.server_host, -						  WJ, Packet1, true); +						  WJ, Packet1, false);  	true -> ok      end,      if WN /= [] -> @@ -4703,7 +5132,7 @@ send_subscriptions_change_notifications(From, Nick, Type, State) ->  			id = p1_rand:get_string(),  			sub_els = [Payload2]}]}}]},  	ejabberd_router_multicast:route_multicast(State#state.jid, State#state.server_host, -					       WN, Packet2, true); +					       WN, Packet2, false);  	true -> ok      end. @@ -4927,6 +5356,23 @@ muc_subscribers_put(Subscriber, MUCSubscribers) ->                       subscriber_nodes = NewSubNodes}. +cleanup_affiliations(State) -> +    case mod_muc_opt:cleanup_affiliations_on_start(State#state.server_host) of +        true -> +            Affiliations = +                maps:filter( +                  fun({LUser, LServer, _}, _) -> +                          case ejabberd_router:is_my_host(LServer) of +                              true -> +                                  ejabberd_auth:user_exists(LUser, LServer); +                              false -> +                                  true +                          end +                  end, State#state.affiliations), +            State#state{affiliations = Affiliations}; +        false -> +            State +    end.  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%  %% Detect messange stanzas that don't have meaningful content diff --git a/src/mod_muc_sql.erl b/src/mod_muc_sql.erl index 1310cde7b..8aa7ad62b 100644 --- a/src/mod_muc_sql.erl +++ b/src/mod_muc_sql.erl @@ -38,7 +38,7 @@  	 register_online_user/4, unregister_online_user/4,  	 count_online_rooms_by_user/3, get_online_rooms_by_user/3,  	 get_subscribed_rooms/3, get_rooms_without_subscribers/2, -	 find_online_room_by_pid/2]). +	 find_online_room_by_pid/2, remove_user/2]).  -export([set_affiliation/6, set_affiliations/4, get_affiliation/5,  	 get_affiliations/3, search_affiliation/4]). @@ -465,6 +465,13 @@ get_subscribed_rooms(LServer, Host, Jid) ->  	    {error, db_failure}      end. +remove_user(LUser, LServer) -> +    SJID = jid:encode(jid:make(LUser, LServer)), +    ejabberd_sql:sql_query( +      LServer, +      ?SQL("delete from muc_room_subscribers where jid=%(SJID)s")), +    ok. +  %%%===================================================================  %%% Internal functions  %%%=================================================================== diff --git a/src/mod_multicast.erl b/src/mod_multicast.erl index 161d3a4c4..fa076da70 100644 --- a/src/mod_multicast.erl +++ b/src/mod_multicast.erl @@ -35,7 +35,7 @@  %% API  -export([start/2, stop/1, reload/3, -         user_send_packet/1]). +	 user_send_packet/1]).  %% gen_server callbacks  -export([init/1, handle_info/2, handle_call/3, @@ -51,11 +51,6 @@  		     response,  		     ts :: integer()}). --record(dest, {jid_string :: binary() | none, -	       jid_jid :: jid() | undefined, -	       type :: bcc | cc | noreply | ofrom | replyroom | replyto | to, -	       address :: address()}). -  -type limit_value() :: {default | custom, integer()}.  -record(limits, {message :: limit_value(),  		 presence :: limit_value()}). @@ -63,14 +58,6 @@  -record(service_limits, {local :: #limits{},  			 remote :: #limits{}}). --type routing() :: route_single | {route_multicast, binary(), #service_limits{}}. - --record(group, {server :: binary(), -		dests :: [#dest{}], -		multicast :: routing() | undefined, -		others :: [address()], -		addresses :: [address()]}). -  -record(state, {lserver :: binary(),  		lservice :: binary(),  		access :: atom(), @@ -117,7 +104,7 @@ reload(LServerS, NewOpts, OldOpts) ->  user_send_packet({#presence{} = Packet, C2SState} = Acc) ->      case xmpp:get_subtag(Packet, #addresses{}) of          #addresses{list = Addresses} -> -            {ToDeliver, _Delivereds} = split_addresses_todeliver(Addresses), +            {CC, BCC, _Invalid, _Delivered} = partition_addresses(Addresses),              NewState =                  lists:foldl(                    fun(Address, St) -> @@ -138,7 +125,7 @@ user_send_packet({#presence{} = Packet, C2SState} = Acc) ->                                undefined ->                                    St                            end -                  end, C2SState, ToDeliver), +                  end, C2SState, CC ++ BCC),              {Packet, NewState};  	false ->  	    Acc @@ -308,19 +295,10 @@ iq_vcard(Lang, State) ->  %%%-------------------------  -spec route_trusted(binary(), binary(), jid(), [jid()], stanza()) -> 'ok'. -route_trusted(LServiceS, LServerS, FromJID, -	      Destinations, Packet) -> -    Packet_stripped = Packet, -    Delivereds = [], -    Dests2 = lists:map( -	       fun(D) -> -		       #dest{jid_string = jid:encode(D), -			     jid_jid    = D, type = bcc, -			     address    = #address{type = bcc, jid = D}} -	       end, Destinations), -    Groups = group_dests(Dests2), -    route_common(LServerS, LServiceS, FromJID, Groups, -		 Delivereds, Packet_stripped). +route_trusted(LServiceS, LServerS, FromJID, Destinations, Packet) -> +    Addresses = [#address{type = bcc, jid = D} || D <- Destinations], +    Groups = group_by_destinations(Addresses, #{}), +    route_grouped(LServerS, LServiceS, FromJID, Groups, [], Packet).  -spec route_untrusted(binary(), binary(), atom(), #service_limits{}, stanza()) -> 'ok'.  route_untrusted(LServiceS, LServerS, Access, SLimits, Packet) -> @@ -356,50 +334,88 @@ route_untrusted(LServiceS, LServerS, Access, SLimits, Packet) ->  route_untrusted2(LServiceS, LServerS, Access, SLimits, Packet) ->      FromJID = xmpp:get_from(Packet),      ok = check_access(LServerS, Access, FromJID), -    {ok, Packet_stripped, Addresses} = strip_addresses_element(Packet), -    {To_deliver, Delivereds} = split_addresses_todeliver(Addresses), -    Dests = convert_dest_record(To_deliver), -    {Dests2, Not_jids} = split_dests_jid(Dests), -    report_not_jid(FromJID, Packet, Not_jids), -    ok = check_limit_dests(SLimits, FromJID, Packet, Dests2), -    Groups = group_dests(Dests2), +    {ok, PacketStripped, Addresses} = strip_addresses_element(Packet), +    {CC, BCC, NotJids, Rest} = partition_addresses(Addresses), +    report_not_jid(FromJID, Packet, NotJids), +    ok = check_limit_dests(SLimits, FromJID, Packet, length(CC) + length(BCC)), +    Groups0 = group_by_destinations(CC, #{}), +    Groups = group_by_destinations(BCC, Groups0),      ok = check_relay(FromJID#jid.server, LServerS, Groups), -    route_common(LServerS, LServiceS, FromJID, Groups, -		 Delivereds, Packet_stripped). - --spec route_common(binary(), binary(), jid(), [#group{}], -		   [address()], stanza()) -> 'ok'. -route_common(LServerS, LServiceS, FromJID, Groups, -	     Delivereds, Packet_stripped) -> -    Groups2 = look_cached_servers(LServerS, LServiceS, Groups), -    Groups3 = build_others_xml(Groups2), -    Groups4 = add_addresses(Delivereds, Groups3), -    AGroups = decide_action_groups(Groups4), -    act_groups(FromJID, Packet_stripped, LServiceS, -	       AGroups). - --spec act_groups(jid(), stanza(), binary(), [{routing(), #group{}}]) -> 'ok'. -act_groups(FromJID, Packet_stripped, LServiceS, AGroups) -> +    route_grouped(LServerS, LServiceS, FromJID, Groups, Rest, PacketStripped). + +-spec mark_as_delivered([address()]) -> [address()]. +mark_as_delivered(Addresses) -> +    [A#address{delivered = true} || A <- Addresses]. + +-spec route_individual(jid(), [address()], [address()], [address()], stanza()) -> ok. +route_individual(From, CC, BCC, Other, Packet) -> +    CCDelivered = mark_as_delivered(CC), +    Addresses = CCDelivered ++ Other, +    PacketWithAddresses = xmpp:append_subtags(Packet, [#addresses{list = Addresses}]),      lists:foreach( -	fun(AGroup) -> -	    perform(FromJID, Packet_stripped, LServiceS, -		    AGroup) -	end, AGroups). - --spec perform(jid(), stanza(), binary(), -	      {routing(), #group{}}) -> 'ok'. -perform(From, Packet, _, -	{route_single, Group}) -> +	fun(#address{jid = To}) -> +	    ejabberd_router:route(xmpp:set_from_to(PacketWithAddresses, From, To)) +	end, CC),      lists:foreach( -	fun(ToUser) -> -	    Group_others = strip_other_bcc(ToUser, Group#group.others), -	    route_packet(From, ToUser, Packet, -			 Group_others, Group#group.addresses) -	end, Group#group.dests); -perform(From, Packet, _, -	{{route_multicast, JID, RLimits}, Group}) -> -    route_packet_multicast(From, JID, Packet, -			   Group#group.dests, Group#group.addresses, RLimits). +	fun(#address{jid = To} = Address) -> +	    Packet2 = case Addresses of +			  [] -> +			      Packet; +			  _ -> +			      xmpp:append_subtags(Packet, [#addresses{list = [Address | Addresses]}]) +		      end, +	    ejabberd_router:route(xmpp:set_from_to(Packet2, From, To)) +	end, BCC). + +-spec route_chunk(jid(), jid(), stanza(), [address()]) -> ok. +route_chunk(From, To, Packet, Addresses) -> +    PacketWithAddresses = xmpp:append_subtags(Packet, [#addresses{list = Addresses}]), +    ejabberd_router:route(xmpp:set_from_to(PacketWithAddresses, From, To)). + +-spec route_in_chunks(jid(), jid(), stanza(), integer(), [address()], [address()], [address()]) -> ok. +route_in_chunks(_From, _To, _Packet, _Limit, [], [], _) -> +    ok; +route_in_chunks(From, To, Packet, Limit, CC, BCC, RestOfAddresses) when length(CC) > Limit -> +    {Chunk, Rest} = lists:split(Limit, CC), +    route_chunk(From, To, Packet, Chunk ++ RestOfAddresses), +    route_in_chunks(From, To, Packet, Limit, Rest, BCC, RestOfAddresses); +route_in_chunks(From, To, Packet, Limit, [], BCC, RestOfAddresses) when length(BCC) > Limit -> +    {Chunk, Rest} = lists:split(Limit, BCC), +    route_chunk(From, To, Packet, Chunk ++ RestOfAddresses), +    route_in_chunks(From, To, Packet, Limit, [], Rest, RestOfAddresses); +route_in_chunks(From, To, Packet, Limit, CC, BCC, RestOfAddresses) when length(BCC) + length(CC) > Limit -> +    {Chunk, Rest} = lists:split(Limit - length(CC), BCC), +    route_chunk(From, To, Packet, CC ++ Chunk ++ RestOfAddresses), +    route_in_chunks(From, To, Packet, Limit, [], Rest, RestOfAddresses); +route_in_chunks(From, To, Packet, _Limit, CC, BCC, RestOfAddresses) -> +    route_chunk(From, To, Packet, CC ++ BCC ++ RestOfAddresses). + +-spec route_multicast(jid(), jid(), [address()], [address()], [address()], stanza(), #limits{}) -> ok. +route_multicast(From, To, CC, BCC, RestOfAddresses, Packet, Limits) -> +    {_Type, Limit} = get_limit_number(element(1, Packet), +				      Limits), +    route_in_chunks(From, To, Packet, Limit, CC, BCC, RestOfAddresses). + +-spec route_grouped(binary(), binary(), jid(), #{}, [address()], stanza()) -> ok. +route_grouped(LServer, LService, From, Groups, RestOfAddresses, Packet) -> +    maps:fold( +	fun(Server, {CC, BCC}, _) -> +	    OtherCC = maps:fold( +		fun(Server2, _, Res) when Server2 == Server -> +			Res; +		   (_, {CC2, _}, Res) -> +			mark_as_delivered(CC2) ++ Res +		end, [], Groups), +	    case search_server_on_cache(Server, +					LServer, LService, +					{?MAXTIME_CACHE_POSITIVE, +					 ?MAXTIME_CACHE_NEGATIVE}) of +		route_single -> +		    route_individual(From, CC, BCC, OtherCC ++ RestOfAddresses, Packet); +		{route_multicast, Service, Limits} -> +		    route_multicast(From, Service, CC, BCC, OtherCC ++ RestOfAddresses, Packet, Limits) +	    end +	end, ok, Groups).  %%%-------------------------  %%% Check access permission @@ -426,244 +442,88 @@ strip_addresses_element(Packet) ->      end.  %%%------------------------- -%%% Strip third-party bcc 'addresses' -%%%------------------------- - -strip_other_bcc(#dest{jid_jid = ToUserJid}, Group_others) -> -    lists:filter( -        fun(#address{jid = JID, type = Type}) -> -            case {JID, Type} of -                {ToUserJid, bcc} -> true; -                {_, bcc} -> false; -                _ -> true -            end -        end, -    Group_others). - -%%%-------------------------  %%% Split Addresses  %%%------------------------- --spec split_addresses_todeliver([address()]) -> {[address()], [address()]}. -split_addresses_todeliver(Addresses) -> -    lists:partition( -      fun(#address{delivered = true}) -> -	      false; -	 (#address{type = Type}) -> -	      case Type of -		  to -> true; -		  cc -> true; -		  bcc -> true; -		  _ -> false -	      end -      end, Addresses). +partition_addresses(Addresses) -> +    lists:foldl( +	fun(#address{delivered = true} = A, {C, B, I, D}) -> +	    {C, B, I, [A | D]}; +	   (#address{type = T, jid = undefined} = A, {C, B, I, D}) +	       when T == to; T == cc; T == bcc -> +	       {C, B, [A | I], D}; +	   (#address{type = T} = A, {C, B, I, D}) +	       when T == to; T == cc -> +	       {[A | C], B, I, D}; +	   (#address{type = bcc} = A, {C, B, I, D}) -> +	       {C, [A | B], I, D}; +	   (A, {C, B, I, D}) -> +	       {C, B, I, [A | D]} +	end, {[], [], [], []}, Addresses).  %%%-------------------------  %%% Check does not exceed limit of destinations  %%%------------------------- --spec check_limit_dests(#service_limits{}, jid(), stanza(), [address()]) -> ok. -check_limit_dests(SLimits, FromJID, Packet, -		  Addresses) -> +-spec check_limit_dests(#service_limits{}, jid(), stanza(), integer()) -> ok. +check_limit_dests(SLimits, FromJID, Packet, NumOfAddresses) ->      SenderT = sender_type(FromJID),      Limits = get_slimit_group(SenderT, SLimits), -    Type_of_stanza = type_of_stanza(Packet), -    {_Type, Limit_number} = get_limit_number(Type_of_stanza, -					     Limits), -    case length(Addresses) > Limit_number of +    StanzaType = type_of_stanza(Packet), +    {_Type, Limit} = get_limit_number(StanzaType, +				      Limits), +    case NumOfAddresses > Limit of        false -> ok;        true -> throw(etoorec)      end. -%%%------------------------- -%%% Convert Destination XML to record -%%%------------------------- - --spec convert_dest_record([address()]) -> [#dest{}]. -convert_dest_record(Addrs) -> -    lists:map( -      fun(#address{jid = undefined, type = Type} = Addr) -> -	      #dest{jid_string = none, -		    type = Type, address = Addr}; -	 (#address{jid = JID, type = Type} = Addr) -> -	      #dest{jid_string = jid:encode(JID), jid_jid = JID, -		    type = Type, address = Addr} -      end, Addrs). - -%%%------------------------- -%%% Split destinations by existence of JID -%%% and send error messages for other dests -%%%------------------------- --spec split_dests_jid([#dest{}]) -> {[#dest{}], [#dest{}]}. -split_dests_jid(Dests) -> -    lists:partition(fun (Dest) -> -			    case Dest#dest.jid_string of -			      none -> false; -			      _ -> true -			    end -		    end, -		    Dests). - --spec report_not_jid(jid(), stanza(), [#dest{}]) -> any(). -report_not_jid(From, Packet, Dests) -> -    Dests2 = [fxml:element_to_binary(xmpp:encode(Dest#dest.address)) -	      || Dest <- Dests], -    [route_error( -       xmpp:set_from_to(Packet, From, From), jid_malformed, -       str:format(?T("This service can not process the address: ~s"), [D])) -     || D <- Dests2]. +-spec report_not_jid(jid(), stanza(), [address()]) -> any(). +report_not_jid(From, Packet, Addresses) -> +    lists:foreach( +	fun(Address) -> +	    route_error( +		xmpp:set_from_to(Packet, From, From), jid_malformed, +		str:format(?T("This service can not process the address: ~s"), +			   [fxml:element_to_binary(xmpp:encode(Address))])) +	end, Addresses).  %%%-------------------------  %%% Group destinations by their servers  %%%------------------------- --spec group_dests([#dest{}]) -> [#group{}]. -group_dests(Dests) -> -    D = lists:foldl(fun (Dest, Dict) -> -			    ServerS = (Dest#dest.jid_jid)#jid.server, -			    dict:append(ServerS, Dest, Dict) -		    end, -		    dict:new(), Dests), -    Keys = dict:fetch_keys(D), -    [#group{server = Key, dests = dict:fetch(Key, D), -	    addresses = [], others = []} -     || Key <- Keys]. - -%%%------------------------- -%%% Look for cached responses -%%%------------------------- - -look_cached_servers(LServerS, LServiceS, Groups) -> -    [look_cached(LServerS, LServiceS, Group) || Group <- Groups]. - -look_cached(LServerS, LServiceS, G) -> -    Maxtime_positive = (?MAXTIME_CACHE_POSITIVE), -    Maxtime_negative = (?MAXTIME_CACHE_NEGATIVE), -    Cached_response = search_server_on_cache(G#group.server, -					     LServerS, LServiceS, -					     {Maxtime_positive, -					      Maxtime_negative}), -    G#group{multicast = Cached_response}. - -%%%------------------------- -%%% Build delivered XML element -%%%------------------------- - -build_others_xml(Groups) -> -    [Group#group{others = -		     build_other_xml(Group#group.dests)} -     || Group <- Groups]. - -build_other_xml(Dests) -> -    lists:foldl(fun (Dest, R) -> -			XML = Dest#dest.address, -			case Dest#dest.type of -			  to -> [add_delivered(XML) | R]; -			  cc -> [add_delivered(XML) | R]; -			  _ -> [XML | R] -			end -		end, -		[], Dests). - --spec add_delivered(address()) -> address(). -add_delivered(Addr) -> -    Addr#address{delivered = true}. - -%%%------------------------- -%%% Add preliminary packets -%%%------------------------- - -add_addresses(Delivereds, Groups) -> -    Ps = [Group#group.others || Group <- Groups], -    add_addresses2(Delivereds, Groups, [], [], Ps). - -add_addresses2(_, [], Res, _, []) -> Res; -add_addresses2(Delivereds, [Group | Groups], Res, Pa, -	       [Pi | Pz]) -> -    Addresses = lists:append([Delivereds] ++ Pa ++ Pz), -    Group2 = Group#group{addresses = Addresses}, -    add_addresses2(Delivereds, Groups, [Group2 | Res], -		   [Pi | Pa], Pz). - -%%%------------------------- -%%% Decide action groups -%%%------------------------- - --spec decide_action_groups([#group{}]) -> [{routing(), #group{}}]. -decide_action_groups(Groups) -> -    [{Group#group.multicast, Group} -     || Group <- Groups]. +group_by_destinations(Addrs, Map) -> +    lists:foldl( +	fun +	    (#address{type = Type, jid = #jid{lserver = Server}} = Addr, Map2) when Type == to; Type == cc -> +		maps:update_with(Server, +		    fun({CC, BCC}) -> +			{[Addr | CC], BCC} +		    end, {[Addr], []}, Map2); +	    (#address{type = bcc, jid = #jid{lserver = Server}} = Addr, Map2) -> +		maps:update_with(Server, +		    fun({CC, BCC}) -> +			{CC, [Addr | BCC]} +		    end, {[], [Addr]}, Map2) +	end, Map, Addrs).  %%%-------------------------  %%% Route packet  %%%------------------------- --spec route_packet(jid(), #dest{}, stanza(), [addresses()], [addresses()]) -> 'ok'. -route_packet(From, ToDest, Packet, Others, Addresses) -> -    Dests = case ToDest#dest.type of -	      bcc -> []; -	      _ -> [ToDest] -	    end, -    route_packet2(From, ToDest#dest.jid_string, Dests, -		  Packet, {Others, Addresses}). - --spec route_packet_multicast(jid(), binary(), stanza(), [#dest{}], [address()], #limits{}) -> 'ok'. -route_packet_multicast(From, ToS, Packet, Dests, -		       Addresses, Limits) -> -    Type_of_stanza = type_of_stanza(Packet), -    {_Type, Limit_number} = get_limit_number(Type_of_stanza, -					     Limits), -    Fragmented_dests = fragment_dests(Dests, Limit_number), -    lists:foreach(fun(DFragment) -> -	route_packet2(From, ToS, DFragment, Packet, -		      Addresses) -	end, Fragmented_dests). - --spec route_packet2(jid(), binary(), [#dest{}], stanza(), {[address()], [address()]} | [address()]) -> 'ok'. -route_packet2(From, ToS, Dests, Packet, Addresses) -> -    Els = case append_dests(Dests, Addresses) of -	      [] -> -		  xmpp:get_els(Packet); -	      ACs -> -		  [#addresses{list = ACs}|xmpp:get_els(Packet)] -	  end, -    Packet2 = xmpp:set_els(Packet, Els), -    ToJID = stj(ToS), -    ejabberd_router:route(xmpp:set_from_to(Packet2, From, ToJID)). - --spec append_dests([#dest{}], {[address()], [address()]} | [address()]) -> [address()]. -append_dests(_Dests, {Others, Addresses}) -> -    Addresses ++ Others; -append_dests([], Addresses) -> Addresses; -append_dests([Dest | Dests], Addresses) -> -    append_dests(Dests, [Dest#dest.address | Addresses]). -  %%%-------------------------  %%% Check relay  %%%------------------------- --spec check_relay(binary(), binary(), [#group{}]) -> ok. +-spec check_relay(binary(), binary(), #{}) -> ok.  check_relay(RS, LS, Gs) -> -    case check_relay_required(RS, LS, Gs) of -      false -> ok; -      true -> throw(edrelay) -    end. - --spec check_relay_required(binary(), binary(), [#group{}]) -> boolean(). -check_relay_required(RServer, LServerS, Groups) -> -    case lists:suffix(str:tokens(LServerS, <<".">>), -                      str:tokens(RServer, <<".">>)) of -      true -> false; -      false -> check_relay_required(LServerS, Groups) +    case lists:suffix(str:tokens(LS, <<".">>), +		      str:tokens(RS, <<".">>)) orelse +	(maps:is_key(LS, Gs) andalso maps:size(Gs) == 1) of +	true -> ok; +	_ -> throw(edrelay)      end. --spec check_relay_required(binary(), [#group{}]) -> boolean(). -check_relay_required(LServerS, Groups) -> -    lists:any(fun (Group) -> Group#group.server /= LServerS -	      end, -	      Groups). -  %%%-------------------------  %%% Check protocol support: Send request  %%%------------------------- @@ -1060,20 +920,6 @@ get_slimit_group(local, SLimits) ->  get_slimit_group(remote, SLimits) ->      SLimits#service_limits.remote. -fragment_dests(Dests, Limit_number) -> -    {R, _} = lists:foldl(fun (Dest, {Res, Count}) -> -				 case Count of -				   Limit_number -> -				       Head2 = [Dest], {[Head2 | Res], 0}; -				   _ -> -				       [Head | Tail] = Res, -				       Head2 = [Dest | Head], -				       {[Head2 | Tail], Count + 1} -				 end -			 end, -			 {[[]], 0}, Dests), -    R. -  %%%-------------------------  %%% Limits: XEP-0128 Service Discovery Extensions  %%%------------------------- diff --git a/src/mod_pubsub.erl b/src/mod_pubsub.erl index 2e40d8f0e..d161ec10c 100644 --- a/src/mod_pubsub.erl +++ b/src/mod_pubsub.erl @@ -95,7 +95,7 @@      terminate/2, code_change/3, depends/2, mod_opt_type/1, mod_options/1]).  %% ejabberd commands --export([get_commands_spec/0, delete_old_items/1]). +-export([get_commands_spec/0, delete_old_items/1, delete_expired_items/0]).  -export([route/1]). @@ -3431,6 +3431,14 @@ max_items(Host, Options) ->  	    end      end. +-spec item_expire(host(), [{atom(), any()}]) -> non_neg_integer() | infinity. +item_expire(Host, Options) -> +    case get_option(Options, item_expire) of +	I when is_integer(I), I < 0 -> 0; +	I when is_integer(I) -> I; +	_ -> get_max_item_expire_node(Host) +    end. +  -spec get_configure_xfields(_, pubsub_node_config:result(),  			    binary(), [binary()]) -> [xdata_field()].  get_configure_xfields(_Type, Options, Lang, Groups) -> @@ -3504,17 +3512,24 @@ decode_node_config(undefined, _, _) ->  decode_node_config(#xdata{fields = Fs}, Host, Lang) ->      try  	Config = pubsub_node_config:decode(Fs), -	Max = get_max_items_node(Host), -	case {check_opt_range(max_items, Config, Max), +	MaxItems = get_max_items_node(Host), +	MaxExpiry = get_max_item_expire_node(Host), +	case {check_opt_range(max_items, Config, MaxItems), +	      check_opt_range(item_expire, Config, MaxExpiry),  	      check_opt_range(max_payload_size, Config, ?MAX_PAYLOAD_SIZE)} of -	    {true, true} -> +	    {true, true, true} ->  		Config; -	    {true, false} -> +	    {true, true, false} ->  		erlang:error(  		  {pubsub_node_config,  		   {bad_var_value, <<"pubsub#max_payload_size">>,  		    ?NS_PUBSUB_NODE_CONFIG}}); -	    {false, _} -> +	    {true, false, _} -> +		erlang:error( +		  {pubsub_node_config, +		   {bad_var_value, <<"pubsub#item_expire">>, +		    ?NS_PUBSUB_NODE_CONFIG}}); +	    {false, _, _} ->  		erlang:error(  		  {pubsub_node_config,  		   {bad_var_value, <<"pubsub#max_items">>, @@ -3560,20 +3575,24 @@ decode_get_pending(#xdata{fields = Fs}, Lang) ->      end.  -spec check_opt_range(atom(), [proplists:property()], -		      non_neg_integer() | unlimited | undefined) -> boolean(). -check_opt_range(_Opt, _Opts, undefined) -> -    true; +		      non_neg_integer() | unlimited | infinity) -> boolean().  check_opt_range(_Opt, _Opts, unlimited) ->      true; +check_opt_range(_Opt, _Opts, infinity) -> +    true;  check_opt_range(Opt, Opts, Max) ->      case proplists:get_value(Opt, Opts, Max) of  	max -> true;  	Val -> Val =< Max      end. --spec get_max_items_node(host()) -> undefined | unlimited | non_neg_integer(). +-spec get_max_items_node(host()) -> unlimited | non_neg_integer().  get_max_items_node(Host) -> -    config(Host, max_items_node, undefined). +    config(Host, max_items_node, ?MAXITEMS). + +-spec get_max_item_expire_node(host()) -> infinity | non_neg_integer(). +get_max_item_expire_node(Host) -> +    config(Host, max_item_expire_node, infinity).  -spec get_max_subscriptions_node(host()) -> undefined | non_neg_integer().  get_max_subscriptions_node(Host) -> @@ -4181,16 +4200,63 @@ delete_old_items(N) ->  	    ok      end. +-spec delete_expired_items() -> ok | error. +delete_expired_items() -> +    Results = lists:flatmap( +		fun(Host) -> +			case tree_action(Host, get_all_nodes, [Host]) of +			    Nodes when is_list(Nodes) -> +				lists:map( +				  fun(#pubsub_node{id = Nidx, type = Type, +						   options = Options}) -> +					  case item_expire(Host, Options) of +					      infinity -> +						  ok; +					      Seconds -> +						  case node_action( +							 Host, Type, +							 remove_expired_items, +							 [Nidx, Seconds]) of +						      {result, []} -> +							  ok; +						      {result, [_|_]} -> +							  unset_cached_item( +							    Host, Nidx); +						      {error, _} -> +							  error +						  end +					  end +				  end, Nodes); +			    _ -> +				error +			end +		end, ejabberd_option:hosts()), +    case lists:member(error, Results) of +	true -> +	    error; +	false -> +	    ok +    end. +  -spec get_commands_spec() -> [ejabberd_commands()].  get_commands_spec() ->      [#ejabberd_commands{name = delete_old_pubsub_items, tags = [purge],  			desc = "Keep only NUMBER of PubSub items per node", +		        note = "added in 21.12",  			module = ?MODULE, function = delete_old_items,  			args_desc = ["Number of items to keep per node"],  			args = [{number, integer}],  			result = {res, rescode},  			result_desc = "0 if command failed, 1 when succeeded",  			args_example = [1000], +			result_example = ok}, +     #ejabberd_commands{name = delete_expired_pubsub_items, tags = [purge], +			desc = "Delete expired PubSub items", +		        note = "added in 21.12", +			module = ?MODULE, function = delete_expired_items, +			args = [], +			result = {res, rescode}, +			result_desc = "0 if command failed, 1 when succeeded",  			result_example = ok}].  -spec mod_opt_type(atom()) -> econf:validator(). @@ -4204,6 +4270,8 @@ mod_opt_type(last_item_cache) ->      econf:bool();  mod_opt_type(max_items_node) ->      econf:non_neg_int(unlimited); +mod_opt_type(max_item_expire_node) -> +    econf:timeout(second, infinity);  mod_opt_type(max_nodes_discoitems) ->      econf:non_neg_int(infinity);  mod_opt_type(max_subscriptions_node) -> @@ -4251,6 +4319,7 @@ mod_options(Host) ->       {ignore_pep_from_offline, true},       {last_item_cache, false},       {max_items_node, ?MAXITEMS}, +     {max_item_expire_node, infinity},       {max_nodes_discoitems, 100},       {nodetree, ?STDTREE},       {pep_mapping, []}, @@ -4329,11 +4398,17 @@ mod_doc() ->  		     " so many nodes, caching last items speeds up pubsub "  		     "and allows to raise user connection rate. The cost "  		     "is memory usage, as every item is stored in memory.")}}, +	   {max_item_expire_node, +	    #{value => "timeout() | infinity", +	      note => "added in 21.12", +	      desc => +		  ?T("Specify the maximum item epiry time. Default value " +		     "is: 'infinity'.")}},  	   {max_items_node,  	    #{value => "non_neg_integer() | infinity",  	      desc =>  		  ?T("Define the maximum number of items that can be " -		     "stored in a node. Default value is: '10'.")}}, +		     "stored in a node. Default value is: '1000'.")}},  	   {max_nodes_discoitems,  	    #{value => "pos_integer() | infinity",  	      desc => diff --git a/src/mod_pubsub_opt.erl b/src/mod_pubsub_opt.erl index 8db5532f6..cb3c014b9 100644 --- a/src/mod_pubsub_opt.erl +++ b/src/mod_pubsub_opt.erl @@ -11,6 +11,7 @@  -export([hosts/1]).  -export([ignore_pep_from_offline/1]).  -export([last_item_cache/1]). +-export([max_item_expire_node/1]).  -export([max_items_node/1]).  -export([max_nodes_discoitems/1]).  -export([max_subscriptions_node/1]). @@ -68,7 +69,13 @@ last_item_cache(Opts) when is_map(Opts) ->  last_item_cache(Host) ->      gen_mod:get_module_opt(Host, mod_pubsub, last_item_cache). --spec max_items_node(gen_mod:opts() | global | binary()) -> non_neg_integer(). +-spec max_item_expire_node(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). +max_item_expire_node(Opts) when is_map(Opts) -> +    gen_mod:get_opt(max_item_expire_node, Opts); +max_item_expire_node(Host) -> +    gen_mod:get_module_opt(Host, mod_pubsub, max_item_expire_node). + +-spec max_items_node(gen_mod:opts() | global | binary()) -> 'unlimited' | non_neg_integer().  max_items_node(Opts) when is_map(Opts) ->      gen_mod:get_opt(max_items_node, Opts);  max_items_node(Host) -> diff --git a/src/mod_push_sql.erl b/src/mod_push_sql.erl index f89b904c2..c024a12d1 100644 --- a/src/mod_push_sql.erl +++ b/src/mod_push_sql.erl @@ -52,7 +52,7 @@ store_session(LUser, LServer, NowTS, PushJID, Node, XData) ->      case ?SQL_UPSERT(LServer, "push_session",  		     ["!username=%(LUser)s",                        "!server_host=%(LServer)s", -		      "!timestamp=%(TS)d", +		      "timestamp=%(TS)d",  		      "!service=%(Service)s",  		      "!node=%(Node)s",  		      "xml=%(XML)s"]) of diff --git a/src/mod_register.erl b/src/mod_register.erl index 379318da6..b85efd57c 100644 --- a/src/mod_register.erl +++ b/src/mod_register.erl @@ -32,11 +32,13 @@  -behaviour(gen_mod).  -export([start/2, stop/1, reload/3, stream_feature_register/2, -	 c2s_unauthenticated_packet/2, try_register/4, +	 c2s_unauthenticated_packet/2, try_register/4, try_register/5,  	 process_iq/1, send_registration_notifications/3,  	 mod_opt_type/1, mod_options/1, depends/2,  	 format_error/1, mod_doc/0]). +-deprecated({try_register, 4}). +  -include("logger.hrl").  -include_lib("xmpp/include/xmpp.hrl").  -include("translate.hrl"). @@ -283,7 +285,7 @@ try_register_or_set_password(User, Server, Password,  	_ when CaptchaSucceed ->  	    case check_from(From, Server) of  		allow -> -		    case try_register(User, Server, Password, Source, Lang) of +		    case try_register(User, Server, Password, Source, ?MODULE, Lang) of  			ok ->  			    xmpp:make_iq_result(IQ);  			{error, Error} -> @@ -328,6 +330,13 @@ try_set_password(User, Server, Password, #iq{lang = Lang, meta = M} = IQ) ->  	    xmpp:make_error(IQ, xmpp:err_internal_server_error(format_error(Why), Lang))      end. +try_register(User, Server, Password, SourceRaw, Module) -> +    Modules = mod_register_opt:allow_modules(Server), +    case (Modules == all) orelse lists:member(Module, Modules) of +        true -> try_register(User, Server, Password, SourceRaw); +        false -> {error, eaccess} +    end. +  try_register(User, Server, Password, SourceRaw) ->      case jid:is_nodename(User) of  	false -> @@ -363,8 +372,8 @@ try_register(User, Server, Password, SourceRaw) ->  	    end      end. -try_register(User, Server, Password, SourceRaw, Lang) -> -    case try_register(User, Server, Password, SourceRaw) of +try_register(User, Server, Password, SourceRaw, Module, Lang) -> +    case try_register(User, Server, Password, SourceRaw, Module) of  	ok ->  	    JID = jid:make(User, Server),  	    Source = may_remove_resource(SourceRaw), @@ -597,6 +606,8 @@ mod_opt_type(access_from) ->      econf:acl();  mod_opt_type(access_remove) ->      econf:acl(); +mod_opt_type(allow_modules) -> +    econf:either(all, econf:list(econf:atom()));  mod_opt_type(captcha_protected) ->      econf:bool();  mod_opt_type(ip_access) -> @@ -623,6 +634,7 @@ mod_options(_Host) ->      [{access, all},       {access_from, none},       {access_remove, all}, +     {allow_modules, all},       {captcha_protected, false},       {ip_access, all},       {password_strength, 0}, @@ -661,6 +673,13 @@ mod_doc() ->                desc =>                    ?T("Specify rules to restrict access for user unregistration. "                       "By default any user is able to unregister their account.")}}, +           {allow_modules, +            #{value => "all | [Module, ...]", +              note => "added in 21.12", +              desc => +                  ?T("List of modules that can register accounts, or 'all'. " +                     "The default value is 'all', which is equivalent to " +                     "something like '[mod_register, mod_register_web]'.")}},             {captcha_protected,              #{value => "true | false",                desc => diff --git a/src/mod_register_opt.erl b/src/mod_register_opt.erl index 53c6ca6ea..e7236424c 100644 --- a/src/mod_register_opt.erl +++ b/src/mod_register_opt.erl @@ -6,6 +6,7 @@  -export([access/1]).  -export([access_from/1]).  -export([access_remove/1]). +-export([allow_modules/1]).  -export([captcha_protected/1]).  -export([ip_access/1]).  -export([password_strength/1]). @@ -31,6 +32,12 @@ access_remove(Opts) when is_map(Opts) ->  access_remove(Host) ->      gen_mod:get_module_opt(Host, mod_register, access_remove). +-spec allow_modules(gen_mod:opts() | global | binary()) -> 'all' | [atom()]. +allow_modules(Opts) when is_map(Opts) -> +    gen_mod:get_opt(allow_modules, Opts); +allow_modules(Host) -> +    gen_mod:get_module_opt(Host, mod_register, allow_modules). +  -spec captcha_protected(gen_mod:opts() | global | binary()) -> boolean().  captcha_protected(Opts) when is_map(Opts) ->      gen_mod:get_opt(captcha_protected, Opts); diff --git a/src/mod_register_web.erl b/src/mod_register_web.erl index 0e216c81c..0cf4bcff8 100644 --- a/src/mod_register_web.erl +++ b/src/mod_register_web.erl @@ -85,7 +85,7 @@ process([Section],  process([<<"new">>],  	#request{method = 'POST', q = Q, ip = {Ip, _Port},  		 lang = Lang, host = _HTTPHost}) -> -    case form_new_post(Q) of +    case form_new_post(Q, Ip) of        {success, ok, {Username, Host, _Password}} ->  	  Jid = jid:make(Username, Host),            mod_register:send_registration_notifications(?MODULE, Jid, Ip), @@ -290,10 +290,10 @@ form_new_get2(Host, Lang, CaptchaEls) ->  %%% Formulary new POST  %%%---------------------------------------------------------------------- -form_new_post(Q) -> +form_new_post(Q, Ip) ->      case catch get_register_parameters(Q) of        [Username, Host, Password, Password, Id, Key] -> -	  form_new_post(Username, Host, Password, {Id, Key}); +	  form_new_post(Username, Host, Password, {Id, Key}, Ip);        [_Username, _Host, _Password, _Password2, false, false] ->  	  {error, passwords_not_identical};        [_Username, _Host, _Password, _Password2, Id, Key] -> @@ -312,13 +312,12 @@ get_register_parameters(Q) ->  	      [<<"username">>, <<"host">>, <<"password">>, <<"password2">>,  	       <<"id">>, <<"key">>]). -form_new_post(Username, Host, Password, -	      {false, false}) -> -    register_account(Username, Host, Password); -form_new_post(Username, Host, Password, {Id, Key}) -> +form_new_post(Username, Host, Password, {false, false}, Ip) -> +    register_account(Username, Host, Password, Ip); +form_new_post(Username, Host, Password, {Id, Key}, Ip) ->      case ejabberd_captcha:check_captcha(Id, Key) of        captcha_valid -> -	  register_account(Username, Host, Password); +	  register_account(Username, Host, Password, Ip);        captcha_non_valid -> {error, captcha_non_valid};        captcha_not_found -> {error, captcha_non_valid}      end. @@ -502,11 +501,11 @@ form_del_get(Host, Lang) ->        {<<"Content-Type">>, <<"text/html">>}],       ejabberd_web:make_xhtml(HeadEls, Els)}. -%% @spec(Username, Host, Password) -> {success, ok, {Username, Host, Password} | +%% @spec(Username, Host, Password, Ip) -> {success, ok, {Username, Host, Password} |  %%                                    {success, exists, {Username, Host, Password}} |  %%                                    {error, not_allowed} |  %%                                    {error, invalid_jid} -register_account(Username, Host, Password) -> +register_account(Username, Host, Password, Ip) ->      try mod_register_opt:access(Host) of  	Access ->  	    case jid:make(Username, Host) of @@ -514,16 +513,15 @@ register_account(Username, Host, Password) ->  		JID ->  		    case acl:match_rule(Host, Access, JID) of  			deny -> {error, not_allowed}; -			allow -> register_account2(Username, Host, Password) +			allow -> register_account2(Username, Host, Password, Ip)  		    end  	    end      catch _:{module_not_loaded, mod_register, _Host} ->  	    {error, host_unknown}      end. -register_account2(Username, Host, Password) -> -    case ejabberd_auth:try_register(Username, Host, -				    Password) +register_account2(Username, Host, Password, Ip) -> +    case mod_register:try_register(Username, Host, Password, Ip, ?MODULE)  	of        ok ->  	  {success, ok, {Username, Host, Password}}; @@ -579,12 +577,8 @@ get_error_text({error, exists}) ->      ?T("The account already exists");  get_error_text({error, password_incorrect}) ->      ?T("Incorrect password"); -get_error_text({error, invalid_jid}) -> -    ?T("The username is not valid");  get_error_text({error, host_unknown}) ->      ?T("Host unknown"); -get_error_text({error, not_allowed}) -> -    ?T("Not allowed");  get_error_text({error, account_doesnt_exist}) ->      ?T("Account doesn't exist");  get_error_text({error, account_exists}) -> @@ -594,7 +588,9 @@ get_error_text({error, password_not_changed}) ->  get_error_text({error, passwords_not_identical}) ->      ?T("The passwords are different");  get_error_text({error, wrong_parameters}) -> -    ?T("Wrong parameters in the web formulary"). +    ?T("Wrong parameters in the web formulary"); +get_error_text({error, Why}) -> +    mod_register:format_error(Why).  mod_options(_) ->      []. diff --git a/src/mod_roster_sql.erl b/src/mod_roster_sql.erl index 76ddb29dd..ebfcde463 100644 --- a/src/mod_roster_sql.erl +++ b/src/mod_roster_sql.erl @@ -80,9 +80,10 @@ get_roster(LUser, LServer) ->  				[]                          end,              GroupsDict = lists:foldl(fun({J, G}, Acc) -> -                                             dict:append(J, G, Acc) +                                             Gs = maps:get(J, Acc, []), +                                             maps:put(J, [G | Gs], Acc)                                       end, -                                     dict:new(), JIDGroups), +                                     maps:new(), JIDGroups),  	    {ok, lists:flatmap(  		   fun(I) ->  			   case raw_to_record(LServer, I) of @@ -90,10 +91,7 @@ get_roster(LUser, LServer) ->  			       error -> [];  			       R ->  				   SJID = jid:encode(R#roster.jid), -				   Groups = case dict:find(SJID, GroupsDict) of -						{ok, Gs} -> Gs; -						error -> [] -					    end, +                                   Groups = maps:get(SJID, GroupsDict, []),  				   [R#roster{groups = Groups}]  			   end  		   end, Items)}; diff --git a/src/mod_shared_roster.erl b/src/mod_shared_roster.erl index 13ff90466..358a8df32 100644 --- a/src/mod_shared_roster.erl +++ b/src/mod_shared_roster.erl @@ -870,12 +870,15 @@ c2s_self_presence(Acc) ->      Acc.  -spec unset_presence(binary(), binary(), binary(), binary()) -> ok. -unset_presence(LUser, LServer, Resource, Status) -> +unset_presence(User, Server, Resource, Status) -> +    LUser = jid:nodeprep(User), +    LServer = jid:nameprep(Server), +    LResource = jid:resourceprep(Resource),      Resources = ejabberd_sm:get_user_resources(LUser,  					       LServer),      ?DEBUG("Unset_presence for ~p @ ~p / ~p -> ~p "  	   "(~p resources)", -	   [LUser, LServer, Resource, Status, length(Resources)]), +	   [LUser, LServer, LResource, Status, length(Resources)]),      case length(Resources) of        0 ->  	  lists:foreach( diff --git a/src/mod_shared_roster_ldap.erl b/src/mod_shared_roster_ldap.erl index 08fbe8793..e842ab261 100644 --- a/src/mod_shared_roster_ldap.erl +++ b/src/mod_shared_roster_ldap.erl @@ -689,9 +689,9 @@ mod_doc() ->             ?T("- Connection parameters: The module also accepts the "  	      "connection parameters, all of which default to the top-level "  	      "parameter of the same name, if unspecified. " -	      "See http://../database-ldap/#ldap-connection[LDAP Connection] " +	      "See http://../ldap/#ldap-connection[LDAP Connection] "  	      "section for more information about them."), "", -           ?T("Check also the http://../database-ldap/#configuration-examples" +           ?T("Check also the http://../ldap/#ldap-examples"  	      "[Configuration examples] section to get details about "  	      "retrieving the roster, "  	      "and configuration examples including Flat DIT and Deep DIT.")], @@ -721,13 +721,13 @@ mod_doc() ->  		     "name of roster entries (usually full names of people in "  		     "the roster). See also the parameters 'ldap_userdesc' and "  		     "'ldap_useruid'. For more information check the LDAP " -		     "http://../database-ldap/#filters[Filters] section.")}}, +		     "http://../ldap/#filters[Filters] section.")}},             {ldap_filter,              #{desc =>  		  ?T("Additional filter which is AND-ed together "  		     "with \"User Filter\" and \"Group Filter\". "  		     "For more information check the LDAP " -		     "http://../database-ldap/#filters[Filters] section.")}}, +		     "http://../ldap/#filters[Filters] section.")}},  	   %% Attributes:             {ldap_groupattr,              #{desc => @@ -785,7 +785,7 @@ mod_doc() ->              #{desc =>  		  ?T("A regex for extracting user ID from the value of the "  		     "attribute named by 'ldap_memberattr'. Check the LDAP " -		     "http://../database-ldap/#control-parameters" +		     "http://../ldap/#control-parameters"  		     "[Control Parameters] section.")}},             {ldap_auth_check,              #{value => "true | false", diff --git a/src/mod_stun_disco.erl b/src/mod_stun_disco.erl index 6e7592453..cbb671639 100644 --- a/src/mod_stun_disco.erl +++ b/src/mod_stun_disco.erl @@ -646,7 +646,7 @@ get_listener_ips(#{ip := {0, 0, 0, 0, 0, 0, 0, 1}} = Opts) ->      {undefined, get_turn_ipv6_addr(Opts)};  get_listener_ips(#{ip := {_, _, _, _} = IP}) ->      {IP, undefined}; -get_listener_ips(#{ip := {_, _, _, _, _,_, _, _, _} = IP}) -> +get_listener_ips(#{ip := {_, _, _, _, _, _, _, _} = IP}) ->      {undefined, IP}.  -spec get_turn_ipv4_addr(map()) -> inet:ip4_address() | undefined. diff --git a/src/node_flat.erl b/src/node_flat.erl index c597b9ce9..55dea0d8d 100644 --- a/src/node_flat.erl +++ b/src/node_flat.erl @@ -40,7 +40,7 @@      create_node_permission/6, create_node/2, delete_node/1,      purge_node/2, subscribe_node/8, unsubscribe_node/4,      publish_item/7, delete_item/4, -    remove_extra_items/2, remove_extra_items/3, +    remove_extra_items/2, remove_extra_items/3, remove_expired_items/2,      get_entity_affiliations/2, get_node_affiliations/1,      get_affiliation/2, set_affiliation/3,      get_entity_subscriptions/2, get_node_subscriptions/1, @@ -432,6 +432,22 @@ remove_extra_items(Nidx, MaxItems, ItemIds) ->      del_items(Nidx, OldItems),      {result, {NewItems, OldItems}}. +remove_expired_items(_Nidx, infinity) -> +    {result, []}; +remove_expired_items(Nidx, Seconds) -> +    Items = mnesia:index_read(pubsub_item, Nidx, #pubsub_item.nodeidx), +    ExpT = misc:usec_to_now( +	     erlang:system_time(microsecond) - (Seconds * 1000000)), +    ExpItems = lists:filtermap( +		 fun(#pubsub_item{itemid = {ItemId, _}, +				  modification = {ModT, _}}) when ModT < ExpT -> +			 {true, ItemId}; +		    (#pubsub_item{}) -> +			 false +		 end, Items), +    del_items(Nidx, ExpItems), +    {result, ExpItems}. +  %% @doc <p>Triggers item deletion.</p>  %% <p>Default plugin: The user performing the deletion must be the node owner  %% or a publisher, or PublishModel being open.</p> diff --git a/src/node_flat_sql.erl b/src/node_flat_sql.erl index 240dc3760..f9c8a209d 100644 --- a/src/node_flat_sql.erl +++ b/src/node_flat_sql.erl @@ -43,7 +43,7 @@      create_node_permission/6, create_node/2, delete_node/1, purge_node/2,      subscribe_node/8, unsubscribe_node/4,      publish_item/7, delete_item/4, -    remove_extra_items/2, remove_extra_items/3, +    remove_extra_items/2, remove_extra_items/3, remove_expired_items/2,      get_entity_affiliations/2, get_node_affiliations/1,      get_affiliation/2, set_affiliation/3,      get_entity_subscriptions/2, get_node_subscriptions/1, @@ -285,6 +285,23 @@ remove_extra_items(Nidx, MaxItems, ItemIds) ->      del_items(Nidx, OldItems),      {result, {NewItems, OldItems}}. +remove_expired_items(_Nidx, infinity) -> +    {result, []}; +remove_expired_items(Nidx, Seconds) -> +    ExpT = encode_now( +	     misc:usec_to_now( +	       erlang:system_time(microsecond) - (Seconds * 1000000))), +    case ejabberd_sql:sql_query_t( +	   ?SQL("select @(itemid)s from pubsub_item where nodeid=%(Nidx)d " +		"and creation < %(ExpT)s")) of +	{selected, RItems} -> +	    ItemIds = [ItemId || {ItemId} <- RItems], +	    del_items(Nidx, ItemIds), +	    {result, ItemIds}; +	_ -> +	    {result, []} +    end. +  delete_item(Nidx, Publisher, PublishModel, ItemId) ->      SubKey = jid:tolower(Publisher),      GenKey = jid:remove_resource(SubKey), diff --git a/src/node_pep.erl b/src/node_pep.erl index 44388ca31..66431b948 100644 --- a/src/node_pep.erl +++ b/src/node_pep.erl @@ -36,7 +36,7 @@      create_node_permission/6, create_node/2, delete_node/1,      purge_node/2, subscribe_node/8, unsubscribe_node/4,      publish_item/7, delete_item/4, -    remove_extra_items/2, remove_extra_items/3, +    remove_extra_items/2, remove_extra_items/3, remove_expired_items/2,      get_entity_affiliations/2, get_node_affiliations/1,      get_affiliation/2, set_affiliation/3,      get_entity_subscriptions/2, get_node_subscriptions/1, @@ -81,10 +81,12 @@ features() ->      [<<"create-nodes">>,  	<<"auto-create">>,  	<<"auto-subscribe">>, +	<<"config-node">>,  	<<"delete-nodes">>,  	<<"delete-items">>,  	<<"filtered-notifications">>,  	<<"modify-affiliations">>, +	<<"multi-items">>,  	<<"outcast-affiliation">>,  	<<"persistent-items">>,  	<<"publish">>, @@ -142,6 +144,9 @@ remove_extra_items(Nidx, MaxItems) ->  remove_extra_items(Nidx, MaxItems, ItemIds) ->      node_flat:remove_extra_items(Nidx, MaxItems, ItemIds). +remove_expired_items(Nidx, Seconds) -> +    node_flat:remove_expired_items(Nidx, Seconds). +  delete_item(Nidx, Publisher, PublishModel, ItemId) ->      node_flat:delete_item(Nidx, Publisher, PublishModel, ItemId). diff --git a/src/node_pep_sql.erl b/src/node_pep_sql.erl index c0cf2b166..3bb66bc4c 100644 --- a/src/node_pep_sql.erl +++ b/src/node_pep_sql.erl @@ -38,7 +38,7 @@      create_node_permission/6, create_node/2, delete_node/1,      purge_node/2, subscribe_node/8, unsubscribe_node/4,      publish_item/7, delete_item/4, -    remove_extra_items/2, remove_extra_items/3, +    remove_extra_items/2, remove_extra_items/3, remove_expired_items/2,      get_entity_affiliations/2, get_node_affiliations/1,      get_affiliation/2, set_affiliation/3,      get_entity_subscriptions/2, get_node_subscriptions/1, @@ -99,6 +99,9 @@ remove_extra_items(Nidx, MaxItems) ->  remove_extra_items(Nidx, MaxItems, ItemIds) ->      node_flat_sql:remove_extra_items(Nidx, MaxItems, ItemIds). +remove_expired_items(Nidx, Seconds) -> +    node_flat_sql:remove_expired_items(Nidx, Seconds). +  delete_item(Nidx, Publisher, PublishModel, ItemId) ->      node_flat_sql:delete_item(Nidx, Publisher, PublishModel, ItemId). diff --git a/src/prosody2ejabberd.erl b/src/prosody2ejabberd.erl index 3992a4034..8f5c35f84 100644 --- a/src/prosody2ejabberd.erl +++ b/src/prosody2ejabberd.erl @@ -118,7 +118,7 @@ eval_file(Path) ->  	    case luerl:eval(NewData, State1) of  		{ok, _} = Res ->  		    Res; -		{error, Why} = Err -> +		{error, Why, _} = Err ->  		    ?ERROR_MSG("Failed to eval ~ts: ~p", [Path, Why]),  		    Err  	    end; diff --git a/src/rest.erl b/src/rest.erl index d724352f2..1bb5c5ef7 100644 --- a/src/rest.erl +++ b/src/rest.erl @@ -191,13 +191,26 @@ base_url(Server, Path) ->          _ -> Url      end. +-ifdef(HAVE_URI_STRING). +uri_hack(Str) -> +    case uri_string:normalize("%25") of +        "%" -> % This hack around bug in httpc >21 <23.2 +            binary:replace(Str, <<"%25">>, <<"%2525">>, [global]); +        _ -> Str +    end. +-else. +uri_hack(Str) -> +    Str. +-endif. +  url(Url, []) ->      Url;  url(Url, Params) ->      L = [<<"&", (iolist_to_binary(Key))/binary, "=",            (misc:url_encode(Value))/binary>>              || {Key, Value} <- Params], -    <<$&, Encoded/binary>> = iolist_to_binary(L), +    <<$&, Encoded0/binary>> = iolist_to_binary(L), +    Encoded = uri_hack(Encoded0),      <<Url/binary, $?, Encoded/binary>>.  url(Server, Path, Params) ->      case binary:split(base_url(Server, Path), <<"?">>) of | 
