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 |