diff --git a/include/mod_muc_room.hrl b/include/mod_muc_room.hrl index aea5432e6..b224192c8 100644 --- a/include/mod_muc_room.hrl +++ b/include/mod_muc_room.hrl @@ -63,6 +63,7 @@ max_users = ?MAX_USERS_DEFAULT :: non_neg_integer() | none, logging = false :: boolean(), vcard = <<"">> :: binary(), + vcard_xupdate = undefined :: undefined | external | binary(), captcha_whitelist = (?SETS):empty() :: ?TGB_SET, mam = false :: boolean(), pubsub = <<"">> :: binary() diff --git a/src/mod_caps.erl b/src/mod_caps.erl index 995e8b487..19838cc8a 100644 --- a/src/mod_caps.erl +++ b/src/mod_caps.erl @@ -38,7 +38,8 @@ -export([read_caps/1, list_features/1, caps_stream_features/2, disco_features/5, disco_identity/5, disco_info/5, get_features/2, export/1, import_info/0, import/5, - get_user_caps/2, import_start/2, import_stop/2]). + get_user_caps/2, import_start/2, import_stop/2, + compute_disco_hash/2, is_valid_node/1]). %% gen_mod callbacks -export([start/2, stop/1, reload/3, depends/2]). @@ -412,13 +413,13 @@ make_my_disco_hash(Host) -> DiscoInfo = #disco_info{identities = Identities, features = Feats, xdata = Info}, - make_disco_hash(DiscoInfo, sha); + compute_disco_hash(DiscoInfo, sha); _Err -> <<"">> end. -type digest_type() :: md5 | sha | sha224 | sha256 | sha384 | sha512. --spec make_disco_hash(disco_info(), digest_type()) -> binary(). -make_disco_hash(DiscoInfo, Algo) -> +-spec compute_disco_hash(disco_info(), digest_type()) -> binary(). +compute_disco_hash(DiscoInfo, Algo) -> Concat = list_to_binary([concat_identities(DiscoInfo), concat_features(DiscoInfo), concat_info(DiscoInfo)]), base64:encode(case Algo of @@ -434,17 +435,17 @@ make_disco_hash(DiscoInfo, Algo) -> check_hash(Caps, DiscoInfo) -> case Caps#caps.hash of <<"md5">> -> - Caps#caps.version == make_disco_hash(DiscoInfo, md5); + Caps#caps.version == compute_disco_hash(DiscoInfo, md5); <<"sha-1">> -> - Caps#caps.version == make_disco_hash(DiscoInfo, sha); + Caps#caps.version == compute_disco_hash(DiscoInfo, sha); <<"sha-224">> -> - Caps#caps.version == make_disco_hash(DiscoInfo, sha224); + Caps#caps.version == compute_disco_hash(DiscoInfo, sha224); <<"sha-256">> -> - Caps#caps.version == make_disco_hash(DiscoInfo, sha256); + Caps#caps.version == compute_disco_hash(DiscoInfo, sha256); <<"sha-384">> -> - Caps#caps.version == make_disco_hash(DiscoInfo, sha384); + Caps#caps.version == compute_disco_hash(DiscoInfo, sha384); <<"sha-512">> -> - Caps#caps.version == make_disco_hash(DiscoInfo, sha512); + Caps#caps.version == compute_disco_hash(DiscoInfo, sha512); _ -> true end. diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl index 87ae28797..158e937c7 100644 --- a/src/mod_muc_room.erl +++ b/src/mod_muc_room.erl @@ -290,7 +290,7 @@ normal_state({route, <<"">>, process_iq_admin(From, IQ, StateData); ?NS_MUC_OWNER -> process_iq_owner(From, IQ, StateData); - ?NS_DISCO_INFO when SubEl#disco_info.node == <<>> -> + ?NS_DISCO_INFO -> process_iq_disco_info(From, IQ, StateData); ?NS_DISCO_ITEMS -> process_iq_disco_items(From, IQ, StateData); @@ -2068,12 +2068,30 @@ presence_broadcast_allowed(JID, StateData) -> -spec send_initial_presences_and_messages( jid(), binary(), presence(), state(), state()) -> ok. send_initial_presences_and_messages(From, Nick, Presence, NewState, OldState) -> + send_self_presence(From, NewState), send_existing_presences(From, NewState), send_initial_presence(From, NewState, OldState), History = get_history(Nick, Presence, NewState), send_history(From, History, NewState), send_subject(From, OldState). +-spec send_self_presence(jid(), state()) -> ok. +send_self_presence(JID, State) -> + AvatarHash = (State#state.config)#config.vcard_xupdate, + DiscoInfo = make_disco_info(JID, State), + DiscoHash = mod_caps:compute_disco_hash(DiscoInfo, sha), + Els1 = [#caps{hash = <<"sha-1">>, + node = ?EJABBERD_URI, + version = DiscoHash}], + Els2 = if is_binary(AvatarHash) -> + [#vcard_xupdate{hash = AvatarHash}|Els1]; + true -> + Els1 + end, + ejabberd_router:route(#presence{from = State#state.jid, to = JID, + id = randoms:get_string(), + sub_els = Els2}). + -spec send_initial_presence(jid(), state(), state()) -> ok. send_initial_presence(NJID, StateData, OldStateData) -> send_new_presence1(NJID, <<"">>, true, StateData, OldStateData). @@ -3344,12 +3362,15 @@ send_config_change_info(New, #state{config = Old} = StateData) -> end ++ case Old#config{anonymous = New#config.anonymous, - vcard = New#config.vcard, logging = New#config.logging} of New -> []; _ -> [104] end, if Codes /= [] -> + lists:foreach( + fun({_LJID, #user{jid = JID}}) -> + send_self_presence(JID, StateData#state{config = New}) + end, ?DICT:to_list(StateData#state.users)), Message = #message{type = groupchat, id = randoms:get_string(), sub_els = [#muc_user{status_codes = Codes}]}, @@ -3377,7 +3398,8 @@ remove_nonmembers(StateData) -> StateData, (?DICT):to_list(get_users_and_subscribers(StateData))). -spec set_opts([{atom(), any()}], state()) -> state(). -set_opts([], StateData) -> StateData; +set_opts([], StateData) -> + set_vcard_xupdate(StateData); set_opts([{Opt, Val} | Opts], StateData) -> NSD = case Opt of title -> @@ -3492,6 +3514,10 @@ set_opts([{Opt, Val} | Opts], StateData) -> StateData#state{config = (StateData#state.config)#config{vcard = Val}}; + vcard_xupdate -> + StateData#state{config = + (StateData#state.config)#config{vcard_xupdate = + Val}}; pubsub -> StateData#state{config = (StateData#state.config)#config{pubsub = Val}}; @@ -3526,6 +3552,20 @@ set_opts([{Opt, Val} | Opts], StateData) -> end, set_opts(Opts, NSD). +set_vcard_xupdate(#state{config = + #config{vcard = VCardRaw, + vcard_xupdate = undefined} = Config} = State) + when VCardRaw /= <<"">> -> + case fxml_stream:parse_element(VCardRaw) of + {error, _} -> + State; + El -> + Hash = mod_vcard_xupdate:compute_hash(El), + State#state{config = Config#config{vcard_xupdate = Hash}} + end; +set_vcard_xupdate(State) -> + State. + -define(MAKE_CONFIG_OPT(Opt), {get_config_opt_name(Opt), element(Opt, Config)}). @@ -3561,6 +3601,7 @@ make_opts(StateData) -> ?MAKE_CONFIG_OPT(#config.presence_broadcast), ?MAKE_CONFIG_OPT(#config.voice_request_min_interval), ?MAKE_CONFIG_OPT(#config.vcard), + ?MAKE_CONFIG_OPT(#config.vcard_xupdate), ?MAKE_CONFIG_OPT(#config.pubsub), {captcha_whitelist, (?SETS):to_list((StateData#state.config)#config.captcha_whitelist)}, @@ -3633,12 +3674,8 @@ destroy_room(DEl, StateData) -> false -> Fiffalse end). --spec process_iq_disco_info(jid(), iq(), state()) -> - {result, disco_info()} | {error, stanza_error()}. -process_iq_disco_info(_From, #iq{type = set, lang = Lang}, _StateData) -> - Txt = <<"Value 'set' of 'type' attribute is not allowed">>, - {error, xmpp:err_not_allowed(Txt, Lang)}; -process_iq_disco_info(_From, #iq{type = get, lang = Lang}, StateData) -> +-spec make_disco_info(jid(), state()) -> disco_info(). +make_disco_info(_From, StateData) -> Config = StateData#state.config, Feats = [?NS_VCARD, ?NS_MUC, ?CONFIG_OPT_TO_FEATURE((Config#config.public), @@ -3664,11 +3701,35 @@ process_iq_disco_info(_From, #iq{type = get, lang = Lang}, StateData) -> _ -> [] end, - {result, #disco_info{xdata = [iq_disco_info_extras(Lang, StateData)], - identities = [#identity{category = <<"conference">>, - type = <<"text">>, - name = get_title(StateData)}], - features = Feats}}. + #disco_info{identities = [#identity{category = <<"conference">>, + type = <<"text">>, + name = get_title(StateData)}], + features = Feats}. + +-spec process_iq_disco_info(jid(), iq(), state()) -> + {result, disco_info()} | {error, stanza_error()}. +process_iq_disco_info(_From, #iq{type = set, lang = Lang}, _StateData) -> + Txt = <<"Value 'set' of 'type' attribute is not allowed">>, + {error, xmpp:err_not_allowed(Txt, Lang)}; +process_iq_disco_info(From, #iq{type = get, lang = Lang, + sub_els = [#disco_info{node = <<>>}]}, + StateData) -> + DiscoInfo = make_disco_info(From, StateData), + Extras = iq_disco_info_extras(Lang, StateData), + {result, DiscoInfo#disco_info{xdata = [Extras]}}; +process_iq_disco_info(From, #iq{type = get, lang = Lang, + sub_els = [#disco_info{node = Node}]}, + StateData) -> + try + true = mod_caps:is_valid_node(Node), + DiscoInfo = make_disco_info(From, StateData), + Hash = mod_caps:compute_disco_hash(DiscoInfo, sha), + Node = <<(?EJABBERD_URI)/binary, $#, Hash/binary>>, + {result, DiscoInfo#disco_info{node = Node}} + catch _:{badmatch, _} -> + Txt = <<"Invalid node name">>, + {error, xmpp:err_item_not_found(Txt, Lang)} + end. -spec iq_disco_info_extras(binary(), state()) -> xdata(). iq_disco_info_extras(Lang, StateData) -> @@ -3734,13 +3795,15 @@ process_iq_vcard(_From, #iq{type = get}, StateData) -> {error, _} -> {error, xmpp:err_item_not_found()} end; -process_iq_vcard(From, #iq{type = set, lang = Lang, sub_els = [SubEl]}, +process_iq_vcard(From, #iq{type = set, lang = Lang, sub_els = [Pkt]}, StateData) -> case get_affiliation(From, StateData) of owner -> - VCardRaw = fxml:element_to_binary(xmpp:encode(SubEl)), + SubEl = xmpp:encode(Pkt), + VCardRaw = fxml:element_to_binary(SubEl), + Hash = mod_vcard_xupdate:compute_hash(SubEl), Config = StateData#state.config, - NewConfig = Config#config{vcard = VCardRaw}, + NewConfig = Config#config{vcard = VCardRaw, vcard_xupdate = Hash}, change_config(NewConfig, StateData); _ -> ErrText = <<"Owner privileges required">>, @@ -4162,6 +4225,28 @@ send_wrapped(From, To, Packet, Node, State) -> ok end; true -> + case Packet of + #presence{type = unavailable} -> + case xmpp:get_subtag(Packet, #muc_user{}) of + #muc_user{destroy = Destroy, + status_codes = Codes} -> + case Destroy /= undefined orelse + (lists:member(110,Codes) andalso + not lists:member(303, Codes)) of + true -> + ejabberd_router:route( + #presence{from = State#state.jid, to = To, + id = randoms:get_string(), + type = unavailable}); + false -> + ok + end; + _ -> + false + end; + _ -> + ok + end, ejabberd_router:route(xmpp:set_from_to(Packet, From, To)) end. diff --git a/src/mod_vcard_xupdate.erl b/src/mod_vcard_xupdate.erl index 867e5ae25..5c10a5a7e 100644 --- a/src/mod_vcard_xupdate.erl +++ b/src/mod_vcard_xupdate.erl @@ -33,6 +33,8 @@ -export([update_presence/1, vcard_set/1, remove_user/2, user_send_packet/1, mod_opt_type/1, mod_options/1, depends/2]). +%% API +-export([compute_hash/1]). -include("ejabberd.hrl"). -include("logger.hrl"). diff --git a/test/muc_tests.erl b/test/muc_tests.erl index bcceb6938..ae6e2e9a7 100644 --- a/test/muc_tests.erl +++ b/test/muc_tests.erl @@ -800,6 +800,11 @@ change_affiliation_slave(Config, {Aff, Role, Status, Reason}) -> MyNick = ?config(nick, Config), MyNickJID = jid:replace_resource(Room, MyNick), ct:comment("Receiving affiliation change to ~s", [Aff]), + if Aff == outcast -> + #presence{from = Room, type = unavailable} = recv_presence(Config); + true -> + ok + end, #muc_user{status_codes = Codes, items = [#muc_item{role = Role, actor = Actor, @@ -858,6 +863,7 @@ kick_slave(Config) -> wait_for_master(Config), {[], _, _} = join(Config), ct:comment("Receiving role change to 'none'"), + #presence{from = Room, type = unavailable} = recv_presence(Config), #muc_user{status_codes = Codes, items = [#muc_item{role = none, affiliation = none, @@ -889,6 +895,7 @@ destroy_master(Config) -> wait_for_slave(Config), ok = destroy(Config, Reason), ct:comment("Receiving destruction presence"), + #presence{from = Room, type = unavailable} = recv_presence(Config), #muc_user{items = [#muc_item{role = none, affiliation = none}], destroy = #muc_destroy{jid = AltRoom, @@ -907,6 +914,7 @@ destroy_slave(Config) -> #stanza_error{reason = 'forbidden'} = destroy(Config, Reason), wait_for_master(Config), ct:comment("Receiving destruction presence"), + #presence{from = Room, type = unavailable} = recv_presence(Config), #muc_user{items = [#muc_item{role = none, affiliation = none}], destroy = #muc_destroy{jid = AltRoom, @@ -938,6 +946,7 @@ vcard_master(Config) -> vcard_slave(Config) -> wait_for_master(Config), {[], _, _} = join(Config), + [104] = recv_config_change_message(Config), VCard = get_event(Config), VCard = get_vcard(Config), #stanza_error{reason = 'forbidden'} = set_vcard(Config, VCard), @@ -1150,11 +1159,13 @@ config_members_only_master(Config) -> disconnect(Config). config_members_only_slave(Config) -> + Room = muc_room_jid(Config), MyJID = my_jid(Config), MyNickJID = my_muc_jid(Config), {[], _, _} = slave_join(Config), [104] = recv_config_change_message(Config), ct:comment("Getting kicked because the room has become members-only"), + #presence{from = Room, type = unavailable} = recv_presence(Config), #muc_user{status_codes = Codes, items = [#muc_item{jid = MyJID, role = none, @@ -1171,6 +1182,7 @@ config_members_only_slave(Config) -> ct:comment("Waiting for the peer to ask for join"), join = get_event(Config), {[], _, _} = join(Config, participant, member), + #presence{from = Room, type = unavailable} = recv_presence(Config), #muc_user{status_codes = NewCodes, items = [#muc_item{jid = MyJID, role = none, @@ -1555,8 +1567,9 @@ join_new(Config, Room) -> MyJID = my_jid(Config), MyNick = ?config(nick, Config), MyNickJID = jid:replace_resource(Room, MyNick), - ct:comment("Joining new room"), + ct:comment("Joining new room ~p", [Room]), send(Config, #presence{to = MyNickJID, sub_els = [#muc{}]}), + #presence{from = Room, type = available} = recv_presence(Config), %% As per XEP-0045 we MUST receive stanzas in the following order: %% 1. In-room presence from other occupants %% 2. In-room presence from the joining entity itself (so-called "self-presence") @@ -1625,30 +1638,33 @@ join(Config, Role, Aff, SubEl) -> case recv_presence(Config) of #presence{type = error, from = MyNickJID} = Err -> xmpp:get_subtag(Err, #stanza_error{}); - #presence{type = available, from = PeerNickJID} = Pres -> - #muc_user{items = [#muc_item{role = moderator, - affiliation = owner}]} = - xmpp:get_subtag(Pres, #muc_user{}), - ct:comment("Receiving initial self-presence"), - #muc_user{status_codes = Codes, - items = [#muc_item{role = Role, - jid = MyJID, - affiliation = Aff}]} = - recv_muc_presence(Config, MyNickJID, available), - ct:comment("Checking if code '110' (self-presence) is set"), - true = lists:member(110, Codes), - {History, Subj} = recv_history_and_subject(Config), - {History, Subj, Codes}; - #presence{type = available, from = MyNickJID} = Pres -> - #muc_user{status_codes = Codes, - items = [#muc_item{role = Role, - jid = MyJID, - affiliation = Aff}]} = - xmpp:get_subtag(Pres, #muc_user{}), - ct:comment("Checking if code '110' (self-presence) is set"), - true = lists:member(110, Codes), - {History, Subj} = recv_history_and_subject(Config), - {empty, History, Subj, Codes} + #presence{from = Room, type = available} -> + case recv_presence(Config) of + #presence{type = available, from = PeerNickJID} = Pres -> + #muc_user{items = [#muc_item{role = moderator, + affiliation = owner}]} = + xmpp:get_subtag(Pres, #muc_user{}), + ct:comment("Receiving initial self-presence"), + #muc_user{status_codes = Codes, + items = [#muc_item{role = Role, + jid = MyJID, + affiliation = Aff}]} = + recv_muc_presence(Config, MyNickJID, available), + ct:comment("Checking if code '110' (self-presence) is set"), + true = lists:member(110, Codes), + {History, Subj} = recv_history_and_subject(Config), + {History, Subj, Codes}; + #presence{type = available, from = MyNickJID} = Pres -> + #muc_user{status_codes = Codes, + items = [#muc_item{role = Role, + jid = MyJID, + affiliation = Aff}]} = + xmpp:get_subtag(Pres, #muc_user{}), + ct:comment("Checking if code '110' (self-presence) is set"), + true = lists:member(110, Codes), + {History, Subj} = recv_history_and_subject(Config), + {empty, History, Subj, Codes} + end end. leave(Config) -> @@ -1667,6 +1683,7 @@ leave(Config, Room) -> end, ct:comment("Leaving the room"), send(Config, #presence{to = MyNickJID, type = unavailable}), + #presence{from = Room, type = unavailable} = recv_presence(Config), #muc_user{ status_codes = Codes, items = [#muc_item{role = none, jid = MyJID}]} = @@ -1702,6 +1719,7 @@ set_config(Config, RoomConfig, Room) -> sub_els = [#muc_owner{config = #xdata{type = submit, fields = Fs}}]}) of #iq{type = result, sub_els = []} -> + #presence{from = Room, type = available} = recv_presence(Config), #message{from = Room, type = groupchat} = Msg = recv_message(Config), #muc_user{status_codes = Codes} = xmpp:get_subtag(Msg, #muc_user{}), lists:sort(Codes); @@ -1846,6 +1864,7 @@ set_vcard(Config, VCard) -> case send_recv(Config, #iq{type = set, to = Room, sub_els = [VCard]}) of #iq{type = result, sub_els = []} -> + [104] = recv_config_change_message(Config), ok; #iq{type = error} = Err -> xmpp:get_subtag(Err, #stanza_error{}) @@ -1865,6 +1884,7 @@ get_vcard(Config) -> recv_config_change_message(Config) -> ct:comment("Receiving configuration change notification message"), Room = muc_room_jid(Config), + #presence{from = Room, type = available} = recv_presence(Config), #message{type = groupchat, from = Room} = Msg = recv_message(Config), #muc_user{status_codes = Codes} = xmpp:get_subtag(Msg, #muc_user{}), lists:sort(Codes).