From 632c05e7000a2fb12d542bd477b88c5d72860534 Mon Sep 17 00:00:00 2001 From: Badlop Date: Wed, 12 Nov 2014 17:47:30 +0100 Subject: [PATCH] Update ejabberd core files to ejabberd 14.07 --- mod_multicast/src/ejabberd.app | 81 - mod_multicast/src/ejabberd_c2s.erl | 3992 ++++++++++++------ mod_multicast/src/ejabberd_sup.erl | 43 +- mod_multicast/src/mod_muc_room.erl | 6271 +++++++++++++++++----------- 4 files changed, 6540 insertions(+), 3847 deletions(-) delete mode 100644 mod_multicast/src/ejabberd.app diff --git a/mod_multicast/src/ejabberd.app b/mod_multicast/src/ejabberd.app deleted file mode 100644 index cdc44ca..0000000 --- a/mod_multicast/src/ejabberd.app +++ /dev/null @@ -1,81 +0,0 @@ -% $Id: ejabberd.app 733 2007-02-19 15:02:28Z mremond $ - -{application, ejabberd, - [{description, "ejabberd"}, - {vsn, "1.1.3"}, - {modules, [acl, - configure, - cyrsasl, - cyrsasl_digest, - cyrsasl_plain, - ejabberd, - ejabberd_app, - ejabberd_auth, - ejabberd_c2s, - ejabberd_config, - ejabberd_listener, - ejabberd_logger_h, - ejabberd_local, - ejabberd_router, - ejabberd_router_multicast, - ejabberd_s2s, - ejabberd_s2s_in, - ejabberd_s2s_out, - ejabberd_service, - ejabberd_sm, - ejabberd_sup, - ejabberd_tmp_sup, - gen_iq_handler, - gen_mod, - jd2ejd, - jlib, - mod_configure, - mod_disco, - mod_echo, - mod_last, - mod_offline, - mod_private, - mod_register, - mod_roster, - mod_stats, - mod_time, - mod_vcard, - mod_version, - randoms, - sha, - shaper, - translate, - xml, - xml_stream - ]}, - {registered, [ejabberd, - ejabberd_sup, - ejabberd_auth, - ejabberd_router, - ejabberd_router_multicast, - ejabberd_sm, - ejabberd_s2s, - ejabberd_local, - ejabberd_listeners, - ejabberd_iq_sup, - ejabberd_service_sup, - ejabberd_s2s_out_sup, - ejabberd_s2s_in_sup, - ejabberd_c2s_sup, - ejabberd_mod_roster, - ejabberd_mod_echo, - ejabberd_mod_pubsub, - ejabberd_mod_irc, - ejabberd_mod_muc, - ejabberd_offline, - random_generator - ]}, - {applications, [kernel, stdlib]}, - {env, []}, - {mod, {ejabberd_app, []}}]}. - - - -% Local Variables: -% mode: erlang -% End: diff --git a/mod_multicast/src/ejabberd_c2s.erl b/mod_multicast/src/ejabberd_c2s.erl index d0daf36..72ebdb5 100644 --- a/mod_multicast/src/ejabberd_c2s.erl +++ b/mod_multicast/src/ejabberd_c2s.erl @@ -1,24 +1,53 @@ %%%---------------------------------------------------------------------- %%% File : ejabberd_c2s.erl -%%% Author : Alexey Shchepin +%%% Author : Alexey Shchepin %%% Purpose : Serve C2S connection -%%% Created : 16 Nov 2002 by Alexey Shchepin -%%% Id : $Id: ejabberd_c2s.erl 952 2009-05-06 17:29:39Z badlop $ +%%% Created : 16 Nov 2002 by Alexey Shchepin +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2014 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(ejabberd_c2s). --author('alexey@sevcom.net'). + +-author('alexey@process-one.net'). + -update_info({update, 0}). --behaviour(gen_fsm). +-define(GEN_FSM, p1_fsm). + +-behaviour(?GEN_FSM). %% External exports -export([start/2, + stop/1, start_link/2, send_text/2, send_element/2, socket_type/0, - get_presence/1]). + get_presence/1, + get_aux_field/2, + set_aux_field/3, + del_aux_field/2, + get_subscription/2, + broadcast/4, + get_subscribed/1, + transform_listen_option/2]). %% gen_fsm callbacks -export([init/1, @@ -28,22 +57,32 @@ wait_for_bind/2, wait_for_session/2, wait_for_sasl_response/2, + wait_for_resume/2, session_established/2, handle_event/3, handle_sync_event/4, code_change/4, handle_info/3, - terminate/3]). + terminate/3, + print_state/1 + ]). -include("ejabberd.hrl"). --include("jlib.hrl"). -include("logger.hrl"). --define(SETS, gb_sets). +-include("jlib.hrl"). +-include("mod_privacy.hrl"). + +-define(SETS, gb_sets). +-define(DICT, dict). + +%% pres_a contains all the presence available send (either through roster mechanism or directed). +%% Directed presence unavailable remove user from pres_a. -record(state, {socket, sockmod, socket_monitor, + xml_socket, streamid, sasl_state, access, @@ -55,31 +94,47 @@ tls_options = [], authenticated = false, jid, - user = "", server = ?MYNAME, resource = "", + user = "", server = <<"">>, resource = <<"">>, sid, pres_t = ?SETS:new(), pres_f = ?SETS:new(), pres_a = ?SETS:new(), - pres_i = ?SETS:new(), pres_last, pres_pri, pres_timestamp, - pres_invis = false, - privacy_list = none, + privacy_list = #userlist{}, + conn = unknown, + auth_module = unknown, ip, - lang}). + aux_fields = [], + csi_state = active, + csi_queue = [], + mgmt_state, + mgmt_xmlns, + mgmt_queue, + mgmt_max_queue, + mgmt_pending_since, + mgmt_timeout, + mgmt_resend, + mgmt_stanzas_in = 0, + mgmt_stanzas_out = 0, + lang = <<"">>}). %-define(DBGFSM, true). -ifdef(DBGFSM). + -define(FSMOPTS, [{debug, [trace]}]). + -else. + -define(FSMOPTS, []). + -endif. %% Module start with or without supervisor: -ifdef(NO_TRANSIENT_SUPERVISORS). --define(SUPERVISOR_START, gen_fsm:start(ejabberd_c2s, [SockData, Opts], - ?FSMOPTS)). +-define(SUPERVISOR_START, ?GEN_FSM:start(ejabberd_c2s, [SockData, Opts], + fsm_limit_opts(Opts) ++ ?FSMOPTS)). -else. -define(SUPERVISOR_START, supervisor:start_child(ejabberd_c2s_sup, [SockData, Opts])). @@ -87,26 +142,62 @@ %% This is the timeout to apply between event when starting a new %% session: --define(C2S_OPEN_TIMEOUT, 5000). +-define(C2S_OPEN_TIMEOUT, 60000). + -define(C2S_HIBERNATE_TIMEOUT, 90000). -define(STREAM_HEADER, - "" - "" - ). + <<"">>). --define(STREAM_TRAILER, ""). +-define(STREAM_TRAILER, <<"">>). + +-define(INVALID_NS_ERR, ?SERR_INVALID_NAMESPACE). + +-define(INVALID_XML_ERR, ?SERR_XML_NOT_WELL_FORMED). + +-define(HOST_UNKNOWN_ERR, ?SERR_HOST_UNKNOWN). --define(INVALID_NS_ERR, - xml:element_to_string(?SERR_INVALID_NAMESPACE)). --define(INVALID_XML_ERR, - xml:element_to_string(?SERR_XML_NOT_WELL_FORMED)). --define(HOST_UNKNOWN_ERR, - xml:element_to_string(?SERR_HOST_UNKNOWN)). -define(POLICY_VIOLATION_ERR(Lang, Text), - xml:element_to_string(?SERRT_POLICY_VIOLATION(Lang, Text))). + ?SERRT_POLICY_VIOLATION(Lang, Text)). + +-define(INVALID_FROM, ?SERR_INVALID_FROM). + +%% XEP-0198: + +-define(IS_STREAM_MGMT_TAG(Name), + Name == <<"enable">>; + Name == <<"resume">>; + Name == <<"a">>; + Name == <<"r">>). + +-define(IS_SUPPORTED_MGMT_XMLNS(Xmlns), + Xmlns == ?NS_STREAM_MGMT_2; + Xmlns == ?NS_STREAM_MGMT_3). + +-define(MGMT_FAILED(Condition, Xmlns), + #xmlel{name = <<"failed">>, + attrs = [{<<"xmlns">>, Xmlns}], + children = [#xmlel{name = Condition, + attrs = [{<<"xmlns">>, ?NS_STANZAS}], + children = []}]}). + +-define(MGMT_BAD_REQUEST(Xmlns), + ?MGMT_FAILED(<<"bad-request">>, Xmlns)). + +-define(MGMT_ITEM_NOT_FOUND(Xmlns), + ?MGMT_FAILED(<<"item-not-found">>, Xmlns)). + +-define(MGMT_SERVICE_UNAVAILABLE(Xmlns), + ?MGMT_FAILED(<<"service-unavailable">>, Xmlns)). + +-define(MGMT_UNEXPECTED_REQUEST(Xmlns), + ?MGMT_FAILED(<<"unexpected-request">>, Xmlns)). + +-define(MGMT_UNSUPPORTED_VERSION(Xmlns), + ?MGMT_FAILED(<<"unsupported-version">>, Xmlns)). %%%---------------------------------------------------------------------- %%% API @@ -115,14 +206,51 @@ start(SockData, Opts) -> ?SUPERVISOR_START. start_link(SockData, Opts) -> - gen_fsm:start_link(ejabberd_c2s, [SockData, Opts], ?FSMOPTS). + ?GEN_FSM:start_link(ejabberd_c2s, [SockData, Opts], + fsm_limit_opts(Opts) ++ ?FSMOPTS). -socket_type() -> - xml_stream. +socket_type() -> xml_stream. %% Return Username, Resource and presence information get_presence(FsmRef) -> - gen_fsm:sync_send_all_state_event(FsmRef, {get_presence}, 1000). + (?GEN_FSM):sync_send_all_state_event(FsmRef, + {get_presence}, 1000). + +get_aux_field(Key, #state{aux_fields = Opts}) -> + case lists:keysearch(Key, 1, Opts) of + {value, {_, Val}} -> {ok, Val}; + _ -> error + end. + +set_aux_field(Key, Val, + #state{aux_fields = Opts} = State) -> + Opts1 = lists:keydelete(Key, 1, Opts), + State#state{aux_fields = [{Key, Val} | Opts1]}. + +del_aux_field(Key, #state{aux_fields = Opts} = State) -> + Opts1 = lists:keydelete(Key, 1, Opts), + State#state{aux_fields = Opts1}. + +get_subscription(From = #jid{}, StateData) -> + get_subscription(jlib:jid_tolower(From), StateData); +get_subscription(LFrom, StateData) -> + LBFrom = setelement(3, LFrom, <<"">>), + F = (?SETS):is_element(LFrom, StateData#state.pres_f) + orelse + (?SETS):is_element(LBFrom, StateData#state.pres_f), + T = (?SETS):is_element(LFrom, StateData#state.pres_t) + orelse + (?SETS):is_element(LBFrom, StateData#state.pres_t), + if F and T -> both; + F -> from; + T -> to; + true -> none + end. + +broadcast(FsmRef, Type, From, Packet) -> + FsmRef ! {broadcast, Type, From, Packet}. + +stop(FsmRef) -> (?GEN_FSM):send_event(FsmRef, closed). %%%---------------------------------------------------------------------- %%% Callback functions from gen_fsm @@ -133,113 +261,178 @@ get_presence(FsmRef) -> %% Returns: {ok, StateName, StateData} | %% {ok, StateName, StateData, Timeout} | %% ignore | -%% {stop, StopReason} +%% {stop, StopReason} %%---------------------------------------------------------------------- init([{SockMod, Socket}, Opts]) -> Access = case lists:keysearch(access, 1, Opts) of - {value, {_, A}} -> A; - _ -> all + {value, {_, A}} -> A; + _ -> all end, Shaper = case lists:keysearch(shaper, 1, Opts) of - {value, {_, S}} -> S; - _ -> none + {value, {_, S}} -> S; + _ -> none end, - Zlib = lists:member(zlib, Opts), - StartTLS = lists:member(starttls, Opts), - StartTLSRequired = lists:member(starttls_required, Opts), - TLSEnabled = lists:member(tls, Opts), - TLS = StartTLS orelse StartTLSRequired orelse TLSEnabled, - TLSOpts = lists:filter(fun({certfile, _}) -> true; - (_) -> false - end, Opts), + XMLSocket = case lists:keysearch(xml_socket, 1, Opts) of + {value, {_, XS}} -> XS; + _ -> false + end, + Zlib = proplists:get_bool(zlib, Opts), + StartTLS = proplists:get_bool(starttls, Opts), + StartTLSRequired = proplists:get_bool(starttls_required, Opts), + TLSEnabled = proplists:get_bool(tls, Opts), + TLS = StartTLS orelse + StartTLSRequired orelse TLSEnabled, + TLSOpts1 = lists:filter(fun ({certfile, _}) -> true; + ({ciphers, _}) -> true; + (_) -> false + end, + Opts), + TLSOpts2 = case lists:keysearch(protocol_options, 1, Opts) of + {value, {_, O}} -> + [_|ProtocolOptions] = lists:foldl( + fun(X, Acc) -> X ++ Acc end, [], + [["|" | binary_to_list(Opt)] || Opt <- O, is_binary(Opt)] + ), + [{protocol_options, iolist_to_binary(ProtocolOptions)} | TLSOpts1]; + _ -> TLSOpts1 + end, + TLSOpts3 = case proplists:get_bool(tls_compression, Opts) of + false -> [compression_none | TLSOpts2]; + true -> TLSOpts2 + end, + TLSOpts = [verify_none | TLSOpts3], + StreamMgmtEnabled = proplists:get_value(stream_management, Opts, true), + StreamMgmtState = if StreamMgmtEnabled -> inactive; + true -> disabled + end, + MaxAckQueue = case proplists:get_value(max_ack_queue, Opts) of + Limit when is_integer(Limit), Limit > 0 -> Limit; + infinity -> infinity; + _ -> 500 + end, + ResumeTimeout = case proplists:get_value(resume_timeout, Opts) of + Timeout when is_integer(Timeout), Timeout >= 0 -> Timeout; + _ -> 300 + end, + ResendOnTimeout = case proplists:get_value(resend_on_timeout, Opts) of + Resend when is_boolean(Resend) -> Resend; + if_offline -> if_offline; + _ -> false + end, IP = peerip(SockMod, Socket), - Socket1 = - if - TLSEnabled -> - SockMod:starttls(Socket, TLSOpts); - true -> - Socket - end, + Socket1 = if TLSEnabled andalso + SockMod /= ejabberd_frontend_socket -> + SockMod:starttls(Socket, TLSOpts); + true -> Socket + end, SocketMonitor = SockMod:monitor(Socket1), - {ok, wait_for_stream, #state{socket = Socket1, - sockmod = SockMod, - socket_monitor = SocketMonitor, - zlib = Zlib, - tls = TLS, - tls_required = StartTLSRequired, - tls_enabled = TLSEnabled, - tls_options = TLSOpts, - streamid = new_id(), - access = Access, - shaper = Shaper, - ip = IP}, ?C2S_OPEN_TIMEOUT}. + StateData = #state{socket = Socket1, sockmod = SockMod, + socket_monitor = SocketMonitor, + xml_socket = XMLSocket, zlib = Zlib, tls = TLS, + tls_required = StartTLSRequired, + tls_enabled = TLSEnabled, tls_options = TLSOpts, + sid = {now(), self()}, streamid = new_id(), + access = Access, shaper = Shaper, ip = IP, + mgmt_state = StreamMgmtState, + mgmt_max_queue = MaxAckQueue, + mgmt_timeout = ResumeTimeout, + mgmt_resend = ResendOnTimeout}, + {ok, wait_for_stream, StateData, ?C2S_OPEN_TIMEOUT}. +%% Return list of all available resources of contacts, +get_subscribed(FsmRef) -> + (?GEN_FSM):sync_send_all_state_event(FsmRef, + get_subscribed, 1000). %%---------------------------------------------------------------------- %% Func: StateName/2 %% Returns: {next_state, NextStateName, NextStateData} | %% {next_state, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} +%% {stop, Reason, NewStateData} %%---------------------------------------------------------------------- wait_for_stream({xmlstreamstart, _Name, Attrs}, StateData) -> - DefaultLang = case ?MYLANG of - undefined -> - " xml:lang='en'"; - DL -> - " xml:lang='" ++ DL ++ "'" - end, - case xml:get_attr_s("xmlns:stream", Attrs) of + DefaultLang = ?MYLANG, + case xml:get_attr_s(<<"xmlns:stream">>, Attrs) of ?NS_STREAM -> - Server = jlib:nameprep(xml:get_attr_s("to", Attrs)), + Server = + case StateData#state.server of + <<"">> -> + jlib:nameprep(xml:get_attr_s(<<"to">>, Attrs)); + S -> S + end, + Lang = case xml:get_attr_s(<<"xml:lang">>, Attrs) of + Lang1 when byte_size(Lang1) =< 35 -> + %% As stated in BCP47, 4.4.1: + %% Protocols or specifications that + %% specify limited buffer sizes for + %% language tags MUST allow for + %% language tags of at least 35 characters. + Lang1; + _ -> + %% Do not store long language tag to + %% avoid possible DoS/flood attacks + <<"">> + end, + IsBlacklistedIP = is_ip_blacklisted(StateData#state.ip, Lang), case lists:member(Server, ?MYHOSTS) of - true -> - Lang = xml:get_attr_s("xml:lang", Attrs), - change_shaper(StateData, jlib:make_jid("", Server, "")), - case xml:get_attr_s("version", Attrs) of - "1.0" -> - Header = io_lib:format(?STREAM_HEADER, - [StateData#state.streamid, - Server, - " version='1.0'", - DefaultLang]), - send_text(StateData, Header), + true when IsBlacklistedIP == false -> + change_shaper(StateData, jlib:make_jid(<<"">>, Server, <<"">>)), + case xml:get_attr_s(<<"version">>, Attrs) of + <<"1.0">> -> + send_header(StateData, Server, <<"1.0">>, DefaultLang), case StateData#state.authenticated of false -> + TLS = StateData#state.tls, + TLSEnabled = StateData#state.tls_enabled, + TLSRequired = StateData#state.tls_required, SASLState = cyrsasl:server_new( - "jabber", Server, "", [], + <<"jabber">>, Server, <<"">>, [], fun(U) -> - ejabberd_auth:get_password( + ejabberd_auth:get_password_with_authmodule( U, Server) end, fun(U, P) -> - ejabberd_auth:check_password( + ejabberd_auth:check_password_with_authmodule( U, Server, P) + end, + fun(U, P, D, DG) -> + ejabberd_auth:check_password_with_authmodule( + U, Server, P, D, DG) end), - Mechs = lists:map( - fun(S) -> - {xmlelement, "mechanism", [], - [{xmlcdata, S}]} - end, cyrsasl:listmech(Server)), + Mechs = + case TLSEnabled or not TLSRequired of + true -> + Ms = lists:map(fun (S) -> + #xmlel{name = <<"mechanism">>, + attrs = [], + children = [{xmlcdata, S}]} + end, + cyrsasl:listmech(Server)), + [#xmlel{name = <<"mechanisms">>, + attrs = [{<<"xmlns">>, ?NS_SASL}], + children = Ms}]; + false -> + [] + end, SockMod = (StateData#state.sockmod):get_sockmod( StateData#state.socket), Zlib = StateData#state.zlib, CompressFeature = case Zlib andalso - (SockMod == gen_tcp) of + ((SockMod == gen_tcp) orelse + (SockMod == p1_tls)) of true -> - [{xmlelement, "compression", - [{"xmlns", ?NS_FEATURE_COMPRESS}], - [{xmlelement, "method", - [], [{xmlcdata, "zlib"}]}]}]; + [#xmlel{name = <<"compression">>, + attrs = [{<<"xmlns">>, ?NS_FEATURE_COMPRESS}], + children = [#xmlel{name = <<"method">>, + attrs = [], + children = [{xmlcdata, <<"zlib">>}]}]}]; _ -> [] end, - TLS = StateData#state.tls, - TLSEnabled = StateData#state.tls_enabled, - TLSRequired = StateData#state.tls_required, TLSFeature = case (TLS == true) andalso (TLSEnabled == false) andalso @@ -247,27 +440,27 @@ wait_for_stream({xmlstreamstart, _Name, Attrs}, StateData) -> true -> case TLSRequired of true -> - [{xmlelement, "starttls", - [{"xmlns", ?NS_TLS}], - [{xmlelement, "required", - [], []}]}]; + [#xmlel{name = <<"starttls">>, + attrs = [{<<"xmlns">>, ?NS_TLS}], + children = [#xmlel{name = <<"required">>, + attrs = [], + children = []}]}]; _ -> - [{xmlelement, "starttls", - [{"xmlns", ?NS_TLS}], []}] + [#xmlel{name = <<"starttls">>, + attrs = [{<<"xmlns">>, ?NS_TLS}], + children = []}] end; false -> [] end, send_element(StateData, - {xmlelement, "stream:features", [], - TLSFeature ++ CompressFeature ++ - [{xmlelement, "mechanisms", - [{"xmlns", ?NS_SASL}], - Mechs}] ++ - ejabberd_hooks:run_fold( - c2s_stream_features, - Server, - [], [])}), + #xmlel{name = <<"stream:features">>, + attrs = [], + children = + TLSFeature ++ CompressFeature ++ Mechs + ++ + ejabberd_hooks:run_fold(c2s_stream_features, + Server, [], [Server])}), fsm_next_state(wait_for_feature_request, StateData#state{ server = Server, @@ -275,653 +468,852 @@ wait_for_stream({xmlstreamstart, _Name, Attrs}, StateData) -> lang = Lang}); _ -> case StateData#state.resource of - "" -> - send_element( - StateData, - {xmlelement, "stream:features", [], - [{xmlelement, "bind", - [{"xmlns", ?NS_BIND}], []}, - {xmlelement, "session", - [{"xmlns", ?NS_SESSION}], []}]}), - fsm_next_state(wait_for_bind, - StateData#state{ - server = Server, - lang = Lang}); - _ -> - send_element( - StateData, - {xmlelement, "stream:features", [], []}), - fsm_next_state(wait_for_session, - StateData#state{ - server = Server, - lang = Lang}) + <<"">> -> + RosterVersioningFeature = + ejabberd_hooks:run_fold(roster_get_versioning_feature, + Server, [], + [Server]), + StreamManagementFeature = + case stream_mgmt_enabled(StateData) of + true -> + [#xmlel{name = <<"sm">>, + attrs = [{<<"xmlns">>, ?NS_STREAM_MGMT_2}], + children = []}, + #xmlel{name = <<"sm">>, + attrs = [{<<"xmlns">>, ?NS_STREAM_MGMT_3}], + children = []}]; + false -> + [] + end, + StreamFeatures = [#xmlel{name = <<"bind">>, + attrs = [{<<"xmlns">>, ?NS_BIND}], + children = []}, + #xmlel{name = <<"session">>, + attrs = [{<<"xmlns">>, ?NS_SESSION}], + children = []}] + ++ + RosterVersioningFeature ++ + StreamManagementFeature ++ + ejabberd_hooks:run_fold(c2s_post_auth_features, + Server, [], [Server]) ++ + ejabberd_hooks:run_fold(c2s_stream_features, + Server, [], [Server]), + send_element(StateData, + #xmlel{name = <<"stream:features">>, + attrs = [], + children = StreamFeatures}), + fsm_next_state(wait_for_bind, + StateData#state{server = Server, lang = Lang}); + _ -> + send_element(StateData, + #xmlel{name = <<"stream:features">>, + attrs = [], + children = []}), + fsm_next_state(wait_for_session, + StateData#state{server = Server, lang = Lang}) end end; - _ -> - Header = io_lib:format( - ?STREAM_HEADER, - [StateData#state.streamid, Server, "", - DefaultLang]), - if - (not StateData#state.tls_enabled) and - StateData#state.tls_required -> - send_text(StateData, - Header ++ - ?POLICY_VIOLATION_ERR( - Lang, - "Use of STARTTLS required") ++ - ?STREAM_TRAILER), - {stop, normal, StateData}; - true -> - send_text(StateData, Header), - fsm_next_state(wait_for_auth, - StateData#state{ - server = Server, - lang = Lang}) - end - end; _ -> - Header = io_lib:format( - ?STREAM_HEADER, - [StateData#state.streamid, ?MYNAME, "", - DefaultLang]), - send_text(StateData, - Header ++ ?HOST_UNKNOWN_ERR ++ ?STREAM_TRAILER), - {stop, normal, StateData} + send_header(StateData, Server, <<"">>, DefaultLang), + if not StateData#state.tls_enabled and + StateData#state.tls_required -> + send_element(StateData, + ?POLICY_VIOLATION_ERR(Lang, + <<"Use of STARTTLS required">>)), + send_trailer(StateData), + {stop, normal, StateData}; + true -> + fsm_next_state(wait_for_auth, + StateData#state{server = Server, + lang = Lang}) + end end; + true -> + IP = StateData#state.ip, + {true, LogReason, ReasonT} = IsBlacklistedIP, + ?INFO_MSG("Connection attempt from blacklisted IP ~s: ~s", + [jlib:ip_to_list(IP), LogReason]), + send_header(StateData, Server, <<"">>, DefaultLang), + send_element(StateData, ?POLICY_VIOLATION_ERR(Lang, ReasonT)), + send_trailer(StateData), + {stop, normal, StateData}; _ -> - Header = io_lib:format( - ?STREAM_HEADER, - [StateData#state.streamid, ?MYNAME, "", DefaultLang]), - send_text(StateData, - Header ++ ?INVALID_NS_ERR ++ ?STREAM_TRAILER), + send_header(StateData, ?MYNAME, <<"">>, DefaultLang), + send_element(StateData, ?HOST_UNKNOWN_ERR), + send_trailer(StateData), {stop, normal, StateData} + end; + _ -> + send_header(StateData, ?MYNAME, <<"">>, DefaultLang), + send_element(StateData, ?INVALID_NS_ERR), + send_trailer(StateData), + {stop, normal, StateData} end; - wait_for_stream(timeout, StateData) -> {stop, normal, StateData}; - wait_for_stream({xmlstreamelement, _}, StateData) -> - send_text(StateData, ?INVALID_XML_ERR ++ ?STREAM_TRAILER), + send_element(StateData, ?INVALID_XML_ERR), + send_trailer(StateData), {stop, normal, StateData}; - wait_for_stream({xmlstreamend, _}, StateData) -> - send_text(StateData, ?INVALID_XML_ERR ++ ?STREAM_TRAILER), + send_element(StateData, ?INVALID_XML_ERR), + send_trailer(StateData), {stop, normal, StateData}; - wait_for_stream({xmlstreamerror, _}, StateData) -> - Header = io_lib:format(?STREAM_HEADER, - ["none", ?MYNAME, " version='1.0'", ""]), - send_text(StateData, - Header ++ ?INVALID_XML_ERR ++ ?STREAM_TRAILER), + send_header(StateData, ?MYNAME, <<"1.0">>, <<"">>), + send_element(StateData, ?INVALID_XML_ERR), + send_trailer(StateData), {stop, normal, StateData}; - wait_for_stream(closed, StateData) -> {stop, normal, StateData}. - +wait_for_auth({xmlstreamelement, #xmlel{name = Name} = El}, StateData) + when ?IS_STREAM_MGMT_TAG(Name) -> + fsm_next_state(wait_for_auth, dispatch_stream_mgmt(El, StateData)); wait_for_auth({xmlstreamelement, El}, StateData) -> case is_auth_packet(El) of - {auth, _ID, get, {U, _, _, _}} -> - {xmlelement, Name, Attrs, _Els} = jlib:make_result_iq_reply(El), - case U of - "" -> - UCdata = []; - _ -> - UCdata = [{xmlcdata, U}] - end, - Res = case ejabberd_auth:plain_password_required( - StateData#state.server) of - false -> - {xmlelement, Name, Attrs, - [{xmlelement, "query", [{"xmlns", ?NS_AUTH}], - [{xmlelement, "username", [], UCdata}, - {xmlelement, "password", [], []}, - {xmlelement, "digest", [], []}, - {xmlelement, "resource", [], []} - ]}]}; - true -> - {xmlelement, Name, Attrs, - [{xmlelement, "query", [{"xmlns", ?NS_AUTH}], - [{xmlelement, "username", [], UCdata}, - {xmlelement, "password", [], []}, - {xmlelement, "resource", [], []} - ]}]} - end, - send_element(StateData, Res), - fsm_next_state(wait_for_auth, StateData); - {auth, _ID, set, {_U, _P, _D, ""}} -> - Err = jlib:make_error_reply( - El, - ?ERR_AUTH_NO_RESOURCE_PROVIDED(StateData#state.lang)), - send_element(StateData, Err), - fsm_next_state(wait_for_auth, StateData); - {auth, _ID, set, {U, P, D, R}} -> - JID = jlib:make_jid(U, StateData#state.server, R), - case (JID /= error) andalso - (acl:match_rule(StateData#state.server, - StateData#state.access, JID) == allow) of - true -> - case ejabberd_auth:check_password( - U, StateData#state.server, P, - StateData#state.streamid, D) of - true -> - ?INFO_MSG( - "(~w) Accepted legacy authentication for ~s", - [StateData#state.socket, - jlib:jid_to_string(JID)]), - SID = {now(), self()}, - IP = StateData#state.ip, - ejabberd_sm:open_session( - SID, U, StateData#state.server, R, IP), - Res1 = jlib:make_result_iq_reply(El), - Res = setelement(4, Res1, []), - send_element(StateData, Res), - change_shaper(StateData, JID), - {Fs, Ts} = ejabberd_hooks:run_fold( - roster_get_subscription_lists, - StateData#state.server, - {[], []}, - [U, StateData#state.server]), - LJID = jlib:jid_tolower( - jlib:jid_remove_resource(JID)), - Fs1 = [LJID | Fs], - Ts1 = [LJID | Ts], - PrivList = - ejabberd_hooks:run_fold( - privacy_get_user_list, StateData#state.server, - none, - [U, StateData#state.server]), - fsm_next_state(session_established, - StateData#state{ - user = U, - resource = R, - jid = JID, - sid = SID, - pres_f = ?SETS:from_list(Fs1), - pres_t = ?SETS:from_list(Ts1), - privacy_list = PrivList}); - _ -> - ?INFO_MSG( - "(~w) Failed legacy authentication for ~s", - [StateData#state.socket, - jlib:jid_to_string(JID)]), - Err = jlib:make_error_reply( - El, ?ERR_NOT_AUTHORIZED), - send_element(StateData, Err), - fsm_next_state(wait_for_auth, StateData) - end; - _ -> - if - JID == error -> - ?INFO_MSG( - "(~w) Forbidden legacy authentication for " - "username '~s' with resource '~s'", - [StateData#state.socket, U, R]), - Err = jlib:make_error_reply(El, ?ERR_JID_MALFORMED), - send_element(StateData, Err), - fsm_next_state(wait_for_auth, StateData); - true -> - ?INFO_MSG( - "(~w) Forbidden legacy authentication for ~s", - [StateData#state.socket, - jlib:jid_to_string(JID)]), - Err = jlib:make_error_reply(El, ?ERR_NOT_ALLOWED), - send_element(StateData, Err), - fsm_next_state(wait_for_auth, StateData) - end - end; - _ -> - process_unauthenticated_stanza(StateData, El), - fsm_next_state(wait_for_auth, StateData) + {auth, _ID, get, {U, _, _, _}} -> + #xmlel{name = Name, attrs = Attrs} = + jlib:make_result_iq_reply(El), + case U of + <<"">> -> UCdata = []; + _ -> UCdata = [{xmlcdata, U}] + end, + Res = case + ejabberd_auth:plain_password_required(StateData#state.server) + of + false -> + #xmlel{name = Name, attrs = Attrs, + children = + [#xmlel{name = <<"query">>, + attrs = [{<<"xmlns">>, ?NS_AUTH}], + children = + [#xmlel{name = <<"username">>, + attrs = [], + children = UCdata}, + #xmlel{name = <<"password">>, + attrs = [], children = []}, + #xmlel{name = <<"digest">>, + attrs = [], children = []}, + #xmlel{name = <<"resource">>, + attrs = [], + children = []}]}]}; + true -> + #xmlel{name = Name, attrs = Attrs, + children = + [#xmlel{name = <<"query">>, + attrs = [{<<"xmlns">>, ?NS_AUTH}], + children = + [#xmlel{name = <<"username">>, + attrs = [], + children = UCdata}, + #xmlel{name = <<"password">>, + attrs = [], children = []}, + #xmlel{name = <<"resource">>, + attrs = [], + children = []}]}]} + end, + send_element(StateData, Res), + fsm_next_state(wait_for_auth, StateData); + {auth, _ID, set, {_U, _P, _D, <<"">>}} -> + Err = jlib:make_error_reply(El, + ?ERR_AUTH_NO_RESOURCE_PROVIDED((StateData#state.lang))), + send_element(StateData, Err), + fsm_next_state(wait_for_auth, StateData); + {auth, _ID, set, {U, P, D, R}} -> + JID = jlib:make_jid(U, StateData#state.server, R), + case JID /= error andalso + acl:match_rule(StateData#state.server, + StateData#state.access, JID) + == allow + of + true -> + DGen = fun (PW) -> + p1_sha:sha(<<(StateData#state.streamid)/binary, PW/binary>>) + end, + case ejabberd_auth:check_password_with_authmodule(U, + StateData#state.server, + P, D, DGen) + of + {true, AuthModule} -> + ?INFO_MSG("(~w) Accepted legacy authentication for ~s by ~p from ~s", + [StateData#state.socket, + jlib:jid_to_string(JID), AuthModule, + jlib:ip_to_list(StateData#state.ip)]), + ejabberd_hooks:run(c2s_auth_result, StateData#state.server, + [true, U, StateData#state.server, + StateData#state.ip]), + Conn = get_conn_type(StateData), + Info = [{ip, StateData#state.ip}, {conn, Conn}, + {auth_module, AuthModule}], + Res = jlib:make_result_iq_reply( + El#xmlel{children = []}), + send_element(StateData, Res), + ejabberd_sm:open_session(StateData#state.sid, U, + StateData#state.server, R, + Info), + change_shaper(StateData, JID), + {Fs, Ts} = + ejabberd_hooks:run_fold(roster_get_subscription_lists, + StateData#state.server, + {[], []}, + [U, + StateData#state.server]), + LJID = + jlib:jid_tolower(jlib:jid_remove_resource(JID)), + Fs1 = [LJID | Fs], + Ts1 = [LJID | Ts], + PrivList = ejabberd_hooks:run_fold(privacy_get_user_list, + StateData#state.server, + #userlist{}, + [U, StateData#state.server]), + NewStateData = StateData#state{user = U, + resource = R, + jid = JID, + conn = Conn, + auth_module = AuthModule, + pres_f = (?SETS):from_list(Fs1), + pres_t = (?SETS):from_list(Ts1), + privacy_list = PrivList}, + fsm_next_state(session_established, NewStateData); + _ -> + ?INFO_MSG("(~w) Failed legacy authentication for ~s from ~s", + [StateData#state.socket, + jlib:jid_to_string(JID), + jlib:ip_to_list(StateData#state.ip)]), + ejabberd_hooks:run(c2s_auth_result, StateData#state.server, + [false, U, StateData#state.server, + StateData#state.ip]), + Err = jlib:make_error_reply(El, ?ERR_NOT_AUTHORIZED), + send_element(StateData, Err), + fsm_next_state(wait_for_auth, StateData) + end; + _ -> + if JID == error -> + ?INFO_MSG("(~w) Forbidden legacy authentication " + "for username '~s' with resource '~s'", + [StateData#state.socket, U, R]), + Err = jlib:make_error_reply(El, ?ERR_JID_MALFORMED), + send_element(StateData, Err), + fsm_next_state(wait_for_auth, StateData); + true -> + ?INFO_MSG("(~w) Forbidden legacy authentication " + "for ~s from ~s", + [StateData#state.socket, + jlib:jid_to_string(JID), + jlib:ip_to_list(StateData#state.ip)]), + ejabberd_hooks:run(c2s_auth_result, StateData#state.server, + [false, U, StateData#state.server, + StateData#state.ip]), + Err = jlib:make_error_reply(El, ?ERR_NOT_ALLOWED), + send_element(StateData, Err), + fsm_next_state(wait_for_auth, StateData) + end + end; + _ -> + process_unauthenticated_stanza(StateData, El), + fsm_next_state(wait_for_auth, StateData) end; - wait_for_auth(timeout, StateData) -> {stop, normal, StateData}; - wait_for_auth({xmlstreamend, _Name}, StateData) -> - send_text(StateData, ?STREAM_TRAILER), - {stop, normal, StateData}; - + send_trailer(StateData), {stop, normal, StateData}; wait_for_auth({xmlstreamerror, _}, StateData) -> - send_text(StateData, ?INVALID_XML_ERR ++ ?STREAM_TRAILER), + send_element(StateData, ?INVALID_XML_ERR), + send_trailer(StateData), {stop, normal, StateData}; - wait_for_auth(closed, StateData) -> {stop, normal, StateData}. - -wait_for_feature_request({xmlstreamelement, El}, StateData) -> - {xmlelement, Name, Attrs, Els} = El, +wait_for_feature_request({xmlstreamelement, #xmlel{name = Name} = El}, + StateData) + when ?IS_STREAM_MGMT_TAG(Name) -> + fsm_next_state(wait_for_feature_request, + dispatch_stream_mgmt(El, StateData)); +wait_for_feature_request({xmlstreamelement, El}, + StateData) -> + #xmlel{name = Name, attrs = Attrs, children = Els} = El, Zlib = StateData#state.zlib, TLS = StateData#state.tls, TLSEnabled = StateData#state.tls_enabled, TLSRequired = StateData#state.tls_required, - SockMod = (StateData#state.sockmod):get_sockmod(StateData#state.socket), - case {xml:get_attr_s("xmlns", Attrs), Name} of - {?NS_SASL, "auth"} when not ((SockMod == gen_tcp) and TLSRequired) -> - Mech = xml:get_attr_s("mechanism", Attrs), - ClientIn = jlib:decode_base64(xml:get_cdata(Els)), - case cyrsasl:server_start(StateData#state.sasl_state, - Mech, - ClientIn) of - {ok, Props} -> - (StateData#state.sockmod):reset_stream( - StateData#state.socket), - send_element(StateData, - {xmlelement, "success", - [{"xmlns", ?NS_SASL}], []}), - U = xml:get_attr_s(username, Props), - ?INFO_MSG("(~w) Accepted authentication for ~s", - [StateData#state.socket, U]), - fsm_next_state(wait_for_stream, - StateData#state{ - streamid = new_id(), - authenticated = true, - user = U }); - {continue, ServerOut, NewSASLState} -> - send_element(StateData, - {xmlelement, "challenge", - [{"xmlns", ?NS_SASL}], - [{xmlcdata, - jlib:encode_base64(ServerOut)}]}), - fsm_next_state(wait_for_sasl_response, - StateData#state{ - sasl_state = NewSASLState}); - {error, Error, Username} -> - ?INFO_MSG( - "(~w) Failed authentication for ~s@~s", - [StateData#state.socket, - Username, StateData#state.server]), - send_element(StateData, - {xmlelement, "failure", - [{"xmlns", ?NS_SASL}], - [{xmlelement, Error, [], []}]}), - {next_state, wait_for_feature_request, StateData, - ?C2S_OPEN_TIMEOUT}; - {error, Error} -> - send_element(StateData, - {xmlelement, "failure", - [{"xmlns", ?NS_SASL}], - [{xmlelement, Error, [], []}]}), - fsm_next_state(wait_for_feature_request, StateData) - end; - {?NS_TLS, "starttls"} when TLS == true, - TLSEnabled == false, - SockMod == gen_tcp -> - TLSOpts = case ejabberd_config:get_local_option( - {domain_certfile, StateData#state.server}) of - undefined -> - StateData#state.tls_options; - CertFile -> - [{certfile, CertFile} | - lists:keydelete( - certfile, 1, StateData#state.tls_options)] - end, - Socket = StateData#state.socket, - TLSSocket = (StateData#state.sockmod):starttls( - Socket, TLSOpts, - xml:element_to_string( - {xmlelement, "proceed", [{"xmlns", ?NS_TLS}], []})), - fsm_next_state(wait_for_stream, - StateData#state{socket = TLSSocket, - streamid = new_id(), - tls_enabled = true - }); - {?NS_COMPRESS, "compress"} when Zlib == true, - SockMod == gen_tcp -> - case xml:get_subtag(El, "method") of - false -> - send_element(StateData, - {xmlelement, "failure", - [{"xmlns", ?NS_COMPRESS}], - [{xmlelement, "setup-failed", [], []}]}), - fsm_next_state(wait_for_feature_request, StateData); - Method -> - case xml:get_tag_cdata(Method) of - "zlib" -> - Socket = StateData#state.socket, - ZlibSocket = (StateData#state.sockmod):compress( - Socket, - xml:element_to_string( - {xmlelement, "compressed", - [{"xmlns", ?NS_COMPRESS}], []})), - fsm_next_state(wait_for_stream, - StateData#state{socket = ZlibSocket, - streamid = new_id() - }); - _ -> - send_element(StateData, - {xmlelement, "failure", - [{"xmlns", ?NS_COMPRESS}], - [{xmlelement, "unsupported-method", - [], []}]}), - fsm_next_state(wait_for_feature_request, - StateData) - end - end; - _ -> - if - (SockMod == gen_tcp) and TLSRequired -> - Lang = StateData#state.lang, - send_text(StateData, ?POLICY_VIOLATION_ERR( - Lang, - "Use of STARTTLS required") ++ - ?STREAM_TRAILER), - {stop, normal, StateData}; - true -> - process_unauthenticated_stanza(StateData, El), - fsm_next_state(wait_for_feature_request, StateData) - end + SockMod = + (StateData#state.sockmod):get_sockmod(StateData#state.socket), + case {xml:get_attr_s(<<"xmlns">>, Attrs), Name} of + {?NS_SASL, <<"auth">>} + when TLSEnabled or not TLSRequired -> + Mech = xml:get_attr_s(<<"mechanism">>, Attrs), + ClientIn = jlib:decode_base64(xml:get_cdata(Els)), + case cyrsasl:server_start(StateData#state.sasl_state, + Mech, ClientIn) + of + {ok, Props} -> + (StateData#state.sockmod):reset_stream(StateData#state.socket), + %U = xml:get_attr_s(username, Props), + U = proplists:get_value(username, Props, <<>>), + %AuthModule = xml:get_attr_s(auth_module, Props), + AuthModule = proplists:get_value(auth_module, Props, undefined), + ?INFO_MSG("(~w) Accepted authentication for ~s " + "by ~p from ~s", + [StateData#state.socket, U, AuthModule, + jlib:ip_to_list(StateData#state.ip)]), + ejabberd_hooks:run(c2s_auth_result, StateData#state.server, + [true, U, StateData#state.server, + StateData#state.ip]), + send_element(StateData, + #xmlel{name = <<"success">>, + attrs = [{<<"xmlns">>, ?NS_SASL}], + children = []}), + fsm_next_state(wait_for_stream, + StateData#state{streamid = new_id(), + authenticated = true, + auth_module = AuthModule, + sasl_state = undefined, + user = U}); + {continue, ServerOut, NewSASLState} -> + send_element(StateData, + #xmlel{name = <<"challenge">>, + attrs = [{<<"xmlns">>, ?NS_SASL}], + children = + [{xmlcdata, + jlib:encode_base64(ServerOut)}]}), + fsm_next_state(wait_for_sasl_response, + StateData#state{sasl_state = NewSASLState}); + {error, Error, Username} -> + ?INFO_MSG("(~w) Failed authentication for ~s@~s from ~s", + [StateData#state.socket, + Username, StateData#state.server, + jlib:ip_to_list(StateData#state.ip)]), + ejabberd_hooks:run(c2s_auth_result, StateData#state.server, + [false, Username, StateData#state.server, + StateData#state.ip]), + send_element(StateData, + #xmlel{name = <<"failure">>, + attrs = [{<<"xmlns">>, ?NS_SASL}], + children = + [#xmlel{name = Error, attrs = [], + children = []}]}), + fsm_next_state(wait_for_feature_request, StateData); + {error, Error} -> + send_element(StateData, + #xmlel{name = <<"failure">>, + attrs = [{<<"xmlns">>, ?NS_SASL}], + children = + [#xmlel{name = Error, attrs = [], + children = []}]}), + fsm_next_state(wait_for_feature_request, StateData) + end; + {?NS_TLS, <<"starttls">>} + when TLS == true, TLSEnabled == false, + SockMod == gen_tcp -> + TLSOpts = case + ejabberd_config:get_option( + {domain_certfile, StateData#state.server}, + fun iolist_to_binary/1) + of + undefined -> StateData#state.tls_options; + CertFile -> + [{certfile, CertFile} | lists:keydelete(certfile, 1, + StateData#state.tls_options)] + end, + Socket = StateData#state.socket, + BProceed = xml:element_to_binary(#xmlel{name = <<"proceed">>, + attrs = [{<<"xmlns">>, ?NS_TLS}]}), + TLSSocket = (StateData#state.sockmod):starttls(Socket, + TLSOpts, + BProceed), + fsm_next_state(wait_for_stream, + StateData#state{socket = TLSSocket, + streamid = new_id(), + tls_enabled = true}); + {?NS_COMPRESS, <<"compress">>} + when Zlib == true, + (SockMod == gen_tcp) or (SockMod == p1_tls) -> + case xml:get_subtag(El, <<"method">>) of + false -> + send_element(StateData, + #xmlel{name = <<"failure">>, + attrs = [{<<"xmlns">>, ?NS_COMPRESS}], + children = + [#xmlel{name = <<"setup-failed">>, + attrs = [], children = []}]}), + fsm_next_state(wait_for_feature_request, StateData); + Method -> + case xml:get_tag_cdata(Method) of + <<"zlib">> -> + Socket = StateData#state.socket, + BCompressed = xml:element_to_binary(#xmlel{name = <<"compressed">>, + attrs = [{<<"xmlns">>, ?NS_COMPRESS}]}), + ZlibSocket = (StateData#state.sockmod):compress(Socket, + BCompressed), + fsm_next_state(wait_for_stream, + StateData#state{socket = ZlibSocket, + streamid = new_id()}); + _ -> + send_element(StateData, + #xmlel{name = <<"failure">>, + attrs = [{<<"xmlns">>, ?NS_COMPRESS}], + children = + [#xmlel{name = + <<"unsupported-method">>, + attrs = [], + children = []}]}), + fsm_next_state(wait_for_feature_request, StateData) + end + end; + _ -> + if TLSRequired and not TLSEnabled -> + Lang = StateData#state.lang, + send_element(StateData, + ?POLICY_VIOLATION_ERR(Lang, + <<"Use of STARTTLS required">>)), + send_trailer(StateData), + {stop, normal, StateData}; + true -> + process_unauthenticated_stanza(StateData, El), + fsm_next_state(wait_for_feature_request, StateData) + end end; - wait_for_feature_request(timeout, StateData) -> {stop, normal, StateData}; - -wait_for_feature_request({xmlstreamend, _Name}, StateData) -> - send_text(StateData, ?STREAM_TRAILER), +wait_for_feature_request({xmlstreamend, _Name}, + StateData) -> + send_trailer(StateData), {stop, normal, StateData}; +wait_for_feature_request({xmlstreamerror, _}, + StateData) -> + send_element(StateData, ?INVALID_XML_ERR), + send_trailer(StateData), {stop, normal, StateData}; - -wait_for_feature_request({xmlstreamerror, _}, StateData) -> - send_text(StateData, ?INVALID_XML_ERR ++ ?STREAM_TRAILER), - {stop, normal, StateData}; - wait_for_feature_request(closed, StateData) -> {stop, normal, StateData}. - -wait_for_sasl_response({xmlstreamelement, El}, StateData) -> - {xmlelement, Name, Attrs, Els} = El, - case {xml:get_attr_s("xmlns", Attrs), Name} of - {?NS_SASL, "response"} -> - ClientIn = jlib:decode_base64(xml:get_cdata(Els)), - case cyrsasl:server_step(StateData#state.sasl_state, - ClientIn) of - {ok, Props} -> - (StateData#state.sockmod):reset_stream( - StateData#state.socket), - send_element(StateData, - {xmlelement, "success", - [{"xmlns", ?NS_SASL}], []}), - U = xml:get_attr_s(username, Props), - ?INFO_MSG("(~w) Accepted authentication for ~s", - [StateData#state.socket, U]), - fsm_next_state(wait_for_stream, - StateData#state{ - streamid = new_id(), - authenticated = true, - user = U}); - {continue, ServerOut, NewSASLState} -> - send_element(StateData, - {xmlelement, "challenge", - [{"xmlns", ?NS_SASL}], - [{xmlcdata, - jlib:encode_base64(ServerOut)}]}), - fsm_next_state(wait_for_sasl_response, - StateData#state{sasl_state = NewSASLState}); - {error, Error, Username} -> - ?INFO_MSG( - "(~w) Failed authentication for ~s@~s", - [StateData#state.socket, - Username, StateData#state.server]), - send_element(StateData, - {xmlelement, "failure", - [{"xmlns", ?NS_SASL}], - [{xmlelement, Error, [], []}]}), - fsm_next_state(wait_for_feature_request, StateData); - {error, Error} -> - send_element(StateData, - {xmlelement, "failure", - [{"xmlns", ?NS_SASL}], - [{xmlelement, Error, [], []}]}), - fsm_next_state(wait_for_feature_request, StateData) - end; - _ -> - process_unauthenticated_stanza(StateData, El), - fsm_next_state(wait_for_feature_request, StateData) +wait_for_sasl_response({xmlstreamelement, #xmlel{name = Name} = El}, StateData) + when ?IS_STREAM_MGMT_TAG(Name) -> + fsm_next_state(wait_for_sasl_response, dispatch_stream_mgmt(El, StateData)); +wait_for_sasl_response({xmlstreamelement, El}, + StateData) -> + #xmlel{name = Name, attrs = Attrs, children = Els} = El, + case {xml:get_attr_s(<<"xmlns">>, Attrs), Name} of + {?NS_SASL, <<"response">>} -> + ClientIn = jlib:decode_base64(xml:get_cdata(Els)), + case cyrsasl:server_step(StateData#state.sasl_state, + ClientIn) + of + {ok, Props} -> + catch + (StateData#state.sockmod):reset_stream(StateData#state.socket), +% U = xml:get_attr_s(username, Props), + U = proplists:get_value(username, Props, <<>>), +% AuthModule = xml:get_attr_s(auth_module, Props), + AuthModule = proplists:get_value(auth_module, Props, <<>>), + ?INFO_MSG("(~w) Accepted authentication for ~s " + "by ~p from ~s", + [StateData#state.socket, U, AuthModule, + jlib:ip_to_list(StateData#state.ip)]), + ejabberd_hooks:run(c2s_auth_result, StateData#state.server, + [true, U, StateData#state.server, + StateData#state.ip]), + send_element(StateData, + #xmlel{name = <<"success">>, + attrs = [{<<"xmlns">>, ?NS_SASL}], + children = []}), + fsm_next_state(wait_for_stream, + StateData#state{streamid = new_id(), + authenticated = true, + auth_module = AuthModule, + sasl_state = undefined, + user = U}); + {ok, Props, ServerOut} -> + (StateData#state.sockmod):reset_stream(StateData#state.socket), +% U = xml:get_attr_s(username, Props), + U = proplists:get_value(username, Props, <<>>), +% AuthModule = xml:get_attr_s(auth_module, Props), + AuthModule = proplists:get_value(auth_module, Props, undefined), + ?INFO_MSG("(~w) Accepted authentication for ~s " + "by ~p from ~s", + [StateData#state.socket, U, AuthModule, + jlib:ip_to_list(StateData#state.ip)]), + ejabberd_hooks:run(c2s_auth_result, StateData#state.server, + [true, U, StateData#state.server, + StateData#state.ip]), + send_element(StateData, + #xmlel{name = <<"success">>, + attrs = [{<<"xmlns">>, ?NS_SASL}], + children = + [{xmlcdata, + jlib:encode_base64(ServerOut)}]}), + fsm_next_state(wait_for_stream, + StateData#state{streamid = new_id(), + authenticated = true, + auth_module = AuthModule, + sasl_state = undefined, + user = U}); + {continue, ServerOut, NewSASLState} -> + send_element(StateData, + #xmlel{name = <<"challenge">>, + attrs = [{<<"xmlns">>, ?NS_SASL}], + children = + [{xmlcdata, + jlib:encode_base64(ServerOut)}]}), + fsm_next_state(wait_for_sasl_response, + StateData#state{sasl_state = NewSASLState}); + {error, Error, Username} -> + ?INFO_MSG("(~w) Failed authentication for ~s@~s from ~s", + [StateData#state.socket, + Username, StateData#state.server, + jlib:ip_to_list(StateData#state.ip)]), + ejabberd_hooks:run(c2s_auth_result, StateData#state.server, + [false, Username, StateData#state.server, + StateData#state.ip]), + send_element(StateData, + #xmlel{name = <<"failure">>, + attrs = [{<<"xmlns">>, ?NS_SASL}], + children = + [#xmlel{name = Error, attrs = [], + children = []}]}), + fsm_next_state(wait_for_feature_request, StateData); + {error, Error} -> + send_element(StateData, + #xmlel{name = <<"failure">>, + attrs = [{<<"xmlns">>, ?NS_SASL}], + children = + [#xmlel{name = Error, attrs = [], + children = []}]}), + fsm_next_state(wait_for_feature_request, StateData) + end; + _ -> + process_unauthenticated_stanza(StateData, El), + fsm_next_state(wait_for_feature_request, StateData) end; - wait_for_sasl_response(timeout, StateData) -> {stop, normal, StateData}; - -wait_for_sasl_response({xmlstreamend, _Name}, StateData) -> - send_text(StateData, ?STREAM_TRAILER), +wait_for_sasl_response({xmlstreamend, _Name}, + StateData) -> + send_trailer(StateData), {stop, normal, StateData}; +wait_for_sasl_response({xmlstreamerror, _}, + StateData) -> + send_element(StateData, ?INVALID_XML_ERR), + send_trailer(StateData), {stop, normal, StateData}; - -wait_for_sasl_response({xmlstreamerror, _}, StateData) -> - send_text(StateData, ?INVALID_XML_ERR ++ ?STREAM_TRAILER), - {stop, normal, StateData}; - wait_for_sasl_response(closed, StateData) -> {stop, normal, StateData}. +resource_conflict_action(U, S, R) -> + OptionRaw = case ejabberd_sm:is_existing_resource(U, S, R) of + true -> + ejabberd_config:get_option( + {resource_conflict, S}, + fun(setresource) -> setresource; + (closeold) -> closeold; + (closenew) -> closenew; + (acceptnew) -> acceptnew + end); + false -> + acceptnew + end, + Option = case OptionRaw of + setresource -> setresource; + closeold -> + acceptnew; %% ejabberd_sm will close old session + closenew -> closenew; + acceptnew -> acceptnew; + _ -> acceptnew %% default ejabberd behavior + end, + case Option of + acceptnew -> {accept_resource, R}; + closenew -> closenew; + setresource -> + Rnew = iolist_to_binary([randoms:get_string() + | [jlib:integer_to_binary(X) + || X <- tuple_to_list(now())]]), + {accept_resource, Rnew} + end. - +wait_for_bind({xmlstreamelement, #xmlel{name = Name, attrs = Attrs} = El}, + StateData) + when ?IS_STREAM_MGMT_TAG(Name) -> + case Name of + <<"resume">> -> + case handle_resume(StateData, Attrs) of + {ok, ResumedState} -> + fsm_next_state(session_established, ResumedState); + error -> + fsm_next_state(wait_for_bind, StateData) + end; + _ -> + fsm_next_state(wait_for_bind, dispatch_stream_mgmt(El, StateData)) + end; wait_for_bind({xmlstreamelement, El}, StateData) -> case jlib:iq_query_info(El) of - #iq{type = set, xmlns = ?NS_BIND, sub_el = SubEl} = IQ -> - U = StateData#state.user, - R1 = xml:get_path_s(SubEl, [{elem, "resource"}, cdata]), - R = case jlib:resourceprep(R1) of - error -> error; - "" -> - lists:concat( - [randoms:get_string() | tuple_to_list(now())]); - Resource -> Resource - end, - case R of - error -> - Err = jlib:make_error_reply(El, ?ERR_BAD_REQUEST), - send_element(StateData, Err), - fsm_next_state(wait_for_bind, StateData); - _ -> - JID = jlib:make_jid(U, StateData#state.server, R), - Res = IQ#iq{type = result, - sub_el = [{xmlelement, "bind", - [{"xmlns", ?NS_BIND}], - [{xmlelement, "jid", [], - [{xmlcdata, - jlib:jid_to_string(JID)}]}]}]}, - send_element(StateData, jlib:iq_to_xml(Res)), - fsm_next_state(wait_for_session, - StateData#state{resource = R, jid = JID}) - end; - _ -> - fsm_next_state(wait_for_bind, StateData) + #iq{type = set, xmlns = ?NS_BIND, sub_el = SubEl} = + IQ -> + U = StateData#state.user, + R1 = xml:get_path_s(SubEl, + [{elem, <<"resource">>}, cdata]), + R = case jlib:resourceprep(R1) of + error -> error; + <<"">> -> + iolist_to_binary([randoms:get_string() + | [jlib:integer_to_binary(X) + || X <- tuple_to_list(now())]]); + Resource -> Resource + end, + case R of + error -> + Err = jlib:make_error_reply(El, ?ERR_BAD_REQUEST), + send_element(StateData, Err), + fsm_next_state(wait_for_bind, StateData); + _ -> + case resource_conflict_action(U, StateData#state.server, + R) + of + closenew -> + Err = jlib:make_error_reply(El, + ?STANZA_ERROR(<<"409">>, + <<"modify">>, + <<"conflict">>)), + send_element(StateData, Err), + fsm_next_state(wait_for_bind, StateData); + {accept_resource, R2} -> + JID = jlib:make_jid(U, StateData#state.server, R2), + Res = IQ#iq{type = result, + sub_el = + [#xmlel{name = <<"bind">>, + attrs = [{<<"xmlns">>, ?NS_BIND}], + children = + [#xmlel{name = <<"jid">>, + attrs = [], + children = + [{xmlcdata, + jlib:jid_to_string(JID)}]}]}]}, + send_element(StateData, jlib:iq_to_xml(Res)), + fsm_next_state(wait_for_session, + StateData#state{resource = R2, jid = JID}) + end + end; + _ -> fsm_next_state(wait_for_bind, StateData) end; - wait_for_bind(timeout, StateData) -> {stop, normal, StateData}; - wait_for_bind({xmlstreamend, _Name}, StateData) -> - send_text(StateData, ?STREAM_TRAILER), - {stop, normal, StateData}; - + send_trailer(StateData), {stop, normal, StateData}; wait_for_bind({xmlstreamerror, _}, StateData) -> - send_text(StateData, ?INVALID_XML_ERR ++ ?STREAM_TRAILER), + send_element(StateData, ?INVALID_XML_ERR), + send_trailer(StateData), {stop, normal, StateData}; - wait_for_bind(closed, StateData) -> {stop, normal, StateData}. - - +wait_for_session({xmlstreamelement, #xmlel{name = Name} = El}, StateData) + when ?IS_STREAM_MGMT_TAG(Name) -> + fsm_next_state(wait_for_session, dispatch_stream_mgmt(El, StateData)); wait_for_session({xmlstreamelement, El}, StateData) -> + NewStateData = update_num_stanzas_in(StateData, El), case jlib:iq_query_info(El) of #iq{type = set, xmlns = ?NS_SESSION} -> - U = StateData#state.user, - R = StateData#state.resource, - JID = StateData#state.jid, - case acl:match_rule(StateData#state.server, - StateData#state.access, JID) of + U = NewStateData#state.user, + R = NewStateData#state.resource, + JID = NewStateData#state.jid, + case acl:match_rule(NewStateData#state.server, + NewStateData#state.access, JID) of allow -> ?INFO_MSG("(~w) Opened session for ~s", - [StateData#state.socket, + [NewStateData#state.socket, jlib:jid_to_string(JID)]), - SID = {now(), self()}, - IP = StateData#state.ip, - ejabberd_sm:open_session( - SID, U, StateData#state.server, R, IP), - Res = jlib:make_result_iq_reply(El), - send_element(StateData, Res), - change_shaper(StateData, JID), + Res = jlib:make_result_iq_reply(El#xmlel{children = []}), + NewState = send_stanza(NewStateData, Res), + change_shaper(NewState, JID), {Fs, Ts} = ejabberd_hooks:run_fold( roster_get_subscription_lists, - StateData#state.server, + NewState#state.server, {[], []}, - [U, StateData#state.server]), + [U, NewState#state.server]), LJID = jlib:jid_tolower(jlib:jid_remove_resource(JID)), Fs1 = [LJID | Fs], Ts1 = [LJID | Ts], PrivList = ejabberd_hooks:run_fold( - privacy_get_user_list, StateData#state.server, - none, - [U, StateData#state.server]), - fsm_next_state(session_established, - StateData#state{ - sid = SID, + privacy_get_user_list, NewState#state.server, + #userlist{}, + [U, NewState#state.server]), + Conn = get_conn_type(NewState), + Info = [{ip, NewState#state.ip}, {conn, Conn}, + {auth_module, NewState#state.auth_module}], + ejabberd_sm:open_session( + NewState#state.sid, U, NewState#state.server, R, Info), + UpdatedStateData = + NewState#state{ + conn = Conn, pres_f = ?SETS:from_list(Fs1), pres_t = ?SETS:from_list(Ts1), - privacy_list = PrivList}); + privacy_list = PrivList}, + fsm_next_state_pack(session_established, + UpdatedStateData); _ -> + ejabberd_hooks:run(forbidden_session_hook, + NewStateData#state.server, [JID]), ?INFO_MSG("(~w) Forbidden session for ~s", - [StateData#state.socket, + [NewStateData#state.socket, jlib:jid_to_string(JID)]), Err = jlib:make_error_reply(El, ?ERR_NOT_ALLOWED), - send_element(StateData, Err), - fsm_next_state(wait_for_session, StateData) + send_element(NewStateData, Err), + fsm_next_state(wait_for_session, NewStateData) end; _ -> - fsm_next_state(wait_for_session, StateData) + fsm_next_state(wait_for_session, NewStateData) end; wait_for_session(timeout, StateData) -> {stop, normal, StateData}; - wait_for_session({xmlstreamend, _Name}, StateData) -> - send_text(StateData, ?STREAM_TRAILER), - {stop, normal, StateData}; - + send_trailer(StateData), {stop, normal, StateData}; wait_for_session({xmlstreamerror, _}, StateData) -> - send_text(StateData, ?INVALID_XML_ERR ++ ?STREAM_TRAILER), + send_element(StateData, ?INVALID_XML_ERR), + send_trailer(StateData), {stop, normal, StateData}; - wait_for_session(closed, StateData) -> {stop, normal, StateData}. - - - -session_established({xmlstreamelement, El}, StateData) -> - {xmlelement, Name, Attrs, _Els} = El, - User = StateData#state.user, - Server = StateData#state.server, - % TODO: check 'from' attribute in stanza +session_established({xmlstreamelement, #xmlel{name = Name} = El}, StateData) + when ?IS_STREAM_MGMT_TAG(Name) -> + fsm_next_state(session_established, dispatch_stream_mgmt(El, StateData)); +session_established({xmlstreamelement, + #xmlel{name = <<"active">>, + attrs = [{<<"xmlns">>, ?NS_CLIENT_STATE}]}}, + StateData) -> + NewStateData = csi_queue_flush(StateData), + fsm_next_state(session_established, NewStateData#state{csi_state = active}); +session_established({xmlstreamelement, + #xmlel{name = <<"inactive">>, + attrs = [{<<"xmlns">>, ?NS_CLIENT_STATE}]}}, + StateData) -> + fsm_next_state(session_established, StateData#state{csi_state = inactive}); +session_established({xmlstreamelement, El}, + StateData) -> FromJID = StateData#state.jid, - To = xml:get_attr_s("to", Attrs), - ToJID = case To of - "" -> - jlib:make_jid(User, Server, ""); - _ -> - jlib:string_to_jid(To) - end, - NewEl1 = jlib:remove_attr("xmlns", El), - NewEl = case xml:get_attr_s("xml:lang", Attrs) of - "" -> - case StateData#state.lang of - "" -> NewEl1; - Lang -> - xml:replace_tag_attr("xml:lang", Lang, NewEl1) - end; - _ -> - NewEl1 - end, - NewState = - case ToJID of - error -> - case xml:get_attr_s("type", Attrs) of - "error" -> StateData; - "result" -> StateData; - _ -> - Err = jlib:make_error_reply(NewEl, ?ERR_JID_MALFORMED), - send_element(StateData, Err), - StateData - end; - _ -> - case Name of - "presence" -> - PresenceEl = ejabberd_hooks:run_fold( - c2s_update_presence, - Server, - NewEl, - [User, Server]), - case ToJID of - #jid{user = User, - server = Server, - resource = ""} -> - ?DEBUG("presence_update(~p,~n\t~p,~n\t~p)", - [FromJID, PresenceEl, StateData]), - presence_update(FromJID, PresenceEl, - StateData); - _ -> - presence_track(FromJID, ToJID, PresenceEl, - StateData) - end; - "iq" -> - case StateData#state.privacy_list of - none -> - ejabberd_router:route(FromJID, ToJID, NewEl), - StateData; - _PrivList -> - case jlib:iq_query_info(NewEl) of - #iq{xmlns = ?NS_PRIVACY} = IQ -> - process_privacy_iq( - FromJID, ToJID, IQ, StateData); - _ -> - ejabberd_hooks:run( - user_send_packet, - Server, - [FromJID, ToJID, NewEl]), - ejabberd_router:route( - FromJID, ToJID, NewEl), - StateData - end - end; - "message" -> - ejabberd_hooks:run(user_send_packet, - Server, - [FromJID, ToJID, NewEl]), - ejabberd_router:route(FromJID, ToJID, NewEl), - StateData; - _ -> - StateData - end - end, - ejabberd_hooks:run(c2s_loop_debug, [{xmlstreamelement, El}]), - fsm_next_state(session_established, NewState); - + case check_from(El, FromJID) of + 'invalid-from' -> + send_element(StateData, ?INVALID_FROM), + send_trailer(StateData), + {stop, normal, StateData}; + _NewEl -> + session_established2(El, StateData) + end; %% We hibernate the process to reduce memory consumption after a %% configurable activity timeout session_established(timeout, StateData) -> - %% TODO: Options must be stored in state: Options = [], - proc_lib:hibernate(gen_fsm, enter_loop, + proc_lib:hibernate(?GEN_FSM, enter_loop, [?MODULE, Options, session_established, StateData]), fsm_next_state(session_established, StateData); - session_established({xmlstreamend, _Name}, StateData) -> - send_text(StateData, ?STREAM_TRAILER), + send_trailer(StateData), {stop, normal, StateData}; +session_established({xmlstreamerror, + <<"XML stanza is too big">> = E}, + StateData) -> + send_element(StateData, + ?POLICY_VIOLATION_ERR((StateData#state.lang), E)), + send_trailer(StateData), {stop, normal, StateData}; - session_established({xmlstreamerror, _}, StateData) -> - send_text(StateData, ?INVALID_XML_ERR ++ ?STREAM_TRAILER), + send_element(StateData, ?INVALID_XML_ERR), + send_trailer(StateData), {stop, normal, StateData}; - +session_established(closed, #state{mgmt_state = active} = StateData) -> + fsm_next_state(wait_for_resume, StateData); session_established(closed, StateData) -> {stop, normal, StateData}. +%% Process packets sent by user (coming from user on c2s XMPP +%% connection) +session_established2(El, StateData) -> + #xmlel{name = Name, attrs = Attrs} = El, + NewStateData = update_num_stanzas_in(StateData, El), + User = NewStateData#state.user, + Server = NewStateData#state.server, + FromJID = NewStateData#state.jid, + To = xml:get_attr_s(<<"to">>, Attrs), + ToJID = case To of + <<"">> -> jlib:make_jid(User, Server, <<"">>); + _ -> jlib:string_to_jid(To) + end, + NewEl1 = jlib:remove_attr(<<"xmlns">>, El), + NewEl = case xml:get_attr_s(<<"xml:lang">>, Attrs) of + <<"">> -> + case NewStateData#state.lang of + <<"">> -> NewEl1; + Lang -> + xml:replace_tag_attr(<<"xml:lang">>, Lang, NewEl1) + end; + _ -> NewEl1 + end, + NewState = case ToJID of + error -> + case xml:get_attr_s(<<"type">>, Attrs) of + <<"error">> -> NewStateData; + <<"result">> -> NewStateData; + _ -> + Err = jlib:make_error_reply(NewEl, + ?ERR_JID_MALFORMED), + send_packet(NewStateData, Err) + end; + _ -> + case Name of + <<"presence">> -> + PresenceEl = + ejabberd_hooks:run_fold(c2s_update_presence, + Server, NewEl, + [User, Server]), + ejabberd_hooks:run(user_send_packet, Server, + [FromJID, ToJID, PresenceEl]), + case ToJID of + #jid{user = User, server = Server, + resource = <<"">>} -> + ?DEBUG("presence_update(~p,~n\t~p,~n\t~p)", + [FromJID, PresenceEl, NewStateData]), + presence_update(FromJID, PresenceEl, + NewStateData); + _ -> + presence_track(FromJID, ToJID, PresenceEl, + NewStateData) + end; + <<"iq">> -> + case jlib:iq_query_info(NewEl) of + #iq{xmlns = Xmlns} = IQ + when Xmlns == (?NS_PRIVACY); + Xmlns == (?NS_BLOCKING) -> + process_privacy_iq(FromJID, ToJID, IQ, + NewStateData); + _ -> + ejabberd_hooks:run(user_send_packet, Server, + [FromJID, ToJID, NewEl]), + check_privacy_route(FromJID, NewStateData, + FromJID, ToJID, NewEl), + NewStateData + end; + <<"message">> -> + ejabberd_hooks:run(user_send_packet, Server, + [FromJID, ToJID, NewEl]), + check_privacy_route(FromJID, NewStateData, FromJID, + ToJID, NewEl), + NewStateData; + _ -> NewStateData + end + end, + ejabberd_hooks:run(c2s_loop_debug, + [{xmlstreamelement, El}]), + fsm_next_state(session_established, NewState). +wait_for_resume({xmlstreamelement, _El} = Event, StateData) -> + session_established(Event, StateData), + fsm_next_state(wait_for_resume, StateData); +wait_for_resume(timeout, StateData) -> + ?DEBUG("Timed out waiting for resumption of stream for ~s", + [jlib:jid_to_string(StateData#state.jid)]), + {stop, normal, StateData}; +wait_for_resume(Event, StateData) -> + ?DEBUG("Ignoring event while waiting for resumption: ~p", [Event]), + fsm_next_state(wait_for_resume, StateData). %%---------------------------------------------------------------------- %% Func: StateName/3 @@ -930,7 +1322,7 @@ session_established(closed, StateData) -> %% {reply, Reply, NextStateName, NextStateData} | %% {reply, Reply, NextStateName, NextStateData, Timeout} | %% {stop, Reason, NewStateData} | -%% {stop, Reason, Reply, NewStateData} +%% {stop, Reason, Reply, NewStateData} %%---------------------------------------------------------------------- %state_name(Event, From, StateData) -> % Reply = ok, @@ -940,7 +1332,7 @@ session_established(closed, StateData) -> %% Func: handle_event/3 %% Returns: {next_state, NextStateName, NextStateData} | %% {next_state, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} +%% {stop, Reason, NewStateData} %%---------------------------------------------------------------------- handle_event(_Event, StateName, StateData) -> fsm_next_state(StateName, StateData). @@ -952,22 +1344,36 @@ handle_event(_Event, StateName, StateData) -> %% {reply, Reply, NextStateName, NextStateData} | %% {reply, Reply, NextStateName, NextStateData, Timeout} | %% {stop, Reason, NewStateData} | -%% {stop, Reason, Reply, NewStateData} +%% {stop, Reason, Reply, NewStateData} %%---------------------------------------------------------------------- -handle_sync_event({get_presence}, _From, StateName, StateData) -> +handle_sync_event({get_presence}, _From, StateName, + StateData) -> User = StateData#state.user, PresLast = StateData#state.pres_last, - Show = get_showtag(PresLast), Status = get_statustag(PresLast), Resource = StateData#state.resource, - Reply = {User, Resource, Show, Status}, fsm_reply(Reply, StateName, StateData); - -handle_sync_event(_Event, _From, StateName, StateData) -> - Reply = ok, - fsm_reply(Reply, StateName, StateData). +handle_sync_event(get_subscribed, _From, StateName, + StateData) -> + Subscribed = (?SETS):to_list(StateData#state.pres_f), + {reply, Subscribed, StateName, StateData}; +handle_sync_event({resume_session, Time}, _From, _StateName, + StateData) when element(1, StateData#state.sid) == Time -> + %% The old session should be closed before the new one is opened, so we do + %% this here instead of leaving it to the terminate callback + ejabberd_sm:close_session(StateData#state.sid, + StateData#state.user, + StateData#state.server, + StateData#state.resource), + {stop, normal, {ok, StateData}, StateData#state{mgmt_state = resumed}}; +handle_sync_event({resume_session, _Time}, _From, StateName, + StateData) -> + {reply, {error, <<"Previous session not found">>}, StateName, StateData}; +handle_sync_event(_Event, _From, StateName, + StateData) -> + Reply = ok, fsm_reply(Reply, StateName, StateData). code_change(_OldVsn, StateName, StateData, _Extra) -> {ok, StateName, StateData}. @@ -976,297 +1382,453 @@ code_change(_OldVsn, StateName, StateData, _Extra) -> %% Func: handle_info/3 %% Returns: {next_state, NextStateName, NextStateData} | %% {next_state, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} +%% {stop, Reason, NewStateData} %%---------------------------------------------------------------------- handle_info({send_text, Text}, StateName, StateData) -> send_text(StateData, Text), ejabberd_hooks:run(c2s_loop_debug, [Text]), - fsm_next_state(StateName, StateData); -handle_info(replaced, _StateName, StateData) -> +handle_info(replaced, StateName, StateData) -> Lang = StateData#state.lang, - send_text(StateData, - xml:element_to_string( - ?SERRT_CONFLICT(Lang, "Replaced by new connection")) - ++ ?STREAM_TRAILER), - {stop, normal, StateData#state{authenticated = replaced}}; -handle_info({route, From, To, Packet}, StateName, StateData) -> - {xmlelement, Name, Attrs, Els} = Packet, - {Pass, NewAttrs, NewState} = - case Name of - "presence" -> - case xml:get_attr_s("type", Attrs) of - "probe" -> - LFrom = jlib:jid_tolower(From), - LBFrom = jlib:jid_remove_resource(LFrom), - NewStateData = - case ?SETS:is_element( - LFrom, StateData#state.pres_a) orelse - ?SETS:is_element( - LBFrom, StateData#state.pres_a) of - true -> - StateData; - false -> - case ?SETS:is_element( - LFrom, StateData#state.pres_f) of - true -> - A = ?SETS:add_element( - LFrom, - StateData#state.pres_a), - StateData#state{pres_a = A}; - false -> - case ?SETS:is_element( - LBFrom, StateData#state.pres_f) of - true -> - A = ?SETS:add_element( - LBFrom, - StateData#state.pres_a), - StateData#state{pres_a = A}; - false -> - StateData - end - end - end, - process_presence_probe(From, To, NewStateData), - {false, Attrs, NewStateData}; - "error" -> - NewA = remove_element(jlib:jid_tolower(From), - StateData#state.pres_a), - {true, Attrs, StateData#state{pres_a = NewA}}; - "invisible" -> - Attrs1 = lists:keydelete("type", 1, Attrs), - {true, [{"type", "unavailable"} | Attrs1], StateData}; - "subscribe" -> - {true, Attrs, StateData}; - "subscribed" -> - {true, Attrs, StateData}; - "unsubscribe" -> - {true, Attrs, StateData}; - "unsubscribed" -> - {true, Attrs, StateData}; - _ -> - case ejabberd_hooks:run_fold( - privacy_check_packet, StateData#state.server, - allow, - [StateData#state.user, - StateData#state.server, - StateData#state.privacy_list, - {From, To, Packet}, - in]) of - allow -> - LFrom = jlib:jid_tolower(From), - LBFrom = jlib:jid_remove_resource(LFrom), - case ?SETS:is_element( - LFrom, StateData#state.pres_a) orelse - ?SETS:is_element( - LBFrom, StateData#state.pres_a) of - true -> - {true, Attrs, StateData}; - false -> - case ?SETS:is_element( - LFrom, StateData#state.pres_f) of - true -> - A = ?SETS:add_element( - LFrom, - StateData#state.pres_a), - {true, Attrs, - StateData#state{pres_a = A}}; - false -> - case ?SETS:is_element( - LBFrom, StateData#state.pres_f) of - true -> - A = ?SETS:add_element( - LBFrom, - StateData#state.pres_a), - {true, Attrs, - StateData#state{pres_a = A}}; - false -> - {true, Attrs, StateData} - end - end - end; - deny -> - {false, Attrs, StateData} - end - end; - "broadcast" -> - ?DEBUG("broadcast~n~p~n", [Els]), - case Els of - [{item, IJID, ISubscription}] -> - {false, Attrs, - roster_change(IJID, ISubscription, - StateData)}; - [{exit, Reason}] -> - {exit, Attrs, Reason}; - [{privacy_list, PrivList, PrivListName}] -> - case ejabberd_hooks:run_fold( - privacy_updated_list, StateData#state.server, - false, - [StateData#state.privacy_list, - PrivList]) of - false -> - {false, Attrs, StateData}; - NewPL -> - PrivPushIQ = - #iq{type = set, xmlns = ?NS_PRIVACY, - id = "push", - sub_el = [{xmlelement, "query", - [{"xmlns", ?NS_PRIVACY}], - [{xmlelement, "list", - [{"name", PrivListName}], - []}]}]}, - PrivPushEl = - jlib:replace_from_to( - jlib:jid_remove_resource( - StateData#state.jid), - StateData#state.jid, - jlib:iq_to_xml(PrivPushIQ)), - send_element(StateData, PrivPushEl), - {false, Attrs, StateData#state{privacy_list = NewPL}} - end; - _ -> - {false, Attrs, StateData} - end; - "iq" -> - IQ = jlib:iq_query_info(Packet), - case IQ of - #iq{xmlns = ?NS_VCARD} -> - Host = StateData#state.server, - case ets:lookup(sm_iqtable, {?NS_VCARD, Host}) of - [{_, Module, Function, Opts}] -> - gen_iq_handler:handle(Host, Module, Function, Opts, - From, To, IQ); - [] -> - Err = jlib:make_error_reply( - Packet, ?ERR_FEATURE_NOT_IMPLEMENTED), - ejabberd_router:route(To, From, Err) - end, - {false, Attrs, StateData}; - #iq{} -> - case ejabberd_hooks:run_fold( - privacy_check_packet, StateData#state.server, - allow, - [StateData#state.user, - StateData#state.server, - StateData#state.privacy_list, - {From, To, Packet}, - in]) of - allow -> - {true, Attrs, StateData}; - deny -> - Err = jlib:make_error_reply( - Packet, ?ERR_FEATURE_NOT_IMPLEMENTED), - ejabberd_router:route(To, From, Err), - {false, Attrs, StateData} - end; - _ -> - {true, Attrs, StateData} - end; - "message" -> - case ejabberd_hooks:run_fold( - privacy_check_packet, StateData#state.server, - allow, - [StateData#state.user, - StateData#state.server, - StateData#state.privacy_list, - {From, To, Packet}, - in]) of - allow -> - {true, Attrs, StateData}; - deny -> - {false, Attrs, StateData} - end; - _ -> - {true, Attrs, StateData} - end, - if - Pass == exit -> - catch send_text(StateData, ?STREAM_TRAILER), + Xmlelement = ?SERRT_CONFLICT(Lang, <<"Replaced by new connection">>), + handle_info({kick, replaced, Xmlelement}, StateName, StateData); +handle_info(kick, StateName, StateData) -> + Lang = StateData#state.lang, + Xmlelement = ?SERRT_POLICY_VIOLATION(Lang, <<"has been kicked">>), + handle_info({kick, kicked_by_admin, Xmlelement}, StateName, StateData); +handle_info({kick, Reason, Xmlelement}, _StateName, StateData) -> + send_element(StateData, Xmlelement), + send_trailer(StateData), + {stop, normal, + StateData#state{authenticated = Reason}}; +handle_info({route, _From, _To, {broadcast, Data}}, + StateName, StateData) -> + ?DEBUG("broadcast~n~p~n", [Data]), + case Data of + {item, IJID, ISubscription} -> + fsm_next_state(StateName, + roster_change(IJID, ISubscription, StateData)); + {exit, Reason} -> + Lang = StateData#state.lang, + send_element(StateData, ?SERRT_CONFLICT(Lang, Reason)), + catch send_trailer(StateData), + {stop, normal, StateData}; + {privacy_list, PrivList, PrivListName} -> + case ejabberd_hooks:run_fold(privacy_updated_list, + StateData#state.server, + false, + [StateData#state.privacy_list, + PrivList]) of + false -> + fsm_next_state(StateName, StateData); + NewPL -> + PrivPushIQ = #iq{type = set, + id = <<"push", + (randoms:get_string())/binary>>, + sub_el = + [#xmlel{name = <<"query">>, + attrs = [{<<"xmlns">>, + ?NS_PRIVACY}], + children = + [#xmlel{name = <<"list">>, + attrs = [{<<"name">>, + PrivListName}], + children = []}]}]}, + PrivPushEl = jlib:replace_from_to( + jlib:jid_remove_resource(StateData#state.jid), + StateData#state.jid, + jlib:iq_to_xml(PrivPushIQ)), + NewState = send_stanza(StateData, PrivPushEl), + fsm_next_state(StateName, + NewState#state{privacy_list = NewPL}) + end; + {blocking, What} -> + NewState = route_blocking(What, StateData), + fsm_next_state(StateName, NewState); + _ -> + fsm_next_state(StateName, StateData) + end; +%% Process Packets that are to be send to the user +handle_info({route, From, To, + #xmlel{name = Name, attrs = Attrs, children = Els} = Packet}, + StateName, StateData) -> + {Pass, NewAttrs, NewState} = case Name of + <<"presence">> -> + State = + ejabberd_hooks:run_fold(c2s_presence_in, + StateData#state.server, + StateData, + [{From, To, + Packet}]), + case xml:get_attr_s(<<"type">>, Attrs) of + <<"probe">> -> + LFrom = jlib:jid_tolower(From), + LBFrom = + jlib:jid_remove_resource(LFrom), + NewStateData = case + (?SETS):is_element(LFrom, + State#state.pres_a) + orelse + (?SETS):is_element(LBFrom, + State#state.pres_a) + of + true -> State; + false -> + case + (?SETS):is_element(LFrom, + State#state.pres_f) + of + true -> + A = + (?SETS):add_element(LFrom, + State#state.pres_a), + State#state{pres_a + = + A}; + false -> + case + (?SETS):is_element(LBFrom, + State#state.pres_f) + of + true -> + A = + (?SETS):add_element(LBFrom, + State#state.pres_a), + State#state{pres_a + = + A}; + false -> + State + end + end + end, + process_presence_probe(From, To, + NewStateData), + {false, Attrs, NewStateData}; + <<"error">> -> + NewA = + remove_element(jlib:jid_tolower(From), + State#state.pres_a), + {true, Attrs, + State#state{pres_a = NewA}}; + <<"subscribe">> -> + SRes = is_privacy_allow(State, + From, To, + Packet, + in), + {SRes, Attrs, State}; + <<"subscribed">> -> + SRes = is_privacy_allow(State, + From, To, + Packet, + in), + {SRes, Attrs, State}; + <<"unsubscribe">> -> + SRes = is_privacy_allow(State, + From, To, + Packet, + in), + {SRes, Attrs, State}; + <<"unsubscribed">> -> + SRes = is_privacy_allow(State, + From, To, + Packet, + in), + {SRes, Attrs, State}; + _ -> + case privacy_check_packet(State, + From, To, + Packet, + in) + of + allow -> + LFrom = + jlib:jid_tolower(From), + LBFrom = + jlib:jid_remove_resource(LFrom), + case + (?SETS):is_element(LFrom, + State#state.pres_a) + orelse + (?SETS):is_element(LBFrom, + State#state.pres_a) + of + true -> + {true, Attrs, State}; + false -> + case + (?SETS):is_element(LFrom, + State#state.pres_f) + of + true -> + A = + (?SETS):add_element(LFrom, + State#state.pres_a), + {true, Attrs, + State#state{pres_a + = + A}}; + false -> + case + (?SETS):is_element(LBFrom, + State#state.pres_f) + of + true -> + A = + (?SETS):add_element(LBFrom, + State#state.pres_a), + {true, + Attrs, + State#state{pres_a + = + A}}; + false -> + {true, + Attrs, + State} + end + end + end; + deny -> {false, Attrs, State} + end + end; + <<"iq">> -> + IQ = jlib:iq_query_info(Packet), + case IQ of + #iq{xmlns = ?NS_LAST} -> + LFrom = jlib:jid_tolower(From), + LBFrom = + jlib:jid_remove_resource(LFrom), + HasFromSub = + ((?SETS):is_element(LFrom, + StateData#state.pres_f) + orelse + (?SETS):is_element(LBFrom, + StateData#state.pres_f)) + andalso + is_privacy_allow(StateData, + To, From, + #xmlel{name + = + <<"presence">>, + attrs + = + [], + children + = + []}, + out), + case HasFromSub of + true -> + case + privacy_check_packet(StateData, + From, + To, + Packet, + in) + of + allow -> + {true, Attrs, + StateData}; + deny -> + {false, Attrs, + StateData} + end; + _ -> + Err = + jlib:make_error_reply(Packet, + ?ERR_FORBIDDEN), + ejabberd_router:route(To, + From, + Err), + {false, Attrs, StateData} + end; + IQ + when is_record(IQ, iq) or + (IQ == reply) -> + case + privacy_check_packet(StateData, + From, To, + Packet, in) + of + allow -> + {true, Attrs, StateData}; + deny when is_record(IQ, iq) -> + Err = + jlib:make_error_reply(Packet, + ?ERR_SERVICE_UNAVAILABLE), + ejabberd_router:route(To, + From, + Err), + {false, Attrs, StateData}; + deny when IQ == reply -> + {false, Attrs, StateData} + end; + IQ + when (IQ == invalid) or + (IQ == not_iq) -> + {false, Attrs, StateData} + end; + <<"message">> -> + case privacy_check_packet(StateData, + From, To, + Packet, in) + of + allow -> {true, Attrs, StateData}; + deny -> {false, Attrs, StateData} + end; + _ -> {true, Attrs, StateData} + end, + if Pass == exit -> + %% When Pass==exit, NewState contains a string instead of a #state{} + Lang = StateData#state.lang, + send_element(StateData, ?SERRT_CONFLICT(Lang, NewState)), + send_trailer(StateData), {stop, normal, StateData}; Pass -> - Attrs2 = jlib:replace_from_to_attrs(jlib:jid_to_string(From), - jlib:jid_to_string(To), - NewAttrs), - FixedPacket = {xmlelement, Name, Attrs2, Els}, - Text = xml:element_to_string(FixedPacket), - send_text(StateData, Text), + Attrs2 = + jlib:replace_from_to_attrs(jlib:jid_to_string(From), + jlib:jid_to_string(To), NewAttrs), + FixedPacket = #xmlel{name = Name, attrs = Attrs2, children = Els}, + SentStateData = send_packet(NewState, FixedPacket), ejabberd_hooks:run(user_receive_packet, - StateData#state.server, - [StateData#state.jid, From, To, FixedPacket]), + SentStateData#state.server, + [SentStateData#state.jid, From, To, FixedPacket]), ejabberd_hooks:run(c2s_loop_debug, [{route, From, To, Packet}]), - fsm_next_state(StateName, NewState); + fsm_next_state(StateName, SentStateData); true -> ejabberd_hooks:run(c2s_loop_debug, [{route, From, To, Packet}]), fsm_next_state(StateName, NewState) end; -handle_info({'DOWN', Monitor, _Type, _Object, _Info}, _StateName, StateData) - when Monitor == StateData#state.socket_monitor -> +handle_info({'DOWN', Monitor, _Type, _Object, _Info}, + _StateName, StateData) + when Monitor == StateData#state.socket_monitor -> + if StateData#state.mgmt_state == active; + StateData#state.mgmt_state == pending -> + fsm_next_state(wait_for_resume, StateData); + true -> + {stop, normal, StateData} + end; +handle_info(system_shutdown, StateName, StateData) -> + case StateName of + wait_for_stream -> + send_header(StateData, ?MYNAME, <<"1.0">>, <<"en">>), + send_element(StateData, ?SERR_SYSTEM_SHUTDOWN), + send_trailer(StateData), + ok; + _ -> + send_element(StateData, ?SERR_SYSTEM_SHUTDOWN), + send_trailer(StateData), + ok + end, {stop, normal, StateData}; +handle_info({force_update_presence, LUser}, StateName, + #state{user = LUser, server = LServer} = StateData) -> + NewStateData = case StateData#state.pres_last of + #xmlel{name = <<"presence">>} -> + PresenceEl = + ejabberd_hooks:run_fold(c2s_update_presence, + LServer, + StateData#state.pres_last, + [LUser, LServer]), + StateData2 = StateData#state{pres_last = PresenceEl}, + presence_update(StateData2#state.jid, PresenceEl, + StateData2), + StateData2; + _ -> StateData + end, + fsm_next_state(StateName, NewStateData); +handle_info({broadcast, Type, From, Packet}, StateName, StateData) -> + Recipients = ejabberd_hooks:run_fold( + c2s_broadcast_recipients, StateData#state.server, + [], + [StateData#state.server, StateData, Type, From, Packet]), + lists:foreach( + fun(USR) -> + ejabberd_router:route( + From, jlib:make_jid(USR), Packet) + end, lists:usort(Recipients)), + fsm_next_state(StateName, StateData); handle_info(Info, StateName, StateData) -> ?ERROR_MSG("Unexpected info: ~p", [Info]), fsm_next_state(StateName, StateData). + +%%---------------------------------------------------------------------- +%% Func: print_state/1 +%% Purpose: Prepare the state to be printed on error log +%% Returns: State to print +%%---------------------------------------------------------------------- +print_state(State = #state{pres_t = T, pres_f = F, pres_a = A}) -> + State#state{pres_t = {pres_t, ?SETS:size(T)}, + pres_f = {pres_f, ?SETS:size(F)}, + pres_a = {pres_a, ?SETS:size(A)} + }. + %%---------------------------------------------------------------------- %% Func: terminate/3 %% Purpose: Shutdown the fsm %% Returns: any %%---------------------------------------------------------------------- terminate(_Reason, StateName, StateData) -> - case StateName of - session_established -> - case StateData#state.authenticated of - replaced -> - ?INFO_MSG("(~w) Replaced session for ~s", - [StateData#state.socket, - jlib:jid_to_string(StateData#state.jid)]), - From = StateData#state.jid, - Packet = {xmlelement, "presence", - [{"type", "unavailable"}], - [{xmlelement, "status", [], - [{xmlcdata, "Replaced by new connection"}]}]}, - ejabberd_sm:close_session_unset_presence( - StateData#state.sid, - StateData#state.user, - StateData#state.server, - StateData#state.resource, - "Replaced by new connection"), - presence_broadcast( - StateData, From, StateData#state.pres_a, Packet), - presence_broadcast( - StateData, From, StateData#state.pres_i, Packet); - _ -> - ?INFO_MSG("(~w) Close session for ~s", - [StateData#state.socket, - jlib:jid_to_string(StateData#state.jid)]), - - EmptySet = ?SETS:new(), - case StateData of - #state{pres_last = undefined, - pres_a = EmptySet, - pres_i = EmptySet, - pres_invis = false} -> - ejabberd_sm:close_session(StateData#state.sid, - StateData#state.user, - StateData#state.server, - StateData#state.resource); - _ -> - From = StateData#state.jid, - Packet = {xmlelement, "presence", - [{"type", "unavailable"}], []}, - ejabberd_sm:close_session_unset_presence( - StateData#state.sid, - StateData#state.user, - StateData#state.server, - StateData#state.resource, - ""), - presence_broadcast( - StateData, From, StateData#state.pres_a, Packet), - presence_broadcast( - StateData, From, StateData#state.pres_i, Packet) - end - end; - _ -> - ok + case StateData#state.mgmt_state of + resumed -> + ?INFO_MSG("Closing former stream of resumed session for ~s", + [jlib:jid_to_string(StateData#state.jid)]); + _ -> + if StateName == session_established; + StateName == wait_for_resume -> + case StateData#state.authenticated of + replaced -> + ?INFO_MSG("(~w) Replaced session for ~s", + [StateData#state.socket, + jlib:jid_to_string(StateData#state.jid)]), + From = StateData#state.jid, + Packet = #xmlel{name = <<"presence">>, + attrs = [{<<"type">>, <<"unavailable">>}], + children = + [#xmlel{name = <<"status">>, attrs = [], + children = + [{xmlcdata, + <<"Replaced by new connection">>}]}]}, + ejabberd_sm:close_session_unset_presence(StateData#state.sid, + StateData#state.user, + StateData#state.server, + StateData#state.resource, + <<"Replaced by new connection">>), + presence_broadcast(StateData, From, + StateData#state.pres_a, Packet), + handle_unacked_stanzas(StateData); + _ -> + ?INFO_MSG("(~w) Close session for ~s", + [StateData#state.socket, + jlib:jid_to_string(StateData#state.jid)]), + EmptySet = (?SETS):new(), + case StateData of + #state{pres_last = undefined, pres_a = EmptySet} -> + ejabberd_sm:close_session(StateData#state.sid, + StateData#state.user, + StateData#state.server, + StateData#state.resource); + _ -> + From = StateData#state.jid, + Packet = #xmlel{name = <<"presence">>, + attrs = [{<<"type">>, <<"unavailable">>}], + children = []}, + ejabberd_sm:close_session_unset_presence(StateData#state.sid, + StateData#state.user, + StateData#state.server, + StateData#state.resource, + <<"">>), + presence_broadcast(StateData, From, + StateData#state.pres_a, Packet) + end, + handle_unacked_stanzas(StateData) + end, + bounce_messages(); + true -> + ok + end end, (StateData#state.sockmod):close(StateData#state.socket), ok. @@ -1278,309 +1840,386 @@ terminate(_Reason, StateName, StateData) -> change_shaper(StateData, JID) -> Shaper = acl:match_rule(StateData#state.server, StateData#state.shaper, JID), - (StateData#state.sockmod):change_shaper(StateData#state.socket, Shaper). + (StateData#state.sockmod):change_shaper(StateData#state.socket, + Shaper). +send_text(StateData, Text) when StateData#state.mgmt_state == pending -> + ?DEBUG("Cannot send text while waiting for resumption: ~p", [Text]); +send_text(StateData, Text) when StateData#state.xml_socket -> + ?DEBUG("Send Text on stream = ~p", [Text]), + (StateData#state.sockmod):send_xml(StateData#state.socket, + {xmlstreamraw, Text}); +send_text(StateData, Text) when StateData#state.mgmt_state == active -> + ?DEBUG("Send XML on stream = ~p", [Text]), + case catch (StateData#state.sockmod):send(StateData#state.socket, Text) of + {'EXIT', _} -> + (StateData#state.sockmod):close(StateData#state.socket), + error; + _ -> + ok + end; send_text(StateData, Text) -> - ?DEBUG("Send XML on stream = ~p", [lists:flatten(Text)]), + ?DEBUG("Send XML on stream = ~p", [Text]), (StateData#state.sockmod):send(StateData#state.socket, Text). +send_element(StateData, El) when StateData#state.mgmt_state == pending -> + ?DEBUG("Cannot send element while waiting for resumption: ~p", [El]); +send_element(StateData, El) when StateData#state.xml_socket -> + (StateData#state.sockmod):send_xml(StateData#state.socket, + {xmlstreamelement, El}); send_element(StateData, El) -> - send_text(StateData, xml:element_to_string(El)). + send_text(StateData, xml:element_to_binary(El)). +send_stanza(StateData, Stanza) when StateData#state.csi_state == inactive -> + csi_filter_stanza(StateData, Stanza); +send_stanza(StateData, Stanza) when StateData#state.mgmt_state == pending -> + mgmt_queue_add(StateData, Stanza); +send_stanza(StateData, Stanza) when StateData#state.mgmt_state == active -> + NewStateData = case send_stanza_and_ack_req(StateData, Stanza) of + ok -> + StateData; + error -> + StateData#state{mgmt_state = pending} + end, + mgmt_queue_add(NewStateData, Stanza); +send_stanza(StateData, Stanza) -> + send_element(StateData, Stanza), + StateData. -new_id() -> - randoms:get_string(). +send_packet(StateData, Packet) -> + case is_stanza(Packet) of + true -> + send_stanza(StateData, Packet); + false -> + send_element(StateData, Packet), + StateData + end. +send_header(StateData, Server, Version, Lang) + when StateData#state.xml_socket -> + VersionAttr = case Version of + <<"">> -> []; + _ -> [{<<"version">>, Version}] + end, + LangAttr = case Lang of + <<"">> -> []; + _ -> [{<<"xml:lang">>, Lang}] + end, + Header = {xmlstreamstart, <<"stream:stream">>, + VersionAttr ++ + LangAttr ++ + [{<<"xmlns">>, <<"jabber:client">>}, + {<<"xmlns:stream">>, + <<"http://etherx.jabber.org/streams">>}, + {<<"id">>, StateData#state.streamid}, + {<<"from">>, Server}]}, + (StateData#state.sockmod):send_xml(StateData#state.socket, + Header); +send_header(StateData, Server, Version, Lang) -> + VersionStr = case Version of + <<"">> -> <<"">>; + _ -> [<<" version='">>, Version, <<"'">>] + end, + LangStr = case Lang of + <<"">> -> <<"">>; + _ -> [<<" xml:lang='">>, Lang, <<"'">>] + end, + Header = io_lib:format(?STREAM_HEADER, + [StateData#state.streamid, Server, VersionStr, + LangStr]), + send_text(StateData, iolist_to_binary(Header)). + +send_trailer(StateData) + when StateData#state.mgmt_state == pending -> + ?DEBUG("Cannot send stream trailer while waiting for resumption", []); +send_trailer(StateData) + when StateData#state.xml_socket -> + (StateData#state.sockmod):send_xml(StateData#state.socket, + {xmlstreamend, <<"stream:stream">>}); +send_trailer(StateData) -> + send_text(StateData, ?STREAM_TRAILER). + +new_id() -> randoms:get_string(). is_auth_packet(El) -> case jlib:iq_query_info(El) of #iq{id = ID, type = Type, xmlns = ?NS_AUTH, sub_el = SubEl} -> - {xmlelement, _, _, Els} = SubEl, + #xmlel{children = Els} = SubEl, {auth, ID, Type, - get_auth_tags(Els, "", "", "", "")}; - _ -> - false + get_auth_tags(Els, <<"">>, <<"">>, <<"">>, <<"">>)}; + _ -> false end. +is_stanza(#xmlel{name = Name, attrs = Attrs}) when Name == <<"message">>; + Name == <<"presence">>; + Name == <<"iq">> -> + case xml:get_attr(<<"xmlns">>, Attrs) of + {value, NS} when NS /= <<"jabber:client">>, + NS /= <<"jabber:server">> -> + false; + _ -> + true + end; +is_stanza(_El) -> + false. -get_auth_tags([{xmlelement, Name, _Attrs, Els}| L], U, P, D, R) -> +get_auth_tags([#xmlel{name = Name, children = Els} | L], + U, P, D, R) -> CData = xml:get_cdata(Els), case Name of - "username" -> - get_auth_tags(L, CData, P, D, R); - "password" -> - get_auth_tags(L, U, CData, D, R); - "digest" -> - get_auth_tags(L, U, P, CData, R); - "resource" -> - get_auth_tags(L, U, P, D, CData); - _ -> - get_auth_tags(L, U, P, D, R) + <<"username">> -> get_auth_tags(L, CData, P, D, R); + <<"password">> -> get_auth_tags(L, U, CData, D, R); + <<"digest">> -> get_auth_tags(L, U, P, CData, R); + <<"resource">> -> get_auth_tags(L, U, P, D, CData); + _ -> get_auth_tags(L, U, P, D, R) end; get_auth_tags([_ | L], U, P, D, R) -> get_auth_tags(L, U, P, D, R); get_auth_tags([], U, P, D, R) -> {U, P, D, R}. +%% Copied from ejabberd_socket.erl +-record(socket_state, {sockmod, socket, receiver}). + +get_conn_type(StateData) -> + case (StateData#state.sockmod):get_sockmod(StateData#state.socket) of + gen_tcp -> c2s; + p1_tls -> c2s_tls; + ezlib -> + case ezlib:get_sockmod((StateData#state.socket)#socket_state.socket) of + gen_tcp -> c2s_compressed; + p1_tls -> c2s_compressed_tls + end; + ejabberd_http_poll -> http_poll; + ejabberd_http_bind -> http_bind; + _ -> unknown + end. process_presence_probe(From, To, StateData) -> LFrom = jlib:jid_tolower(From), - LBFrom = setelement(3, LFrom, ""), + LBFrom = setelement(3, LFrom, <<"">>), case StateData#state.pres_last of undefined -> ok; _ -> - Cond1 = (not StateData#state.pres_invis) - andalso (?SETS:is_element(LFrom, StateData#state.pres_f) - orelse - ((LFrom /= LBFrom) andalso - ?SETS:is_element(LBFrom, StateData#state.pres_f))) - andalso (not - (?SETS:is_element(LFrom, StateData#state.pres_i) - orelse - ((LFrom /= LBFrom) andalso - ?SETS:is_element(LBFrom, StateData#state.pres_i)))), - Cond2 = StateData#state.pres_invis - andalso ?SETS:is_element(LFrom, StateData#state.pres_f) - andalso ?SETS:is_element(LFrom, StateData#state.pres_a), + Cond = ?SETS:is_element(LFrom, StateData#state.pres_f) + orelse + ((LFrom /= LBFrom) andalso + ?SETS:is_element(LBFrom, StateData#state.pres_f)), if - Cond1 -> - Packet = StateData#state.pres_last, - case ejabberd_hooks:run_fold( - privacy_check_packet, StateData#state.server, - allow, - [StateData#state.user, - StateData#state.server, - StateData#state.privacy_list, - {To, From, Packet}, - out]) of + Cond -> + Timestamp = StateData#state.pres_timestamp, + Packet = xml:append_subtags( + StateData#state.pres_last, + %% To is the one sending the presence (the target of the probe) + [jlib:timestamp_to_xml(Timestamp, utc, To, <<"">>), + %% TODO: Delete the next line once XEP-0091 is Obsolete + jlib:timestamp_to_xml(Timestamp)]), + case privacy_check_packet(StateData, To, From, Packet, out) of deny -> ok; allow -> - ejabberd_router:route(To, From, Packet) + Pid=element(2, StateData#state.sid), + ejabberd_hooks:run(presence_probe_hook, StateData#state.server, [From, To, Pid]), + %% Don't route a presence probe to oneself + case From == To of + false -> + ejabberd_router:route(To, From, Packet); + true -> + ok + end end; - Cond2 -> - ejabberd_router:route(To, From, - {xmlelement, "presence", - [], - []}); true -> ok end end. +%% User updates his presence (non-directed presence packet) presence_update(From, Packet, StateData) -> - {xmlelement, _Name, Attrs, _Els} = Packet, - case xml:get_attr_s("type", Attrs) of - "unavailable" -> - Status = case xml:get_subtag(Packet, "status") of - false -> - ""; - StatusTag -> - xml:get_tag_cdata(StatusTag) + #xmlel{attrs = Attrs} = Packet, + case xml:get_attr_s(<<"type">>, Attrs) of + <<"unavailable">> -> + Status = case xml:get_subtag(Packet, <<"status">>) of + false -> <<"">>; + StatusTag -> xml:get_tag_cdata(StatusTag) + end, + Info = [{ip, StateData#state.ip}, + {conn, StateData#state.conn}, + {auth_module, StateData#state.auth_module}], + ejabberd_sm:unset_presence(StateData#state.sid, + StateData#state.user, + StateData#state.server, + StateData#state.resource, Status, Info), + presence_broadcast(StateData, From, + StateData#state.pres_a, Packet), + StateData#state{pres_last = undefined, + pres_timestamp = undefined, pres_a = (?SETS):new()}; + <<"error">> -> StateData; + <<"probe">> -> StateData; + <<"subscribe">> -> StateData; + <<"subscribed">> -> StateData; + <<"unsubscribe">> -> StateData; + <<"unsubscribed">> -> StateData; + _ -> + OldPriority = case StateData#state.pres_last of + undefined -> 0; + OldPresence -> get_priority_from_presence(OldPresence) + end, + NewPriority = get_priority_from_presence(Packet), + Timestamp = calendar:now_to_universal_time(now()), + update_priority(NewPriority, Packet, StateData), + FromUnavail = (StateData#state.pres_last == undefined), + ?DEBUG("from unavail = ~p~n", [FromUnavail]), + NewStateData = StateData#state{pres_last = Packet, + pres_timestamp = Timestamp}, + NewState = if FromUnavail -> + ejabberd_hooks:run(user_available_hook, + NewStateData#state.server, + [NewStateData#state.jid]), + ResentStateData = if NewPriority >= 0 -> + resend_offline_messages(NewStateData), + resend_subscription_requests(NewStateData); + true -> NewStateData + end, + presence_broadcast_first(From, ResentStateData, + Packet); + true -> + presence_broadcast_to_trusted(NewStateData, From, + NewStateData#state.pres_f, + NewStateData#state.pres_a, + Packet), + if OldPriority < 0, NewPriority >= 0 -> + resend_offline_messages(NewStateData); + true -> ok + end, + NewStateData end, - ejabberd_sm:unset_presence(StateData#state.sid, - StateData#state.user, - StateData#state.server, - StateData#state.resource, - Status, - StateData#state.ip), - presence_broadcast(StateData, From, StateData#state.pres_a, Packet), - presence_broadcast(StateData, From, StateData#state.pres_i, Packet), - StateData#state{pres_last = undefined, - pres_a = ?SETS:new(), - pres_i = ?SETS:new(), - pres_invis = false}; - "invisible" -> - NewState = - if - not StateData#state.pres_invis -> - presence_broadcast(StateData, From, - StateData#state.pres_a, - Packet), - presence_broadcast(StateData, From, - StateData#state.pres_i, - Packet), - S1 = StateData#state{pres_last = undefined, - pres_a = ?SETS:new(), - pres_i = ?SETS:new(), - pres_invis = true}, - presence_broadcast_first(From, S1, Packet); - true -> - StateData - end, - NewState; - "error" -> - StateData; - "probe" -> - StateData; - "subscribe" -> - StateData; - "subscribed" -> - StateData; - "unsubscribe" -> - StateData; - "unsubscribed" -> - StateData; - _ -> - OldPriority = case StateData#state.pres_last of - undefined -> - 0; - OldPresence -> - get_priority_from_presence(OldPresence) - end, - NewPriority = get_priority_from_presence(Packet), - update_priority(NewPriority, Packet, StateData), - FromUnavail = (StateData#state.pres_last == undefined) or - StateData#state.pres_invis, - ?DEBUG("from unavail = ~p~n", [FromUnavail]), - NewState = - if - FromUnavail -> - ejabberd_hooks:run(user_available_hook, - StateData#state.server, - [StateData#state.jid]), - if NewPriority >= 0 -> - resend_offline_messages(StateData), - resend_subscription_requests(StateData); - true -> - ok - end, - presence_broadcast_first( - From, StateData#state{pres_last = Packet, - pres_invis = false - }, Packet); - true -> - presence_broadcast_to_trusted(StateData, - From, - StateData#state.pres_f, - StateData#state.pres_a, - Packet), - if OldPriority < 0, NewPriority >= 0 -> - resend_offline_messages(StateData); - true -> - ok - end, - StateData#state{pres_last = Packet, - pres_invis = false - } - end, - NewState + NewState end. +%% User sends a directed presence packet presence_track(From, To, Packet, StateData) -> - {xmlelement, _Name, Attrs, _Els} = Packet, + #xmlel{attrs = Attrs} = Packet, LTo = jlib:jid_tolower(To), User = StateData#state.user, Server = StateData#state.server, - case xml:get_attr_s("type", Attrs) of - "unavailable" -> - ejabberd_router:route(From, To, Packet), - I = remove_element(LTo, StateData#state.pres_i), - A = remove_element(LTo, StateData#state.pres_a), - StateData#state{pres_i = I, - pres_a = A}; - "invisible" -> - ejabberd_router:route(From, To, Packet), - I = ?SETS:add_element(LTo, StateData#state.pres_i), - A = remove_element(LTo, StateData#state.pres_a), - StateData#state{pres_i = I, - pres_a = A}; - "subscribe" -> + case xml:get_attr_s(<<"type">>, Attrs) of + <<"unavailable">> -> + check_privacy_route(From, StateData, From, To, Packet), + A = remove_element(LTo, StateData#state.pres_a), + StateData#state{pres_a = A}; + <<"subscribe">> -> + try_roster_subscribe(subscribe, User, Server, From, To, Packet, StateData), + StateData; + <<"subscribed">> -> + ejabberd_hooks:run(roster_out_subscription, Server, + [User, Server, To, subscribed]), + check_privacy_route(From, StateData, + jlib:jid_remove_resource(From), To, Packet), + StateData; + <<"unsubscribe">> -> + try_roster_subscribe(unsubscribe, User, Server, From, To, Packet, StateData), + StateData; + <<"unsubscribed">> -> + ejabberd_hooks:run(roster_out_subscription, Server, + [User, Server, To, unsubscribed]), + check_privacy_route(From, StateData, + jlib:jid_remove_resource(From), To, Packet), + StateData; + <<"error">> -> + check_privacy_route(From, StateData, From, To, Packet), + StateData; + <<"probe">> -> + check_privacy_route(From, StateData, From, To, Packet), + StateData; + _ -> + check_privacy_route(From, StateData, From, To, Packet), + A = (?SETS):add_element(LTo, StateData#state.pres_a), + StateData#state{pres_a = A} + end. + +check_privacy_route(From, StateData, FromRoute, To, + Packet) -> + case privacy_check_packet(StateData, From, To, Packet, + out) + of + deny -> + Lang = StateData#state.lang, + ErrText = <<"Your active privacy list has denied " + "the routing of this stanza.">>, + Err = jlib:make_error_reply(Packet, + ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)), + ejabberd_router:route(To, From, Err), + ok; + allow -> ejabberd_router:route(FromRoute, To, Packet) + end. + +%% Check if privacy rules allow this delivery +privacy_check_packet(StateData, From, To, Packet, + Dir) -> + ejabberd_hooks:run_fold(privacy_check_packet, + StateData#state.server, allow, + [StateData#state.user, StateData#state.server, + StateData#state.privacy_list, {From, To, Packet}, + Dir]). + +is_privacy_allow(StateData, From, To, Packet, Dir) -> + allow == + privacy_check_packet(StateData, From, To, Packet, Dir). + +%%% Check ACL before allowing to send a subscription stanza +try_roster_subscribe(Type, User, Server, From, To, Packet, StateData) -> + JID1 = jlib:make_jid(User, Server, <<"">>), + Access = gen_mod:get_module_opt(Server, mod_roster, access, fun(A) when is_atom(A) -> A end, all), + case acl:match_rule(Server, Access, JID1) of + deny -> + %% Silently drop this (un)subscription request + ok; + allow -> ejabberd_hooks:run(roster_out_subscription, Server, - [User, Server, To, subscribe]), - ejabberd_router:route(jlib:jid_remove_resource(From), To, Packet), - StateData; - "subscribed" -> - ejabberd_hooks:run(roster_out_subscription, - Server, - [User, Server, To, subscribed]), - ejabberd_router:route(jlib:jid_remove_resource(From), To, Packet), - StateData; - "unsubscribe" -> - ejabberd_hooks:run(roster_out_subscription, - Server, - [User, Server, To, unsubscribe]), - ejabberd_router:route(jlib:jid_remove_resource(From), To, Packet), - StateData; - "unsubscribed" -> - ejabberd_hooks:run(roster_out_subscription, - Server, - [User, Server, To, unsubscribed]), - ejabberd_router:route(jlib:jid_remove_resource(From), To, Packet), - StateData; - "error" -> - ejabberd_router:route(From, To, Packet), - StateData; - "probe" -> - ejabberd_router:route(From, To, Packet), - StateData; - _ -> - case ejabberd_hooks:run_fold( - privacy_check_packet, StateData#state.server, - allow, - [StateData#state.user, - StateData#state.server, - StateData#state.privacy_list, - {From, To, Packet}, - out]) of - deny -> - ok; - allow -> - ejabberd_router:route(From, To, Packet) - end, - I = remove_element(LTo, StateData#state.pres_i), - A = ?SETS:add_element(LTo, StateData#state.pres_a), - StateData#state{pres_i = I, - pres_a = A} + [User, Server, To, Type]), + check_privacy_route(From, StateData, jlib:jid_remove_resource(From), + To, Packet) end. %% Send presence when disconnecting presence_broadcast(StateData, From, JIDSet, Packet) -> JIDs = ?SETS:to_list(JIDSet), - JIDs2 = format_and_check_privacy(From, StateData, Packet, JIDs), - Server = StateData#state.server, - send_multiple(From, Server, JIDs2, Packet). + JIDs2 = format_and_check_privacy(From, StateData, Packet, JIDs, out), + send_multiple(StateData, From, JIDs2, Packet). %% Send presence when updating presence presence_broadcast_to_trusted(StateData, From, Trusted, JIDSet, Packet) -> JIDs = ?SETS:to_list(JIDSet), JIDs_trusted = [JID || JID <- JIDs, ?SETS:is_element(JID, Trusted)], - JIDs2 = format_and_check_privacy(From, StateData, Packet, JIDs_trusted), - Server = StateData#state.server, - send_multiple(From, Server, JIDs2, Packet). - + JIDs2 = format_and_check_privacy(From, StateData, Packet, JIDs_trusted, out), + send_multiple(StateData, From, JIDs2, Packet). %% Send presence when connecting presence_broadcast_first(From, StateData, Packet) -> - JIDsProbe = + JIDsProbe = ?SETS:fold( fun(JID, L) -> [JID | L] end, [], StateData#state.pres_t), - PacketProbe = {xmlelement, "presence", [{"type", "probe"}], []}, - JIDs2Probe = format_and_check_privacy(From, StateData, Packet, JIDsProbe), + PacketProbe = #xmlel{name = <<"presence">>, attrs = [{<<"type">>,<<"probe">>}], children = []}, + JIDs2Probe = format_and_check_privacy(From, StateData, PacketProbe, JIDsProbe, out), Server = StateData#state.server, - send_multiple(From, Server, JIDs2Probe, PacketProbe), - if - StateData#state.pres_invis -> - StateData; - true -> - {As, JIDs} = - ?SETS:fold( - fun(JID, {A, JID_list}) -> - {?SETS:add_element(JID, A), JID_list++[JID]} - end, - {StateData#state.pres_a, []}, - StateData#state.pres_f), - JIDs2 = format_and_check_privacy(From, StateData, Packet, JIDs), - Server = StateData#state.server, - send_multiple(From, Server, JIDs2, Packet), - StateData#state{pres_a = As} - end. + send_multiple(StateData, From, JIDs2Probe, PacketProbe), + {As, JIDs} = + ?SETS:fold( + fun(JID, {A, JID_list}) -> + {?SETS:add_element(JID, A), JID_list++[JID]} + end, + {StateData#state.pres_a, []}, + StateData#state.pres_f), + JIDs2 = format_and_check_privacy(From, StateData, Packet, JIDs, out), + Server = StateData#state.server, + send_multiple(StateData, From, JIDs2, Packet), + StateData#state{pres_a = As}. -format_and_check_privacy(From, StateData, Packet, JIDs) -> +format_and_check_privacy(From, StateData, Packet, JIDs, Dir) -> FJIDs = [jlib:make_jid(JID) || JID <- JIDs], lists:filter( fun(FJID) -> @@ -1591,274 +2230,871 @@ format_and_check_privacy(From, StateData, Packet, JIDs) -> StateData#state.server, StateData#state.privacy_list, {From, FJID, Packet}, - out]) of + Dir]) of deny -> false; allow -> true end end, FJIDs). - -send_multiple(From, Server, JIDs, Packet) -> - ejabberd_router_multicast:route_multicast(From, Server, JIDs, Packet). + +send_multiple(StateData, From, JIDs, Packet) -> + lists:foreach( + fun(JID) -> + case privacy_check_packet(StateData, From, JID, Packet, out) of + deny -> + ok; + allow -> + ejabberd_router:route(From, JID, Packet) + end + end, JIDs). remove_element(E, Set) -> - case ?SETS:is_element(E, Set) of - true -> - ?SETS:del_element(E, Set); - _ -> - Set + case (?SETS):is_element(E, Set) of + true -> (?SETS):del_element(E, Set); + _ -> Set end. - roster_change(IJID, ISubscription, StateData) -> LIJID = jlib:jid_tolower(IJID), IsFrom = (ISubscription == both) or (ISubscription == from), - IsTo = (ISubscription == both) or (ISubscription == to), - OldIsFrom = ?SETS:is_element(LIJID, StateData#state.pres_f), + IsTo = (ISubscription == both) or (ISubscription == to), + OldIsFrom = (?SETS):is_element(LIJID, StateData#state.pres_f), FSet = if - IsFrom -> - ?SETS:add_element(LIJID, StateData#state.pres_f); - true -> - remove_element(LIJID, StateData#state.pres_f) + IsFrom -> (?SETS):add_element(LIJID, StateData#state.pres_f); + not IsFrom -> remove_element(LIJID, StateData#state.pres_f) end, TSet = if - IsTo -> - ?SETS:add_element(LIJID, StateData#state.pres_t); - true -> - remove_element(LIJID, StateData#state.pres_t) + IsTo -> (?SETS):add_element(LIJID, StateData#state.pres_t); + not IsTo -> remove_element(LIJID, StateData#state.pres_t) end, case StateData#state.pres_last of - undefined -> - StateData#state{pres_f = FSet, pres_t = TSet}; - P -> - ?DEBUG("roster changed for ~p~n", [StateData#state.user]), - From = StateData#state.jid, - To = jlib:make_jid(IJID), - Cond1 = (not StateData#state.pres_invis) and IsFrom - and (not OldIsFrom), - Cond2 = (not IsFrom) and OldIsFrom - and (?SETS:is_element(LIJID, StateData#state.pres_a) or - ?SETS:is_element(LIJID, StateData#state.pres_i)), - if - Cond1 -> - ?DEBUG("C1: ~p~n", [LIJID]), - case ejabberd_hooks:run_fold( - privacy_check_packet, StateData#state.server, - allow, - [StateData#state.user, - StateData#state.server, - StateData#state.privacy_list, - {From, To, P}, - out]) of - deny -> - ok; - allow -> - ejabberd_router:route(From, To, P) - end, - A = ?SETS:add_element(LIJID, - StateData#state.pres_a), - StateData#state{pres_a = A, - pres_f = FSet, - pres_t = TSet}; - Cond2 -> - ?DEBUG("C2: ~p~n", [LIJID]), - PU = {xmlelement, "presence", - [{"type", "unavailable"}], []}, - case ejabberd_hooks:run_fold( - privacy_check_packet, StateData#state.server, - allow, - [StateData#state.user, - StateData#state.server, - StateData#state.privacy_list, - {From, To, PU}, - out]) of - deny -> - ok; - allow -> - ejabberd_router:route(From, To, PU) - end, - I = remove_element(LIJID, - StateData#state.pres_i), - A = remove_element(LIJID, - StateData#state.pres_a), - StateData#state{pres_i = I, - pres_a = A, - pres_f = FSet, - pres_t = TSet}; - true -> - StateData#state{pres_f = FSet, pres_t = TSet} - end + undefined -> + StateData#state{pres_f = FSet, pres_t = TSet}; + P -> + ?DEBUG("roster changed for ~p~n", + [StateData#state.user]), + From = StateData#state.jid, + To = jlib:make_jid(IJID), + Cond1 = IsFrom andalso not OldIsFrom, + Cond2 = not IsFrom andalso OldIsFrom andalso + ((?SETS):is_element(LIJID, StateData#state.pres_a)), + if Cond1 -> + ?DEBUG("C1: ~p~n", [LIJID]), + case privacy_check_packet(StateData, From, To, P, out) + of + deny -> ok; + allow -> ejabberd_router:route(From, To, P) + end, + A = (?SETS):add_element(LIJID, StateData#state.pres_a), + StateData#state{pres_a = A, pres_f = FSet, + pres_t = TSet}; + Cond2 -> + ?DEBUG("C2: ~p~n", [LIJID]), + PU = #xmlel{name = <<"presence">>, + attrs = [{<<"type">>, <<"unavailable">>}], + children = []}, + case privacy_check_packet(StateData, From, To, PU, out) + of + deny -> ok; + allow -> ejabberd_router:route(From, To, PU) + end, + A = remove_element(LIJID, StateData#state.pres_a), + StateData#state{pres_a = A, pres_f = FSet, + pres_t = TSet}; + true -> StateData#state{pres_f = FSet, pres_t = TSet} + end end. - update_priority(Priority, Packet, StateData) -> + Info = [{ip, StateData#state.ip}, {conn, StateData#state.conn}, + {auth_module, StateData#state.auth_module}], ejabberd_sm:set_presence(StateData#state.sid, - StateData#state.user, - StateData#state.server, - StateData#state.resource, - Priority, - Packet, - StateData#state.ip). + StateData#state.user, StateData#state.server, + StateData#state.resource, Priority, Packet, Info). get_priority_from_presence(PresencePacket) -> - case xml:get_subtag(PresencePacket, "priority") of - false -> - 0; - SubEl -> - case catch list_to_integer(xml:get_tag_cdata(SubEl)) of - P when is_integer(P) -> - P; - _ -> - 0 - end + case xml:get_subtag(PresencePacket, <<"priority">>) of + false -> 0; + SubEl -> + case catch + jlib:binary_to_integer(xml:get_tag_cdata(SubEl)) + of + P when is_integer(P) -> P; + _ -> 0 + end end. process_privacy_iq(From, To, - #iq{type = Type, sub_el = SubEl} = IQ, - StateData) -> - {Res, NewStateData} = - case Type of - get -> - R = ejabberd_hooks:run_fold( - privacy_iq_get, StateData#state.server, - {error, ?ERR_FEATURE_NOT_IMPLEMENTED}, - [From, To, IQ, StateData#state.privacy_list]), - {R, StateData}; - set -> - case ejabberd_hooks:run_fold( - privacy_iq_set, StateData#state.server, - {error, ?ERR_FEATURE_NOT_IMPLEMENTED}, - [From, To, IQ]) of - {result, R, NewPrivList} -> - {{result, R}, - StateData#state{privacy_list = NewPrivList}}; - R -> {R, StateData} - end - end, - IQRes = - case Res of - {result, Result} -> - IQ#iq{type = result, sub_el = Result}; - {error, Error} -> - IQ#iq{type = error, sub_el = [SubEl, Error]} - end, - ejabberd_router:route( - To, From, jlib:iq_to_xml(IQRes)), + #iq{type = Type, sub_el = SubEl} = IQ, StateData) -> + {Res, NewStateData} = case Type of + get -> + R = ejabberd_hooks:run_fold(privacy_iq_get, + StateData#state.server, + {error, + ?ERR_FEATURE_NOT_IMPLEMENTED}, + [From, To, IQ, + StateData#state.privacy_list]), + {R, StateData}; + set -> + case ejabberd_hooks:run_fold(privacy_iq_set, + StateData#state.server, + {error, + ?ERR_FEATURE_NOT_IMPLEMENTED}, + [From, To, IQ]) + of + {result, R, NewPrivList} -> + {{result, R}, + StateData#state{privacy_list = + NewPrivList}}; + R -> {R, StateData} + end + end, + IQRes = case Res of + {result, Result} -> + IQ#iq{type = result, sub_el = Result}; + {error, Error} -> + IQ#iq{type = error, sub_el = [SubEl, Error]} + end, + ejabberd_router:route(To, From, jlib:iq_to_xml(IQRes)), NewStateData. - -resend_offline_messages(#state{user = User, - server = Server, - privacy_list = PrivList} = StateData) -> +resend_offline_messages(StateData) -> case ejabberd_hooks:run_fold(resend_offline_messages_hook, - Server, - [], - [User, Server]) of - Rs when is_list(Rs) -> - lists:foreach( - fun({route, - From, To, {xmlelement, Name, Attrs, Els} = Packet}) -> - Pass = case ejabberd_hooks:run_fold( - privacy_check_packet, Server, - allow, - [User, - Server, - PrivList, - {From, To, Packet}, - in]) of - allow -> - true; - deny -> - false - end, - if - Pass -> - Attrs2 = jlib:replace_from_to_attrs( - jlib:jid_to_string(From), - jlib:jid_to_string(To), - Attrs), - send_element(StateData, - {xmlelement, Name, Attrs2, Els}); - true -> - ok - end - end, Rs) + StateData#state.server, [], + [StateData#state.user, StateData#state.server]) + of + Rs -> %%when is_list(Rs) -> + lists:foreach(fun ({route, From, To, + #xmlel{} = Packet}) -> + Pass = case privacy_check_packet(StateData, + From, To, + Packet, in) + of + allow -> true; + deny -> false + end, + if Pass -> + ejabberd_router:route(From, To, Packet); + true -> ok + end + end, + Rs) end. resend_subscription_requests(#state{user = User, server = Server} = StateData) -> - PendingSubscriptions = ejabberd_hooks:run_fold( - resend_subscription_requests_hook, - Server, - [], - [User, Server]), - lists:foreach(fun(XMLPacket) -> - send_element(StateData, - XMLPacket) - end, - PendingSubscriptions). + PendingSubscriptions = + ejabberd_hooks:run_fold(resend_subscription_requests_hook, + Server, [], [User, Server]), + lists:foldl(fun (XMLPacket, AccStateData) -> + send_packet(AccStateData, XMLPacket) + end, + StateData, + PendingSubscriptions). -get_showtag(undefined) -> - "unavailable"; +get_showtag(undefined) -> <<"unavailable">>; get_showtag(Presence) -> - case xml:get_path_s(Presence, [{elem, "show"}, cdata]) of - "" -> "available"; + case xml:get_path_s(Presence, [{elem, <<"show">>}, cdata]) of + <<"">> -> <<"available">>; ShowTag -> ShowTag end. -get_statustag(undefined) -> - ""; +get_statustag(undefined) -> <<"">>; get_statustag(Presence) -> - case xml:get_path_s(Presence, [{elem, "status"}, cdata]) of - ShowTag -> ShowTag - end. + xml:get_path_s(Presence, [{elem, <<"status">>}, cdata]). process_unauthenticated_stanza(StateData, El) -> - case jlib:iq_query_info(El) of - #iq{} = IQ -> - Res = ejabberd_hooks:run_fold(c2s_unauthenticated_iq, - StateData#state.server, - empty, - [StateData#state.server, IQ]), - case Res of - empty -> - % The only reasonable IQ's here are auth and register IQ's - % They contain secrets, so don't include subelements to response - ResIQ = IQ#iq{type = error, - sub_el = [?ERR_SERVICE_UNAVAILABLE]}, - Res1 = jlib:replace_from_to( - jlib:make_jid("", StateData#state.server, ""), - jlib:make_jid("", "", ""), - jlib:iq_to_xml(ResIQ)), - send_element(StateData, jlib:remove_attr("to", Res1)); - _ -> - send_element(StateData, Res) - end; - _ -> - % Drop any stanza, which isn't IQ stanza - ok + NewEl = case xml:get_tag_attr_s(<<"xml:lang">>, El) of + <<"">> -> + case StateData#state.lang of + <<"">> -> El; + Lang -> xml:replace_tag_attr(<<"xml:lang">>, Lang, El) + end; + _ -> El + end, + case jlib:iq_query_info(NewEl) of + #iq{} = IQ -> + Res = ejabberd_hooks:run_fold(c2s_unauthenticated_iq, + StateData#state.server, empty, + [StateData#state.server, IQ, + StateData#state.ip]), + case Res of + empty -> + ResIQ = IQ#iq{type = error, + sub_el = [?ERR_SERVICE_UNAVAILABLE]}, + Res1 = jlib:replace_from_to(jlib:make_jid(<<"">>, + StateData#state.server, + <<"">>), + jlib:make_jid(<<"">>, <<"">>, + <<"">>), + jlib:iq_to_xml(ResIQ)), + send_element(StateData, + jlib:remove_attr(<<"to">>, Res1)); + _ -> send_element(StateData, Res) + end; + _ -> + % Drop any stanza, which isn't IQ stanza + ok end. peerip(SockMod, Socket) -> IP = case SockMod of - gen_tcp -> inet:peername(Socket); - _ -> SockMod:peername(Socket) + gen_tcp -> inet:peername(Socket); + _ -> SockMod:peername(Socket) end, case IP of - {ok, IPOK} -> IPOK; - _ -> undefined + {ok, IPOK} -> IPOK; + _ -> undefined end. +%% fsm_next_state_pack: Pack the StateData structure to improve +%% sharing. +fsm_next_state_pack(StateName, StateData) -> + fsm_next_state_gc(StateName, pack(StateData)). + +%% fsm_next_state_gc: Garbage collect the process heap to make use of +%% the newly packed StateData structure. +fsm_next_state_gc(StateName, PackedStateData) -> + erlang:garbage_collect(), + fsm_next_state(StateName, PackedStateData). + %% fsm_next_state: Generate the next_state FSM tuple with different %% timeout, depending on the future state +fsm_next_state(session_established, #state{mgmt_max_queue = exceeded} = + StateData) -> + ?WARNING_MSG("ACK queue too long, terminating session for ~s", + [jlib:jid_to_string(StateData#state.jid)]), + Err = ?SERRT_POLICY_VIOLATION(StateData#state.lang, + <<"Too many unacked stanzas">>), + send_element(StateData, Err), + send_trailer(StateData), + {stop, normal, StateData#state{mgmt_resend = false}}; +fsm_next_state(session_established, #state{mgmt_state = pending} = StateData) -> + fsm_next_state(wait_for_resume, StateData); fsm_next_state(session_established, StateData) -> - {next_state, session_established, StateData, ?C2S_HIBERNATE_TIMEOUT}; + {next_state, session_established, StateData, + ?C2S_HIBERNATE_TIMEOUT}; +fsm_next_state(wait_for_resume, #state{mgmt_timeout = 0} = StateData) -> + {stop, normal, StateData}; +fsm_next_state(wait_for_resume, #state{mgmt_pending_since = undefined} = + StateData) -> + ?INFO_MSG("Waiting for resumption of stream for ~s", + [jlib:jid_to_string(StateData#state.jid)]), + {next_state, wait_for_resume, + StateData#state{mgmt_state = pending, mgmt_pending_since = os:timestamp()}, + StateData#state.mgmt_timeout}; +fsm_next_state(wait_for_resume, StateData) -> + Diff = timer:now_diff(os:timestamp(), StateData#state.mgmt_pending_since), + Timeout = max(StateData#state.mgmt_timeout - Diff div 1000, 1), + {next_state, wait_for_resume, StateData, Timeout}; fsm_next_state(StateName, StateData) -> {next_state, StateName, StateData, ?C2S_OPEN_TIMEOUT}. %% fsm_reply: Generate the reply FSM tuple with different timeout, %% depending on the future state fsm_reply(Reply, session_established, StateData) -> - {reply, Reply, session_established, StateData, ?C2S_HIBERNATE_TIMEOUT}; + {reply, Reply, session_established, StateData, + ?C2S_HIBERNATE_TIMEOUT}; +fsm_reply(Reply, wait_for_resume, StateData) -> + Diff = timer:now_diff(os:timestamp(), StateData#state.mgmt_pending_since), + Timeout = max(StateData#state.mgmt_timeout - Diff div 1000, 1), + {reply, Reply, wait_for_resume, StateData, Timeout}; fsm_reply(Reply, StateName, StateData) -> {reply, Reply, StateName, StateData, ?C2S_OPEN_TIMEOUT}. + +%% Used by c2s blacklist plugins +is_ip_blacklisted(undefined, _Lang) -> false; +is_ip_blacklisted({IP, _Port}, Lang) -> + ejabberd_hooks:run_fold(check_bl_c2s, false, [IP, Lang]). + +%% Check from attributes +%% returns invalid-from|NewElement +check_from(El, FromJID) -> + case xml:get_tag_attr(<<"from">>, El) of + false -> + El; + {value, SJID} -> + JID = jlib:string_to_jid(SJID), + case JID of + error -> + 'invalid-from'; + #jid{} -> + if + (JID#jid.luser == FromJID#jid.luser) and + (JID#jid.lserver == FromJID#jid.lserver) and + (JID#jid.lresource == FromJID#jid.lresource) -> + El; + (JID#jid.luser == FromJID#jid.luser) and + (JID#jid.lserver == FromJID#jid.lserver) and + (JID#jid.lresource == <<"">>) -> + El; + true -> + 'invalid-from' + end + end + end. + +fsm_limit_opts(Opts) -> + case lists:keysearch(max_fsm_queue, 1, Opts) of + {value, {_, N}} when is_integer(N) -> [{max_queue, N}]; + _ -> + case ejabberd_config:get_option( + max_fsm_queue, + fun(I) when is_integer(I), I > 0 -> I end) of + undefined -> []; + N -> [{max_queue, N}] + end + end. + +bounce_messages() -> + receive + {route, From, To, El} -> + ejabberd_router:route(From, To, El), bounce_messages() + after 0 -> ok + end. + +%%%---------------------------------------------------------------------- +%%% XEP-0191 +%%%---------------------------------------------------------------------- + +route_blocking(What, StateData) -> + SubEl = case What of + {block, JIDs} -> + #xmlel{name = <<"block">>, + attrs = [{<<"xmlns">>, ?NS_BLOCKING}], + children = + lists:map(fun (JID) -> + #xmlel{name = <<"item">>, + attrs = + [{<<"jid">>, + jlib:jid_to_string(JID)}], + children = []} + end, + JIDs)}; + {unblock, JIDs} -> + #xmlel{name = <<"unblock">>, + attrs = [{<<"xmlns">>, ?NS_BLOCKING}], + children = + lists:map(fun (JID) -> + #xmlel{name = <<"item">>, + attrs = + [{<<"jid">>, + jlib:jid_to_string(JID)}], + children = []} + end, + JIDs)}; + unblock_all -> + #xmlel{name = <<"unblock">>, + attrs = [{<<"xmlns">>, ?NS_BLOCKING}], children = []} + end, + PrivPushIQ = #iq{type = set, id = <<"push">>, sub_el = [SubEl]}, + PrivPushEl = + jlib:replace_from_to(jlib:jid_remove_resource(StateData#state.jid), + StateData#state.jid, jlib:iq_to_xml(PrivPushIQ)), + %% No need to replace active privacy list here, + %% blocking pushes are always accompanied by + %% Privacy List pushes + send_stanza(StateData, PrivPushEl). + +%%%---------------------------------------------------------------------- +%%% XEP-0198 +%%%---------------------------------------------------------------------- + +stream_mgmt_enabled(#state{mgmt_state = disabled}) -> + false; +stream_mgmt_enabled(_StateData) -> + true. + +dispatch_stream_mgmt(El, StateData) + when StateData#state.mgmt_state == active; + StateData#state.mgmt_state == pending -> + perform_stream_mgmt(El, StateData); +dispatch_stream_mgmt(El, StateData) -> + negotiate_stream_mgmt(El, StateData). + +negotiate_stream_mgmt(_El, #state{resource = <<"">>} = StateData) -> + %% XEP-0198 says: "For client-to-server connections, the client MUST NOT + %% attempt to enable stream management until after it has completed Resource + %% Binding unless it is resuming a previous session". However, it also + %% says: "Stream management errors SHOULD be considered recoverable", so we + %% won't bail out. + send_element(StateData, ?MGMT_UNEXPECTED_REQUEST(?NS_STREAM_MGMT_3)), + StateData; +negotiate_stream_mgmt(#xmlel{name = Name, attrs = Attrs}, StateData) -> + case xml:get_attr_s(<<"xmlns">>, Attrs) of + Xmlns when ?IS_SUPPORTED_MGMT_XMLNS(Xmlns) -> + case stream_mgmt_enabled(StateData) of + true -> + case Name of + <<"enable">> -> + handle_enable(StateData#state{mgmt_xmlns = Xmlns}, Attrs); + _ -> + Res = if Name == <<"a">>; + Name == <<"r">>; + Name == <<"resume">> -> + ?MGMT_UNEXPECTED_REQUEST(Xmlns); + true -> + ?MGMT_BAD_REQUEST(Xmlns) + end, + send_element(StateData, Res), + StateData + end; + false -> + send_element(StateData, ?MGMT_SERVICE_UNAVAILABLE(Xmlns)), + StateData + end; + _ -> + send_element(StateData, ?MGMT_UNSUPPORTED_VERSION(?NS_STREAM_MGMT_3)), + StateData + end. + +perform_stream_mgmt(#xmlel{name = Name, attrs = Attrs}, StateData) -> + case xml:get_attr_s(<<"xmlns">>, Attrs) of + Xmlns when Xmlns == StateData#state.mgmt_xmlns -> + case Name of + <<"r">> -> + handle_r(StateData); + <<"a">> -> + handle_a(StateData, Attrs); + _ -> + Res = if Name == <<"enable">>; + Name == <<"resume">> -> + ?MGMT_UNEXPECTED_REQUEST(Xmlns); + true -> + ?MGMT_BAD_REQUEST(Xmlns) + end, + send_element(StateData, Res), + StateData + end; + _ -> + send_element(StateData, + ?MGMT_UNSUPPORTED_VERSION(StateData#state.mgmt_xmlns)), + StateData + end. + +handle_enable(#state{mgmt_timeout = ConfigTimeout} = StateData, Attrs) -> + Timeout = case xml:get_attr_s(<<"resume">>, Attrs) of + ResumeAttr when ResumeAttr == <<"true">>; + ResumeAttr == <<"1">> -> + MaxAttr = xml:get_attr_s(<<"max">>, Attrs), + case catch jlib:binary_to_integer(MaxAttr) of + Max when is_integer(Max), Max > 0, Max =< ConfigTimeout -> + Max; + _ -> + ConfigTimeout + end; + _ -> + 0 + end, + ResAttrs = [{<<"xmlns">>, StateData#state.mgmt_xmlns}] ++ + if Timeout > 0 -> + ?INFO_MSG("Stream management with resumption enabled for ~s", + [jlib:jid_to_string(StateData#state.jid)]), + [{<<"id">>, make_resume_id(StateData)}, + {<<"resume">>, <<"true">>}, + {<<"max">>, jlib:integer_to_binary(Timeout)}]; + true -> + ?INFO_MSG("Stream management without resumption enabled for ~s", + [jlib:jid_to_string(StateData#state.jid)]), + [] + end, + Res = #xmlel{name = <<"enabled">>, + attrs = ResAttrs, + children = []}, + send_element(StateData, Res), + StateData#state{mgmt_state = active, + mgmt_queue = queue:new(), + mgmt_timeout = Timeout * 1000}. + +handle_r(StateData) -> + H = jlib:integer_to_binary(StateData#state.mgmt_stanzas_in), + Res = #xmlel{name = <<"a">>, + attrs = [{<<"xmlns">>, StateData#state.mgmt_xmlns}, + {<<"h">>, H}], + children = []}, + send_element(StateData, Res), + StateData. + +handle_a(StateData, Attrs) -> + case catch jlib:binary_to_integer(xml:get_attr_s(<<"h">>, Attrs)) of + H when is_integer(H), H >= 0 -> + check_h_attribute(StateData, H); + _ -> + ?DEBUG("Ignoring invalid ACK element from ~s", + [jlib:jid_to_string(StateData#state.jid)]), + StateData + end. + +handle_resume(StateData, Attrs) -> + R = case xml:get_attr_s(<<"xmlns">>, Attrs) of + Xmlns when ?IS_SUPPORTED_MGMT_XMLNS(Xmlns) -> + case stream_mgmt_enabled(StateData) of + true -> + case {xml:get_attr(<<"previd">>, Attrs), + catch jlib:binary_to_integer(xml:get_attr_s(<<"h">>, Attrs))} + of + {{value, PrevID}, H} when is_integer(H), H >= 0 -> + case inherit_session_state(StateData, PrevID) of + {ok, InheritedState} -> + {ok, InheritedState, H}; + {error, Err} -> + {error, ?MGMT_ITEM_NOT_FOUND(Xmlns), Err} + end; + _ -> + {error, ?MGMT_BAD_REQUEST(Xmlns), + <<"Invalid request">>} + end; + false -> + {error, ?MGMT_SERVICE_UNAVAILABLE(Xmlns), + <<"XEP-0198 disabled">>} + end; + _ -> + {error, ?MGMT_UNSUPPORTED_VERSION(?NS_STREAM_MGMT_3), + <<"Invalid XMLNS">>} + end, + case R of + {ok, ResumedState, NumHandled} -> + NewState = check_h_attribute(ResumedState, NumHandled), + AttrXmlns = NewState#state.mgmt_xmlns, + AttrId = make_resume_id(NewState), + AttrH = jlib:integer_to_binary(NewState#state.mgmt_stanzas_in), + send_element(NewState, + #xmlel{name = <<"resumed">>, + attrs = [{<<"xmlns">>, AttrXmlns}, + {<<"h">>, AttrH}, + {<<"previd">>, AttrId}], + children = []}), + SendFun = fun(_F, _T, El) -> send_element(NewState, El) end, + handle_unacked_stanzas(NewState, SendFun), + send_element(NewState, + #xmlel{name = <<"r">>, + attrs = [{<<"xmlns">>, AttrXmlns}], + children = []}), + FlushedState = csi_queue_flush(NewState), + NewStateData = FlushedState#state{csi_state = active}, + ?INFO_MSG("Resumed session for ~s", + [jlib:jid_to_string(NewStateData#state.jid)]), + {ok, NewStateData}; + {error, El, Msg} -> + send_element(StateData, El), + ?INFO_MSG("Cannot resume session for ~s@~s: ~s", + [StateData#state.user, StateData#state.server, Msg]), + error + end. + +check_h_attribute(#state{mgmt_stanzas_out = NumStanzasOut} = StateData, H) + when H > NumStanzasOut -> + ?DEBUG("~s acknowledged ~B stanzas, but only ~B were sent", + [jlib:jid_to_string(StateData#state.jid), H, NumStanzasOut]), + mgmt_queue_drop(StateData#state{mgmt_stanzas_out = H}, NumStanzasOut); +check_h_attribute(#state{mgmt_stanzas_out = NumStanzasOut} = StateData, H) -> + ?DEBUG("~s acknowledged ~B of ~B stanzas", + [jlib:jid_to_string(StateData#state.jid), H, NumStanzasOut]), + mgmt_queue_drop(StateData, H). + +update_num_stanzas_in(#state{mgmt_state = active} = StateData, El) -> + NewNum = case {is_stanza(El), StateData#state.mgmt_stanzas_in} of + {true, 4294967295} -> + 0; + {true, Num} -> + Num + 1; + {false, Num} -> + Num + end, + StateData#state{mgmt_stanzas_in = NewNum}; +update_num_stanzas_in(StateData, _El) -> + StateData. + +send_stanza_and_ack_req(StateData, Stanza) -> + AckReq = #xmlel{name = <<"r">>, + attrs = [{<<"xmlns">>, StateData#state.mgmt_xmlns}], + children = []}, + StanzaS = xml:element_to_binary(Stanza), + AckReqS = xml:element_to_binary(AckReq), + send_text(StateData, [StanzaS, AckReqS]). + +mgmt_queue_add(StateData, El) -> + NewNum = case StateData#state.mgmt_stanzas_out of + 4294967295 -> + 0; + Num -> + Num + 1 + end, + NewQueue = queue:in({NewNum, El}, StateData#state.mgmt_queue), + NewState = StateData#state{mgmt_queue = NewQueue, + mgmt_stanzas_out = NewNum}, + check_queue_length(NewState). + +mgmt_queue_drop(StateData, NumHandled) -> + NewQueue = jlib:queue_drop_while(fun({N, _Stanza}) -> N =< NumHandled end, + StateData#state.mgmt_queue), + StateData#state{mgmt_queue = NewQueue}. + +check_queue_length(#state{mgmt_max_queue = Limit} = StateData) + when Limit == infinity; + Limit == exceeded -> + StateData; +check_queue_length(#state{mgmt_queue = Queue, + mgmt_max_queue = Limit} = StateData) -> + case queue:len(Queue) > Limit of + true -> + StateData#state{mgmt_max_queue = exceeded}; + false -> + StateData + end. + +handle_unacked_stanzas(StateData, F) + when StateData#state.mgmt_state == active; + StateData#state.mgmt_state == pending -> + Queue = StateData#state.mgmt_queue, + case queue:len(Queue) of + 0 -> + ok; + N -> + ?INFO_MSG("~B stanzas were not acknowledged by ~s", + [N, jlib:jid_to_string(StateData#state.jid)]), + lists:foreach( + fun({_, #xmlel{attrs = Attrs} = El}) -> + From_s = xml:get_attr_s(<<"from">>, Attrs), + From = jlib:string_to_jid(From_s), + To_s = xml:get_attr_s(<<"to">>, Attrs), + To = jlib:string_to_jid(To_s), + F(From, To, El) + end, queue:to_list(Queue)) + end; +handle_unacked_stanzas(_StateData, _F) -> + ok. + +handle_unacked_stanzas(StateData) + when StateData#state.mgmt_state == active; + StateData#state.mgmt_state == pending -> + ResendOnTimeout = + case StateData#state.mgmt_resend of + Resend when is_boolean(Resend) -> + Resend; + if_offline -> + ejabberd_sm:get_user_resources(StateData#state.user, + StateData#state.server) == [] + end, + ReRoute = case ResendOnTimeout of + true -> + fun ejabberd_router:route/3; + false -> + fun(From, To, El) -> + Err = + jlib:make_error_reply(El, + ?ERR_SERVICE_UNAVAILABLE), + ejabberd_router:route(To, From, Err) + end + end, + F = fun(From, To, El) -> + %% We'll drop the stanza if it was by some + %% encapsulating protocol as per XEP-0297. One such protocol is + %% XEP-0280, which says: "When a receiving server attempts to + %% deliver a forked message, and that message bounces with an + %% error for any reason, the receiving server MUST NOT forward + %% that error back to the original sender." Resending such a + %% stanza could easily lead to unexpected results as well. + case is_encapsulated_forward(El) of + true -> + ?DEBUG("Dropping forwarded stanza from ~s", + [xml:get_attr_s(<<"from">>, El#xmlel.attrs)]); + false -> + ReRoute(From, To, El) + end + end, + handle_unacked_stanzas(StateData, F); +handle_unacked_stanzas(_StateData) -> + ok. + +is_encapsulated_forward(#xmlel{name = <<"message">>} = El) -> + SubTag = case {xml:get_subtag(El, <<"sent">>), + xml:get_subtag(El, <<"received">>), + xml:get_subtag(El, <<"result">>)} of + {false, false, false} -> + false; + {Tag, false, false} -> + Tag; + {false, Tag, false} -> + Tag; + {_, _, Tag} -> + Tag + end, + if SubTag == false -> + false; + true -> + case xml:get_subtag(SubTag, <<"forwarded">>) of + false -> + false; + _ -> + true + end + end; +is_encapsulated_forward(_El) -> + false. + +inherit_session_state(#state{user = U, server = S} = StateData, ResumeID) -> + case jlib:base64_to_term(ResumeID) of + {term, {R, Time}} -> + case ejabberd_sm:get_session_pid(U, S, R) of + none -> + {error, <<"Previous session PID not found">>}; + OldPID -> + OldSID = {Time, OldPID}, + case catch resume_session(OldSID) of + {ok, OldStateData} -> + NewSID = {Time, self()}, % Old time, new PID + Priority = case OldStateData#state.pres_last of + undefined -> + 0; + Presence -> + get_priority_from_presence(Presence) + end, + Conn = get_conn_type(StateData), + Info = [{ip, StateData#state.ip}, {conn, Conn}, + {auth_module, StateData#state.auth_module}], + ejabberd_sm:open_session(NewSID, U, S, R, + Priority, Info), + {ok, StateData#state{conn = Conn, + sid = NewSID, + jid = OldStateData#state.jid, + resource = OldStateData#state.resource, + pres_t = OldStateData#state.pres_t, + pres_f = OldStateData#state.pres_f, + pres_a = OldStateData#state.pres_a, + pres_last = OldStateData#state.pres_last, + pres_pri = OldStateData#state.pres_pri, + pres_timestamp = OldStateData#state.pres_timestamp, + privacy_list = OldStateData#state.privacy_list, + aux_fields = OldStateData#state.aux_fields, + csi_state = OldStateData#state.csi_state, + csi_queue = OldStateData#state.csi_queue, + mgmt_xmlns = OldStateData#state.mgmt_xmlns, + mgmt_queue = OldStateData#state.mgmt_queue, + mgmt_timeout = OldStateData#state.mgmt_timeout, + mgmt_stanzas_in = OldStateData#state.mgmt_stanzas_in, + mgmt_stanzas_out = OldStateData#state.mgmt_stanzas_out, + mgmt_state = active}}; + {error, Msg} -> + {error, Msg}; + _ -> + {error, <<"Cannot grab session state">>} + end + end; + _ -> + {error, <<"Invalid 'previd' value">>} + end. + +resume_session({Time, PID}) -> + (?GEN_FSM):sync_send_all_state_event(PID, {resume_session, Time}, 3000). + +make_resume_id(StateData) -> + {Time, _} = StateData#state.sid, + jlib:term_to_base64({StateData#state.resource, Time}). + +%%%---------------------------------------------------------------------- +%%% XEP-0352 +%%%---------------------------------------------------------------------- + +csi_filter_stanza(#state{csi_state = CsiState, jid = JID} = StateData, + Stanza) -> + Action = ejabberd_hooks:run_fold(csi_filter_stanza, + StateData#state.server, + send, [Stanza]), + ?DEBUG("Going to ~p stanza for inactive client ~p", + [Action, jlib:jid_to_string(JID)]), + case Action of + queue -> csi_queue_add(StateData, Stanza); + drop -> StateData; + send -> + From = xml:get_tag_attr_s(<<"from">>, Stanza), + StateData1 = csi_queue_send(StateData, From), + StateData2 = send_stanza(StateData1#state{csi_state = active}, + Stanza), + StateData2#state{csi_state = CsiState} + end. + +csi_queue_add(#state{csi_queue = Queue, server = Host} = StateData, + #xmlel{children = Els} = Stanza) -> + From = xml:get_tag_attr_s(<<"from">>, Stanza), + Time = calendar:now_to_universal_time(os:timestamp()), + DelayTag = [jlib:timestamp_to_xml(Time, utc, + jlib:make_jid(<<"">>, Host, <<"">>), + <<"Client Inactive">>)], + NewStanza = Stanza#xmlel{children = Els ++ DelayTag}, + case length(StateData#state.csi_queue) >= csi_max_queue(StateData) of + true -> csi_queue_add(csi_queue_flush(StateData), NewStanza); + false -> + NewQueue = lists:keystore(From, 1, Queue, {From, NewStanza}), + StateData#state{csi_queue = NewQueue} + end. + +csi_queue_send(#state{csi_queue = Queue, csi_state = CsiState} = StateData, + From) -> + case lists:keytake(From, 1, Queue) of + {value, {From, Stanza}, NewQueue} -> + NewStateData = send_stanza(StateData#state{csi_state = active}, + Stanza), + NewStateData#state{csi_queue = NewQueue, csi_state = CsiState}; + false -> StateData + end. + +csi_queue_flush(#state{csi_queue = Queue, csi_state = CsiState, jid = JID} = + StateData) -> + ?DEBUG("Flushing CSI queue for ~s", [jlib:jid_to_string(JID)]), + NewStateData = + lists:foldl(fun({_From, Stanza}, AccState) -> + send_stanza(AccState, Stanza) + end, StateData#state{csi_state = active}, Queue), + NewStateData#state{csi_queue = [], csi_state = CsiState}. + +%% Make sure we won't push too many messages to the XEP-0198 queue when the +%% client becomes 'active' again. Otherwise, the client might not manage to +%% acknowledge the message flood in time. Also, don't let the queue grow to +%% more than 100 stanzas. +csi_max_queue(#state{mgmt_max_queue = infinity}) -> 100; +csi_max_queue(#state{mgmt_max_queue = Max}) when Max > 200 -> 100; +csi_max_queue(#state{mgmt_max_queue = Max}) when Max < 2 -> 1; +csi_max_queue(#state{mgmt_max_queue = Max}) -> Max div 2. + +%%%---------------------------------------------------------------------- +%%% JID Set memory footprint reduction code +%%%---------------------------------------------------------------------- + +%% Try to reduce the heap footprint of the four presence sets +%% by ensuring that we re-use strings and Jids wherever possible. +pack(S = #state{pres_a = A, pres_f = F, + pres_t = T}) -> + {NewA, Pack2} = pack_jid_set(A, gb_trees:empty()), + {NewF, Pack3} = pack_jid_set(F, Pack2), + {NewT, _Pack4} = pack_jid_set(T, Pack3), + S#state{pres_a = NewA, pres_f = NewF, + pres_t = NewT}. + +pack_jid_set(Set, Pack) -> + Jids = (?SETS):to_list(Set), + {PackedJids, NewPack} = pack_jids(Jids, Pack, []), + {(?SETS):from_list(PackedJids), NewPack}. + +pack_jids([], Pack, Acc) -> {Acc, Pack}; +pack_jids([{U, S, R} = Jid | Jids], Pack, Acc) -> + case gb_trees:lookup(Jid, Pack) of + {value, PackedJid} -> + pack_jids(Jids, Pack, [PackedJid | Acc]); + none -> + {NewU, Pack1} = pack_string(U, Pack), + {NewS, Pack2} = pack_string(S, Pack1), + {NewR, Pack3} = pack_string(R, Pack2), + NewJid = {NewU, NewS, NewR}, + NewPack = gb_trees:insert(NewJid, NewJid, Pack3), + pack_jids(Jids, NewPack, [NewJid | Acc]) + end. + +pack_string(String, Pack) -> + case gb_trees:lookup(String, Pack) of + {value, PackedString} -> {PackedString, Pack}; + none -> {String, gb_trees:insert(String, String, Pack)} + end. + +transform_listen_option(Opt, Opts) -> + [Opt|Opts]. diff --git a/mod_multicast/src/ejabberd_sup.erl b/mod_multicast/src/ejabberd_sup.erl index abbde74..c89a068 100644 --- a/mod_multicast/src/ejabberd_sup.erl +++ b/mod_multicast/src/ejabberd_sup.erl @@ -1,13 +1,30 @@ %%%---------------------------------------------------------------------- %%% File : ejabberd_sup.erl -%%% Author : Alexey Shchepin -%%% Purpose : -%%% Created : 31 Jan 2003 by Alexey Shchepin -%%% Id : $Id: ejabberd_sup.erl 440 2007-12-06 22:36:21Z badlop $ +%%% Author : Alexey Shchepin +%%% Purpose : Erlang/OTP supervisor +%%% Created : 31 Jan 2003 by Alexey Shchepin +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2014 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(ejabberd_sup). --author('alexey@sevcom.net'). +-author('alexey@process-one.net'). -behaviour(supervisor). @@ -45,13 +62,6 @@ init([]) -> brutal_kill, worker, [ejabberd_router]}, - Router_multicast = - {ejabberd_router_multicast, - {ejabberd_router_multicast, start_link, []}, - permanent, - brutal_kill, - worker, - [ejabberd_router_multicast]}, SM = {ejabberd_sm, {ejabberd_sm, start_link, []}, @@ -73,6 +83,13 @@ init([]) -> brutal_kill, worker, [ejabberd_local]}, + Captcha = + {ejabberd_captcha, + {ejabberd_captcha, start_link, []}, + permanent, + brutal_kill, + worker, + [ejabberd_captcha]}, Listener = {ejabberd_listener, {ejabberd_listener, start_link, []}, @@ -156,10 +173,10 @@ init([]) -> NodeGroups, SystemMonitor, Router, - Router_multicast, SM, S2S, Local, + Captcha, ReceiverSupervisor, C2SSupervisor, S2SInSupervisor, diff --git a/mod_multicast/src/mod_muc_room.erl b/mod_multicast/src/mod_muc_room.erl index c5bdae5..6a2a91d 100644 --- a/mod_multicast/src/mod_muc_room.erl +++ b/mod_multicast/src/mod_muc_room.erl @@ -1,16 +1,34 @@ %%%---------------------------------------------------------------------- %%% File : mod_muc_room.erl -%%% Author : Alexey Shchepin +%%% Author : Alexey Shchepin %%% Purpose : MUC room stuff -%%% Created : 19 Mar 2003 by Alexey Shchepin +%%% Created : 19 Mar 2003 by Alexey Shchepin +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2014 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_muc_room). --author('alexey@sevcom.net'). + +-author('alexey@process-one.net'). -behaviour(gen_fsm). - %% External exports -export([start_link/9, start_link/7, @@ -28,71 +46,25 @@ code_change/4]). -include("ejabberd.hrl"). --include("jlib.hrl"). -include("logger.hrl"). --define(MAX_USERS_DEFAULT, 200). +-include("jlib.hrl"). + +-include("mod_muc_room.hrl"). + -define(MAX_USERS_DEFAULT_LIST, [5, 10, 20, 30, 50, 100, 200, 500, 1000, 2000, 5000]). --define(SETS, gb_sets). --define(DICT, dict). - --record(lqueue, {queue, len, max}). - --record(config, {title = "", - allow_change_subj = true, - allow_query_users = true, - allow_private_messages = true, - public = true, - public_list = true, - persistent = false, - moderated = false, % TODO - members_by_default = true, - members_only = false, - allow_user_invites = false, - password_protected = false, - password = "", - anonymous = true, - max_users = ?MAX_USERS_DEFAULT, - logging = false - }). - --record(user, {jid, - nick, - role, - last_presence}). - --record(activity, {message_time = 0, - presence_time = 0, - message_shaper, - presence_shaper, - message, - presence}). - --record(state, {room, - host, - server_host, - access, - jid, - config = #config{}, - users = ?DICT:new(), - affiliations = ?DICT:new(), - history = lqueue_new(20), - subject = "", - subject_author = "", - just_created = false, - activity = ?DICT:new(), - room_shaper, - room_queue = queue:new()}). - - %-define(DBGFSM, true). -ifdef(DBGFSM). + -define(FSMOPTS, [{debug, [trace]}]). + -else. + -define(FSMOPTS, []). + -endif. %% Module start with or without supervisor: @@ -145,27 +117,39 @@ start_link(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts) -> %% {stop, StopReason} %%---------------------------------------------------------------------- init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Creator, _Nick, DefRoomOpts]) -> + process_flag(trap_exit, true), Shaper = shaper:new(RoomShaper), State = set_affiliation(Creator, owner, - #state{host = Host, - server_host = ServerHost, - access = Access, - room = Room, + #state{host = Host, server_host = ServerHost, + access = Access, room = Room, history = lqueue_new(HistorySize), - jid = jlib:make_jid(Room, Host, ""), + jid = jlib:make_jid(Room, Host, <<"">>), just_created = true, room_shaper = Shaper}), State1 = set_opts(DefRoomOpts, State), + if (State1#state.config)#config.persistent -> + mod_muc:store_room(State1#state.server_host, + State1#state.host, + State1#state.room, + make_opts(State1)); + true -> ok + end, + ?INFO_MSG("Created MUC room ~s@~s by ~s", + [Room, Host, jlib:jid_to_string(Creator)]), + add_to_log(room_existence, created, State1), + add_to_log(room_existence, started, State1), {ok, normal_state, State1}; init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts]) -> + process_flag(trap_exit, true), Shaper = shaper:new(RoomShaper), State = set_opts(Opts, #state{host = Host, server_host = ServerHost, access = Access, room = Room, history = lqueue_new(HistorySize), - jid = jlib:make_jid(Room, Host, ""), + jid = jlib:make_jid(Room, Host, <<"">>), room_shaper = Shaper}), + add_to_log(room_existence, started, State), {ok, normal_state, State}. %%---------------------------------------------------------------------- @@ -174,442 +158,570 @@ init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts]) -> %% {next_state, NextStateName, NextStateData, Timeout} | %% {stop, Reason, NewStateData} %%---------------------------------------------------------------------- -normal_state({route, From, "", - {xmlelement, "message", Attrs, Els} = Packet}, +normal_state({route, From, <<"">>, + #xmlel{name = <<"message">>, attrs = Attrs, + children = Els} = + Packet}, StateData) -> - Lang = xml:get_attr_s("xml:lang", Attrs), - case is_user_online(From, StateData) of - true -> - case xml:get_attr_s("type", Attrs) of - "groupchat" -> - Activity = get_user_activity(From, StateData), - Now = now_to_usec(now()), - MinMessageInterval = - trunc(gen_mod:get_module_opt( - StateData#state.server_host, - mod_muc, min_message_interval, 0) * 1000000), - Size = lists:flatlength(xml:element_to_string(Packet)), - {MessageShaper, MessageShaperInterval} = - shaper:update(Activity#activity.message_shaper, Size), - if - Activity#activity.message /= undefined -> - ErrText = "Traffic rate limit is exceeded", - Err = jlib:make_error_reply( - Packet, ?ERRT_RESOURCE_CONSTRAINT(Lang, ErrText)), - ejabberd_router:route( - StateData#state.jid, - From, Err), - {next_state, normal_state, StateData}; - Now >= Activity#activity.message_time + MinMessageInterval, - MessageShaperInterval == 0 -> - {RoomShaper, RoomShaperInterval} = - shaper:update(StateData#state.room_shaper, Size), - RoomQueueEmpty = queue:is_empty( - StateData#state.room_queue), - if - RoomShaperInterval == 0, - RoomQueueEmpty -> - NewActivity = Activity#activity{ - message_time = Now, - message_shaper = MessageShaper}, - StateData1 = - StateData#state{ - activity = ?DICT:store( - jlib:jid_tolower(From), - NewActivity, - StateData#state.activity), - room_shaper = RoomShaper}, - process_groupchat_message(From, Packet, StateData1); - true -> - StateData1 = - if - RoomQueueEmpty -> - erlang:send_after( - RoomShaperInterval, self(), - process_room_queue), - StateData#state{ - room_shaper = RoomShaper}; - true -> - StateData - end, - NewActivity = Activity#activity{ - message_time = Now, - message_shaper = MessageShaper, - message = Packet}, - RoomQueue = queue:in( - {message, From}, - StateData#state.room_queue), - StateData2 = - StateData1#state{ - activity = ?DICT:store( - jlib:jid_tolower(From), - NewActivity, - StateData#state.activity), - room_queue = RoomQueue}, - {next_state, normal_state, StateData2} - end; - true -> - MessageInterval = - (Activity#activity.message_time + - MinMessageInterval - Now) div 1000, - Interval = lists:max([MessageInterval, - MessageShaperInterval]), - erlang:send_after( - Interval, self(), {process_user_message, From}), - NewActivity = Activity#activity{ - message = Packet, - message_shaper = MessageShaper}, - StateData1 = - StateData#state{ - activity = ?DICT:store( - jlib:jid_tolower(From), - NewActivity, - StateData#state.activity)}, - {next_state, normal_state, StateData1} - end; - "error" -> - case is_user_online(From, StateData) of - true -> - NewState = - add_user_presence_un( - From, - {xmlelement, "presence", - [{"type", "unavailable"}], []}, - StateData), - send_new_presence(From, NewState), - {next_state, normal_state, - remove_online_user(From, NewState)}; - _ -> - {next_state, normal_state, StateData} - end; - "chat" -> - ErrText = "It is not allowed to send private messages to the conference", - Err = jlib:make_error_reply( - Packet, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)), - ejabberd_router:route( - StateData#state.jid, - From, Err), - {next_state, normal_state, StateData}; - Type when (Type == "") or (Type == "normal") -> - case catch check_invitation(From, Els, Lang, StateData) of - {error, Error} -> - Err = jlib:make_error_reply( - Packet, Error), - ejabberd_router:route( - StateData#state.jid, - From, Err), - {next_state, normal_state, StateData}; - IJID -> - Config = StateData#state.config, - case Config#config.members_only of - true -> - case get_affiliation(IJID, StateData) of - none -> - NSD = set_affiliation( - IJID, - member, - StateData), - case (NSD#state.config)#config.persistent of - true -> - mod_muc:store_room( - NSD#state.host, - NSD#state.room, - make_opts(NSD)); - _ -> - ok - end, - {next_state, normal_state, NSD}; + Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), + case is_user_online(From, StateData) orelse + is_user_allowed_message_nonparticipant(From, StateData) + of + true -> + case xml:get_attr_s(<<"type">>, Attrs) of + <<"groupchat">> -> + Activity = get_user_activity(From, StateData), + Now = now_to_usec(now()), + MinMessageInterval = + trunc(gen_mod:get_module_opt(StateData#state.server_host, + mod_muc, min_message_interval, fun(MMI) when is_integer(MMI) -> MMI end, 0) + * 1000000), + Size = element_size(Packet), + {MessageShaper, MessageShaperInterval} = + shaper:update(Activity#activity.message_shaper, Size), + if Activity#activity.message /= undefined -> + ErrText = <<"Traffic rate limit is exceeded">>, + Err = jlib:make_error_reply(Packet, + ?ERRT_RESOURCE_CONSTRAINT(Lang, + ErrText)), + ejabberd_router:route(StateData#state.jid, From, Err), + {next_state, normal_state, StateData}; + Now >= + Activity#activity.message_time + MinMessageInterval, + MessageShaperInterval == 0 -> + {RoomShaper, RoomShaperInterval} = + shaper:update(StateData#state.room_shaper, Size), + RoomQueueEmpty = + queue:is_empty(StateData#state.room_queue), + if RoomShaperInterval == 0, RoomQueueEmpty -> + NewActivity = Activity#activity{message_time = + Now, + message_shaper = + MessageShaper}, + StateData1 = store_user_activity(From, + NewActivity, + StateData), + StateData2 = StateData1#state{room_shaper = + RoomShaper}, + process_groupchat_message(From, Packet, + StateData2); + true -> + StateData1 = if RoomQueueEmpty -> + erlang:send_after(RoomShaperInterval, + self(), + process_room_queue), + StateData#state{room_shaper = + RoomShaper}; + true -> StateData + end, + NewActivity = Activity#activity{message_time = + Now, + message_shaper = + MessageShaper, + message = Packet}, + RoomQueue = queue:in({message, From}, + StateData#state.room_queue), + StateData2 = store_user_activity(From, + NewActivity, + StateData1), + StateData3 = StateData2#state{room_queue = + RoomQueue}, + {next_state, normal_state, StateData3} + end; + true -> + MessageInterval = (Activity#activity.message_time + + MinMessageInterval + - Now) + div 1000, + Interval = lists:max([MessageInterval, + MessageShaperInterval]), + erlang:send_after(Interval, self(), + {process_user_message, From}), + NewActivity = Activity#activity{message = Packet, + message_shaper = + MessageShaper}, + StateData1 = store_user_activity(From, NewActivity, + StateData), + {next_state, normal_state, StateData1} + end; + <<"error">> -> + case is_user_online(From, StateData) of + true -> + ErrorText = <<"This participant is kicked from the " + "room because he sent an error message">>, + NewState = expulse_participant(Packet, From, StateData, + translate:translate(Lang, + ErrorText)), + close_room_if_temporary_and_empty(NewState); + _ -> {next_state, normal_state, StateData} + end; + <<"chat">> -> + ErrText = + <<"It is not allowed to send private messages " + "to the conference">>, + Err = jlib:make_error_reply(Packet, + ?ERRT_NOT_ACCEPTABLE(Lang, + ErrText)), + ejabberd_router:route(StateData#state.jid, From, Err), + {next_state, normal_state, StateData}; + Type when (Type == <<"">>) or (Type == <<"normal">>) -> + IsInvitation = is_invitation(Els), + IsVoiceRequest = is_voice_request(Els) and + is_visitor(From, StateData), + IsVoiceApprovement = is_voice_approvement(Els) and + not is_visitor(From, StateData), + if IsInvitation -> + case catch check_invitation(From, Els, Lang, StateData) + of + {error, Error} -> + Err = jlib:make_error_reply(Packet, Error), + ejabberd_router:route(StateData#state.jid, From, Err), + {next_state, normal_state, StateData}; + IJID -> + Config = StateData#state.config, + case Config#config.members_only of + true -> + case get_affiliation(IJID, StateData) of + none -> + NSD = set_affiliation(IJID, member, + StateData), + case + (NSD#state.config)#config.persistent + of + true -> + mod_muc:store_room(NSD#state.server_host, + NSD#state.host, + NSD#state.room, + make_opts(NSD)); + _ -> ok + end, + {next_state, normal_state, NSD}; + _ -> {next_state, normal_state, StateData} + end; + false -> {next_state, normal_state, StateData} + end + end; + IsVoiceRequest -> + NewStateData = case + (StateData#state.config)#config.allow_voice_requests + of + true -> + MinInterval = + (StateData#state.config)#config.voice_request_min_interval, + BareFrom = + jlib:jid_remove_resource(jlib:jid_tolower(From)), + NowPriority = -now_to_usec(now()), + CleanPriority = NowPriority + + MinInterval * + 1000000, + Times = + clean_treap(StateData#state.last_voice_request_time, + CleanPriority), + case treap:lookup(BareFrom, Times) + of + error -> + Times1 = + treap:insert(BareFrom, + NowPriority, + true, Times), + NSD = + StateData#state{last_voice_request_time + = + Times1}, + send_voice_request(From, NSD), + NSD; + {ok, _, _} -> + ErrText = + <<"Please, wait for a while before sending " + "new voice request">>, + Err = + jlib:make_error_reply(Packet, + ?ERRT_NOT_ACCEPTABLE(Lang, + ErrText)), + ejabberd_router:route(StateData#state.jid, + From, Err), + StateData#state{last_voice_request_time + = Times} + end; + false -> + ErrText = + <<"Voice requests are disabled in this " + "conference">>, + Err = jlib:make_error_reply(Packet, + ?ERRT_FORBIDDEN(Lang, + ErrText)), + ejabberd_router:route(StateData#state.jid, + From, Err), + StateData + end, + {next_state, normal_state, NewStateData}; + IsVoiceApprovement -> + NewStateData = case is_moderator(From, StateData) of + true -> + case + extract_jid_from_voice_approvement(Els) + of + error -> + ErrText = + <<"Failed to extract JID from your voice " + "request approval">>, + Err = + jlib:make_error_reply(Packet, + ?ERRT_BAD_REQUEST(Lang, + ErrText)), + ejabberd_router:route(StateData#state.jid, + From, Err), + StateData; + {ok, TargetJid} -> + case is_visitor(TargetJid, + StateData) + of + true -> + Reason = <<>>, + NSD = + set_role(TargetJid, + participant, + StateData), + catch + send_new_presence(TargetJid, + Reason, + NSD), + NSD; + _ -> StateData + end + end; _ -> - {next_state, normal_state, - StateData} - end; - false -> - {next_state, normal_state, StateData} - end - end; - _ -> - ErrText = "Improper message type", - Err = jlib:make_error_reply( - Packet, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)), - ejabberd_router:route( - StateData#state.jid, - From, Err), - {next_state, normal_state, StateData} - end; - _ -> - case xml:get_attr_s("type", Attrs) of - "error" -> - ok; - _ -> - ErrText = "Only occupants are allowed to send messages to the conference", - Err = jlib:make_error_reply( - Packet, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)), - ejabberd_router:route(StateData#state.jid, From, Err) - end, - {next_state, normal_state, StateData} + ErrText = + <<"Only moderators can approve voice requests">>, + Err = jlib:make_error_reply(Packet, + ?ERRT_NOT_ALLOWED(Lang, + ErrText)), + ejabberd_router:route(StateData#state.jid, + From, Err), + StateData + end, + {next_state, normal_state, NewStateData}; + true -> {next_state, normal_state, StateData} + end; + _ -> + ErrText = <<"Improper message type">>, + Err = jlib:make_error_reply(Packet, + ?ERRT_NOT_ACCEPTABLE(Lang, + ErrText)), + ejabberd_router:route(StateData#state.jid, From, Err), + {next_state, normal_state, StateData} + end; + _ -> + case xml:get_attr_s(<<"type">>, Attrs) of + <<"error">> -> ok; + _ -> + handle_roommessage_from_nonparticipant(Packet, Lang, + StateData, From) + end, + {next_state, normal_state, StateData} end; - -normal_state({route, From, "", - {xmlelement, "iq", _Attrs, _Els} = Packet}, +normal_state({route, From, <<"">>, + #xmlel{name = <<"iq">>} = Packet}, StateData) -> case jlib:iq_query_info(Packet) of - #iq{type = Type, xmlns = XMLNS, lang = Lang, sub_el = SubEl} = IQ when - (XMLNS == ?NS_MUC_ADMIN) or - (XMLNS == ?NS_MUC_OWNER) or - (XMLNS == ?NS_DISCO_INFO) or - (XMLNS == ?NS_DISCO_ITEMS) -> - Res1 = case XMLNS of - ?NS_MUC_ADMIN -> - process_iq_admin(From, Type, Lang, SubEl, StateData); - ?NS_MUC_OWNER -> - process_iq_owner(From, Type, Lang, SubEl, StateData); - ?NS_DISCO_INFO -> - process_iq_disco_info(From, Type, Lang, StateData); - ?NS_DISCO_ITEMS -> - process_iq_disco_items(From, Type, Lang, StateData) - end, - {IQRes, NewStateData} = - case Res1 of - {result, Res, SD} -> - {IQ#iq{type = result, - sub_el = [{xmlelement, "query", - [{"xmlns", XMLNS}], - Res - }]}, - SD}; - {error, Error} -> - {IQ#iq{type = error, - sub_el = [SubEl, Error]}, - StateData} - end, - ejabberd_router:route(StateData#state.jid, - From, - jlib:iq_to_xml(IQRes)), - case NewStateData of - stop -> - {stop, normal, StateData}; - _ -> - {next_state, normal_state, NewStateData} - end; - reply -> - {next_state, normal_state, StateData}; - _ -> - Err = jlib:make_error_reply( - Packet, ?ERR_FEATURE_NOT_IMPLEMENTED), - ejabberd_router:route(StateData#state.jid, From, Err), - {next_state, normal_state, StateData} + #iq{type = Type, xmlns = XMLNS, lang = Lang, + sub_el = #xmlel{name = SubElName} = SubEl} = + IQ + when (XMLNS == (?NS_MUC_ADMIN)) or + (XMLNS == (?NS_MUC_OWNER)) + or (XMLNS == (?NS_DISCO_INFO)) + or (XMLNS == (?NS_DISCO_ITEMS)) + or (XMLNS == (?NS_VCARD)) + or (XMLNS == (?NS_CAPTCHA)) -> + Res1 = case XMLNS of + ?NS_MUC_ADMIN -> + process_iq_admin(From, Type, Lang, SubEl, StateData); + ?NS_MUC_OWNER -> + process_iq_owner(From, Type, Lang, SubEl, StateData); + ?NS_DISCO_INFO -> + process_iq_disco_info(From, Type, Lang, StateData); + ?NS_DISCO_ITEMS -> + process_iq_disco_items(From, Type, Lang, StateData); + ?NS_VCARD -> + process_iq_vcard(From, Type, Lang, SubEl, StateData); + ?NS_CAPTCHA -> + process_iq_captcha(From, Type, Lang, SubEl, StateData) + end, + {IQRes, NewStateData} = case Res1 of + {result, Res, SD} -> + {IQ#iq{type = result, + sub_el = + [#xmlel{name = SubElName, + attrs = + [{<<"xmlns">>, + XMLNS}], + children = Res}]}, + SD}; + {error, Error} -> + {IQ#iq{type = error, + sub_el = [SubEl, Error]}, + StateData} + end, + ejabberd_router:route(StateData#state.jid, From, + jlib:iq_to_xml(IQRes)), + case NewStateData of + stop -> {stop, normal, StateData}; + _ -> {next_state, normal_state, NewStateData} + end; + reply -> {next_state, normal_state, StateData}; + _ -> + Err = jlib:make_error_reply(Packet, + ?ERR_FEATURE_NOT_IMPLEMENTED), + ejabberd_router:route(StateData#state.jid, From, Err), + {next_state, normal_state, StateData} end; - normal_state({route, From, Nick, - {xmlelement, "presence", _Attrs, _Els} = Packet}, + #xmlel{name = <<"presence">>} = Packet}, StateData) -> Activity = get_user_activity(From, StateData), Now = now_to_usec(now()), MinPresenceInterval = - trunc(gen_mod:get_module_opt( - StateData#state.server_host, - mod_muc, min_presence_interval, 0) * 1000000), - if - (Now >= Activity#activity.presence_time + MinPresenceInterval) and - (Activity#activity.presence == undefined) -> - NewActivity = Activity#activity{presence_time = Now}, - StateData1 = - StateData#state{ - activity = ?DICT:store( - jlib:jid_tolower(From), - NewActivity, - StateData#state.activity)}, - process_presence(From, Nick, Packet, StateData1); - true -> - if - Activity#activity.presence == undefined -> - Interval = (Activity#activity.presence_time + - MinPresenceInterval - Now) div 1000, - erlang:send_after( - Interval, self(), {process_user_presence, From}); - true -> - ok - end, - NewActivity = Activity#activity{presence = {Nick, Packet}}, - StateData1 = - StateData#state{ - activity = ?DICT:store( - jlib:jid_tolower(From), - NewActivity, - StateData#state.activity)}, - {next_state, normal_state, StateData1} + trunc(gen_mod:get_module_opt(StateData#state.server_host, + mod_muc, min_presence_interval, + fun(I) when is_number(I), I>=0 -> + I + end, 0) + * 1000000), + if (Now >= + Activity#activity.presence_time + MinPresenceInterval) + and (Activity#activity.presence == undefined) -> + NewActivity = Activity#activity{presence_time = Now}, + StateData1 = store_user_activity(From, NewActivity, + StateData), + process_presence(From, Nick, Packet, StateData1); + true -> + if Activity#activity.presence == undefined -> + Interval = (Activity#activity.presence_time + + MinPresenceInterval + - Now) + div 1000, + erlang:send_after(Interval, self(), + {process_user_presence, From}); + true -> ok + end, + NewActivity = Activity#activity{presence = + {Nick, Packet}}, + StateData1 = store_user_activity(From, NewActivity, + StateData), + {next_state, normal_state, StateData1} end; - normal_state({route, From, ToNick, - {xmlelement, "message", Attrs, _Els} = Packet}, + #xmlel{name = <<"message">>, attrs = Attrs} = Packet}, StateData) -> - Type = xml:get_attr_s("type", Attrs), - Lang = xml:get_attr_s("xml:lang", Attrs), - case Type of - "error" -> - case is_user_online(From, StateData) of - true -> - NewState = - add_user_presence_un( - From, - {xmlelement, "presence", - [{"type", "unavailable"}], []}, - StateData), - send_new_presence(From, NewState), - {next_state, normal_state, - remove_online_user(From, NewState)}; - _ -> - {next_state, normal_state, StateData} - end; - _ -> - case (StateData#state.config)#config.allow_private_messages - andalso is_user_online(From, StateData) of - true -> - case Type of - "groupchat" -> - ErrText = "It is not allowed to send private " - "messages of type \"groupchat\"", - Err = jlib:make_error_reply( - Packet, ?ERRT_BAD_REQUEST(Lang, ErrText)), - ejabberd_router:route( - jlib:jid_replace_resource( - StateData#state.jid, - ToNick), - From, Err); - _ -> - case find_jid_by_nick(ToNick, StateData) of - false -> - ErrText = "Recipient is not in the conference room", - Err = jlib:make_error_reply( - Packet, ?ERRT_ITEM_NOT_FOUND(Lang, ErrText)), - ejabberd_router:route( - jlib:jid_replace_resource( - StateData#state.jid, - ToNick), - From, Err); - ToJID -> - {ok, #user{nick = FromNick}} = - ?DICT:find(jlib:jid_tolower(From), - StateData#state.users), - ejabberd_router:route( - jlib:jid_replace_resource( - StateData#state.jid, - FromNick), - ToJID, Packet) + Type = xml:get_attr_s(<<"type">>, Attrs), + Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), + case decide_fate_message(Type, Packet, From, StateData) + of + {expulse_sender, Reason} -> + ?DEBUG(Reason, []), + ErrorText = <<"This participant is kicked from the " + "room because he sent an error message " + "to another participant">>, + NewState = expulse_participant(Packet, From, StateData, + translate:translate(Lang, ErrorText)), + {next_state, normal_state, NewState}; + forget_message -> {next_state, normal_state, StateData}; + continue_delivery -> + case + {(StateData#state.config)#config.allow_private_messages, + is_user_online(From, StateData)} + of + {true, true} -> + case Type of + <<"groupchat">> -> + ErrText = + <<"It is not allowed to send private messages " + "of type \"groupchat\"">>, + Err = jlib:make_error_reply(Packet, + ?ERRT_BAD_REQUEST(Lang, + ErrText)), + ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, + ToNick), + From, Err); + _ -> + case find_jids_by_nick(ToNick, StateData) of + false -> + ErrText = + <<"Recipient is not in the conference room">>, + Err = jlib:make_error_reply(Packet, + ?ERRT_ITEM_NOT_FOUND(Lang, + ErrText)), + ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, + ToNick), + From, Err); + ToJIDs -> + SrcIsVisitor = is_visitor(From, StateData), + DstIsModerator = is_moderator(hd(ToJIDs), + StateData), + PmFromVisitors = + (StateData#state.config)#config.allow_private_messages_from_visitors, + if SrcIsVisitor == false; + PmFromVisitors == anyone; + (PmFromVisitors == moderators) and + DstIsModerator -> + {ok, #user{nick = FromNick}} = + (?DICT):find(jlib:jid_tolower(From), + StateData#state.users), + FromNickJID = + jlib:jid_replace_resource(StateData#state.jid, + FromNick), + [ejabberd_router:route(FromNickJID, ToJID, Packet) + || ToJID <- ToJIDs]; + true -> + ErrText = + <<"It is not allowed to send private messages">>, + Err = jlib:make_error_reply(Packet, + ?ERRT_FORBIDDEN(Lang, + ErrText)), + ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, + ToNick), + From, Err) end - end; - _ -> - ErrText = "Only occupants are allowed to send messages to the conference", - Err = jlib:make_error_reply( - Packet, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)), - ejabberd_router:route( - jlib:jid_replace_resource( - StateData#state.jid, - ToNick), - From, Err) - end, - {next_state, normal_state, StateData} + end + end; + {true, false} -> + ErrText = + <<"Only occupants are allowed to send messages " + "to the conference">>, + Err = jlib:make_error_reply(Packet, + ?ERRT_NOT_ACCEPTABLE(Lang, + ErrText)), + ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, + ToNick), + From, Err); + {false, _} -> + ErrText = + <<"It is not allowed to send private messages">>, + Err = jlib:make_error_reply(Packet, + ?ERRT_FORBIDDEN(Lang, ErrText)), + ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, + ToNick), + From, Err) + end, + {next_state, normal_state, StateData} end; - normal_state({route, From, ToNick, - {xmlelement, "iq", Attrs, _Els} = Packet}, + #xmlel{name = <<"iq">>, attrs = Attrs} = Packet}, StateData) -> - Lang = xml:get_attr_s("xml:lang", Attrs), + Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), + StanzaId = xml:get_attr_s(<<"id">>, Attrs), case {(StateData#state.config)#config.allow_query_users, - is_user_online(From, StateData)} of - {true, true} -> - case find_jid_by_nick(ToNick, StateData) of - false -> - case jlib:iq_query_info(Packet) of - reply -> - ok; - _ -> - ErrText = "Recipient is not in the conference room", - Err = jlib:make_error_reply( - Packet, ?ERRT_ITEM_NOT_FOUND(Lang, ErrText)), - ejabberd_router:route( - jlib:jid_replace_resource( - StateData#state.jid, ToNick), - From, Err) - end; - ToJID -> - {ok, #user{nick = FromNick}} = - ?DICT:find(jlib:jid_tolower(From), - StateData#state.users), - ejabberd_router:route( - jlib:jid_replace_resource(StateData#state.jid, FromNick), - ToJID, Packet) - end; - {_, false} -> - case jlib:iq_query_info(Packet) of - reply -> - ok; - _ -> - ErrText = "Only occupants are allowed to send queries to the conference", - Err = jlib:make_error_reply( - Packet, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)), - ejabberd_router:route( - jlib:jid_replace_resource(StateData#state.jid, ToNick), - From, Err) - end; - _ -> - case jlib:iq_query_info(Packet) of - reply -> - ok; - _ -> - ErrText = "Queries to the conference members are not allowed in this room", - Err = jlib:make_error_reply( - Packet, ?ERRT_NOT_ALLOWED(Lang, ErrText)), - ejabberd_router:route( - jlib:jid_replace_resource(StateData#state.jid, ToNick), - From, Err) - end + is_user_online_iq(StanzaId, From, StateData)} + of + {true, {true, NewId, FromFull}} -> + case find_jid_by_nick(ToNick, StateData) of + false -> + case jlib:iq_query_info(Packet) of + reply -> ok; + _ -> + ErrText = <<"Recipient is not in the conference room">>, + Err = jlib:make_error_reply(Packet, + ?ERRT_ITEM_NOT_FOUND(Lang, + ErrText)), + ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, + ToNick), + From, Err) + end; + ToJID -> + {ok, #user{nick = FromNick}} = + (?DICT):find(jlib:jid_tolower(FromFull), + StateData#state.users), + {ToJID2, Packet2} = handle_iq_vcard(FromFull, ToJID, + StanzaId, NewId, Packet), + ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, + FromNick), + ToJID2, Packet2) + end; + {_, {false, _, _}} -> + case jlib:iq_query_info(Packet) of + reply -> ok; + _ -> + ErrText = + <<"Only occupants are allowed to send queries " + "to the conference">>, + Err = jlib:make_error_reply(Packet, + ?ERRT_NOT_ACCEPTABLE(Lang, + ErrText)), + ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, + ToNick), + From, Err) + end; + _ -> + case jlib:iq_query_info(Packet) of + reply -> ok; + _ -> + ErrText = <<"Queries to the conference members are " + "not allowed in this room">>, + Err = jlib:make_error_reply(Packet, + ?ERRT_NOT_ALLOWED(Lang, ErrText)), + ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, + ToNick), + From, Err) + end end, {next_state, normal_state, StateData}; - normal_state(_Event, StateData) -> {next_state, normal_state, StateData}. - - %%---------------------------------------------------------------------- %% Func: handle_event/3 %% Returns: {next_state, NextStateName, NextStateData} | %% {next_state, NextStateName, NextStateData, Timeout} | %% {stop, Reason, NewStateData} %%---------------------------------------------------------------------- -handle_event({service_message, Msg}, _StateName, StateData) -> - MessagePkt = {xmlelement, "message", - [{"type", "groupchat"}], - [{xmlelement, "body", [], [{xmlcdata, Msg}]}]}, - send_multiple( - StateData#state.jid, - StateData#state.server_host, - StateData#state.users, - MessagePkt), - NSD = add_message_to_history("", - MessagePkt, - StateData), +handle_event({service_message, Msg}, _StateName, + StateData) -> + MessagePkt = #xmlel{name = <<"message">>, + attrs = [{<<"type">>, <<"groupchat">>}], + children = + [#xmlel{name = <<"body">>, attrs = [], + children = [{xmlcdata, Msg}]}]}, + lists:foreach( + fun({_LJID, Info}) -> + ejabberd_router:route( + StateData#state.jid, + Info#user.jid, + MessagePkt) + end, + ?DICT:to_list(StateData#state.users)), + NSD = add_message_to_history(<<"">>, + StateData#state.jid, MessagePkt, StateData), {next_state, normal_state, NSD}; - -handle_event({destroy, Reason}, _StateName, StateData) -> - {result, [], stop} = - destroy_room( - {xmlelement, "destroy", - [{"xmlns", ?NS_MUC_OWNER}], - case Reason of - none -> []; - _Else -> - [{xmlelement, "reason", - [], [{xmlcdata, Reason}]}] - end}, StateData), - {stop, normal, StateData}; +handle_event({destroy, Reason}, _StateName, + StateData) -> + {result, [], stop} = destroy_room(#xmlel{name = + <<"destroy">>, + attrs = + [{<<"xmlns">>, ?NS_MUC_OWNER}], + children = + case Reason of + none -> []; + _Else -> + [#xmlel{name = + <<"reason">>, + attrs = [], + children = + [{xmlcdata, + Reason}]}] + end}, + StateData), + ?INFO_MSG("Destroyed MUC room ~s with reason: ~p", + [jlib:jid_to_string(StateData#state.jid), Reason]), + add_to_log(room_existence, destroyed, StateData), + {stop, shutdown, StateData}; handle_event(destroy, StateName, StateData) -> + ?INFO_MSG("Destroyed MUC room ~s", + [jlib:jid_to_string(StateData#state.jid)]), handle_event({destroy, none}, StateName, StateData); - -handle_event({set_affiliations, Affiliations}, StateName, StateData) -> - {next_state, StateName, StateData#state{affiliations = Affiliations}}; - +handle_event({set_affiliations, Affiliations}, + StateName, StateData) -> + {next_state, StateName, + StateData#state{affiliations = Affiliations}}; handle_event(_Event, StateName, StateData) -> {next_state, StateName, StateData}. @@ -623,46 +735,26 @@ handle_event(_Event, StateName, StateData) -> %% {stop, Reason, Reply, NewStateData} %%---------------------------------------------------------------------- handle_sync_event({get_disco_item, JID, Lang}, _From, StateName, StateData) -> - FAffiliation = get_affiliation(JID, StateData), - FRole = get_role(JID, StateData), - Tail = - case ((StateData#state.config)#config.public_list == true) orelse - (FRole /= none) orelse - (FAffiliation == admin) orelse - (FAffiliation == owner) of - true -> - Desc = case (StateData#state.config)#config.public of - true -> - ""; - _ -> - translate:translate(Lang, "private, ") - end, - Len = ?DICT:fold(fun(_, _, Acc) -> Acc + 1 end, 0, - StateData#state.users), - " (" ++ Desc ++ integer_to_list(Len) ++ ")"; - _ -> - "" - end, - Reply = case ((StateData#state.config)#config.public == true) orelse - (FRole /= none) orelse - (FAffiliation == admin) orelse - (FAffiliation == owner) of - true -> - {item, get_title(StateData) ++ Tail}; - _ -> - false - end, + Reply = get_roomdesc_reply(JID, StateData, + get_roomdesc_tail(StateData, Lang)), {reply, Reply, StateName, StateData}; -handle_sync_event(get_config, _From, StateName, StateData) -> - {reply, {ok, StateData#state.config}, StateName, StateData}; -handle_sync_event(get_state, _From, StateName, StateData) -> +handle_sync_event(get_config, _From, StateName, + StateData) -> + {reply, {ok, StateData#state.config}, StateName, + StateData}; +handle_sync_event(get_state, _From, StateName, + StateData) -> {reply, {ok, StateData}, StateName, StateData}; -handle_sync_event({change_config, Config}, _From, StateName, StateData) -> +handle_sync_event({change_config, Config}, _From, + StateName, StateData) -> {result, [], NSD} = change_config(Config, StateData), {reply, {ok, NSD#state.config}, StateName, NSD}; -handle_sync_event(_Event, _From, StateName, StateData) -> - Reply = ok, - {reply, Reply, StateName, StateData}. +handle_sync_event({change_state, NewStateData}, _From, + StateName, _StateData) -> + {reply, {ok, NewStateData}, StateName, NewStateData}; +handle_sync_event(_Event, _From, StateName, + StateData) -> + Reply = ok, {reply, Reply, StateName, StateData}. code_change(_OldVsn, StateName, StateData, _Extra) -> {ok, StateName, StateData}. @@ -674,62 +766,81 @@ code_change(_OldVsn, StateName, StateData, _Extra) -> %% {stop, Reason, NewStateData} %%---------------------------------------------------------------------- handle_info({process_user_presence, From}, normal_state = _StateName, StateData) -> - Activity = get_user_activity(From, StateData), - Now = now_to_usec(now()), - {Nick, Packet} = Activity#activity.presence, - NewActivity = Activity#activity{presence_time = Now, - presence = undefined}, - StateData1 = - StateData#state{ - activity = ?DICT:store( - jlib:jid_tolower(From), - NewActivity, - StateData#state.activity)}, - process_presence(From, Nick, Packet, StateData1); -handle_info({process_user_message, From}, normal_state = _StateName, StateData) -> - Activity = get_user_activity(From, StateData), - Now = now_to_usec(now()), - Packet = Activity#activity.message, - NewActivity = Activity#activity{message_time = Now, - message = undefined}, - StateData1 = - StateData#state{ - activity = ?DICT:store( - jlib:jid_tolower(From), - NewActivity, - StateData#state.activity)}, - process_groupchat_message(From, Packet, StateData1); -handle_info(process_room_queue, normal_state = StateName, StateData) -> - case queue:out(StateData#state.room_queue) of - {{value, {message, From}}, RoomQueue} -> - Activity = get_user_activity(From, StateData), - Packet = Activity#activity.message, - NewActivity = Activity#activity{message = undefined}, - StateData1 = - StateData#state{ - activity = ?DICT:store( - jlib:jid_tolower(From), - NewActivity, - StateData#state.activity), - room_queue = RoomQueue}, - StateData2 = prepare_room_queue(StateData1), - process_groupchat_message(From, Packet, StateData2); - {{value, {presence, From}}, RoomQueue} -> - Activity = get_user_activity(From, StateData), - {Nick, Packet} = Activity#activity.presence, - NewActivity = Activity#activity{presence = undefined}, - StateData1 = - StateData#state{ - activity = ?DICT:store( - jlib:jid_tolower(From), - NewActivity, - StateData#state.activity), - room_queue = RoomQueue}, - StateData2 = prepare_room_queue(StateData1), - process_presence(From, Nick, Packet, StateData2); - {empty, _} -> - {next_state, StateName, StateData} + RoomQueueEmpty = queue:is_empty(StateData#state.room_queue), + RoomQueue = queue:in({presence, From}, StateData#state.room_queue), + StateData1 = StateData#state{room_queue = RoomQueue}, + if RoomQueueEmpty -> + StateData2 = prepare_room_queue(StateData1), + {next_state, normal_state, StateData2}; + true -> {next_state, normal_state, StateData1} end; +handle_info({process_user_message, From}, + normal_state = _StateName, StateData) -> + RoomQueueEmpty = + queue:is_empty(StateData#state.room_queue), + RoomQueue = queue:in({message, From}, + StateData#state.room_queue), + StateData1 = StateData#state{room_queue = RoomQueue}, + if RoomQueueEmpty -> + StateData2 = prepare_room_queue(StateData1), + {next_state, normal_state, StateData2}; + true -> {next_state, normal_state, StateData1} + end; +handle_info(process_room_queue, + normal_state = StateName, StateData) -> + case queue:out(StateData#state.room_queue) of + {{value, {message, From}}, RoomQueue} -> + Activity = get_user_activity(From, StateData), + Packet = Activity#activity.message, + NewActivity = Activity#activity{message = undefined}, + StateData1 = store_user_activity(From, NewActivity, + StateData), + StateData2 = StateData1#state{room_queue = RoomQueue}, + StateData3 = prepare_room_queue(StateData2), + process_groupchat_message(From, Packet, StateData3); + {{value, {presence, From}}, RoomQueue} -> + Activity = get_user_activity(From, StateData), + {Nick, Packet} = Activity#activity.presence, + NewActivity = Activity#activity{presence = undefined}, + StateData1 = store_user_activity(From, NewActivity, + StateData), + StateData2 = StateData1#state{room_queue = RoomQueue}, + StateData3 = prepare_room_queue(StateData2), + process_presence(From, Nick, Packet, StateData3); + {empty, _} -> {next_state, StateName, StateData} + end; +handle_info({captcha_succeed, From}, normal_state, + StateData) -> + NewState = case (?DICT):find(From, + StateData#state.robots) + of + {ok, {Nick, Packet}} -> + Robots = (?DICT):store(From, passed, + StateData#state.robots), + add_new_user(From, Nick, Packet, + StateData#state{robots = Robots}); + _ -> StateData + end, + {next_state, normal_state, NewState}; +handle_info({captcha_failed, From}, normal_state, + StateData) -> + NewState = case (?DICT):find(From, + StateData#state.robots) + of + {ok, {Nick, Packet}} -> + Robots = (?DICT):erase(From, StateData#state.robots), + Err = jlib:make_error_reply(Packet, + ?ERR_NOT_AUTHORIZED), + ejabberd_router:route % TODO: s/Nick/""/ + (jlib:jid_replace_resource(StateData#state.jid, + Nick), + From, Err), + StateData#state{robots = Robots}; + _ -> StateData + end, + {next_state, normal_state, NewState}; +handle_info(shutdown, _StateName, StateData) -> + {stop, shutdown, StateData}; handle_info(_Info, StateName, StateData) -> {next_state, StateName, StateData}. @@ -738,7 +849,44 @@ handle_info(_Info, StateName, StateData) -> %% Purpose: Shutdown the fsm %% Returns: any %%---------------------------------------------------------------------- -terminate(_Reason, _StateName, StateData) -> +terminate(Reason, _StateName, StateData) -> + ?INFO_MSG("Stopping MUC room ~s@~s", + [StateData#state.room, StateData#state.host]), + ReasonT = case Reason of + shutdown -> + <<"You are being removed from the room " + "because of a system shutdown">>; + _ -> <<"Room terminates">> + end, + ItemAttrs = [{<<"affiliation">>, <<"none">>}, + {<<"role">>, <<"none">>}], + ReasonEl = #xmlel{name = <<"reason">>, attrs = [], + children = [{xmlcdata, ReasonT}]}, + Packet = #xmlel{name = <<"presence">>, + attrs = [{<<"type">>, <<"unavailable">>}], + children = + [#xmlel{name = <<"x">>, + attrs = [{<<"xmlns">>, ?NS_MUC_USER}], + children = + [#xmlel{name = <<"item">>, + attrs = ItemAttrs, + children = [ReasonEl]}, + #xmlel{name = <<"status">>, + attrs = [{<<"code">>, <<"332">>}], + children = []}]}]}, + (?DICT):fold(fun (LJID, Info, _) -> + Nick = Info#user.nick, + case Reason of + shutdown -> + ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, + Nick), + Info#user.jid, Packet); + _ -> ok + end, + tab_remove_online_user(LJID, StateData) + end, + [], StateData#state.users), + add_to_log(room_existence, stopped, StateData), mod_muc:room_destroyed(StateData#state.host, StateData#state.room, self(), StateData#state.server_host), ok. @@ -750,1596 +898,2218 @@ terminate(_Reason, _StateName, StateData) -> route(Pid, From, ToNick, Packet) -> gen_fsm:send_event(Pid, {route, From, ToNick, Packet}). -process_groupchat_message(From, {xmlelement, "message", Attrs, _Els} = Packet, +process_groupchat_message(From, + #xmlel{name = <<"message">>, attrs = Attrs} = Packet, StateData) -> - Lang = xml:get_attr_s("xml:lang", Attrs), - case is_user_online(From, StateData) of - true -> - {ok, #user{nick = FromNick, role = Role}} = - ?DICT:find(jlib:jid_tolower(From), - StateData#state.users), - if - (Role == moderator) or (Role == participant) -> - {NewStateData1, IsAllowed} = - case check_subject(Packet) of - false -> - {StateData, true}; - Subject -> - case can_change_subject(Role, - StateData) of - true -> - NSD = - StateData#state{ - subject = Subject, - subject_author = - FromNick}, - case (NSD#state.config)#config.persistent of - true -> - mod_muc:store_room( - NSD#state.host, - NSD#state.room, - make_opts(NSD)); - _ -> - ok - end, - {NSD, true}; - _ -> - {StateData, false} - end - end, - case IsAllowed of - true -> - send_multiple( - jlib:jid_replace_resource(StateData#state.jid, FromNick), - StateData#state.server_host, - StateData#state.users, - Packet), - NewStateData2 = - add_message_to_history(FromNick, - Packet, - NewStateData1), - {next_state, normal_state, NewStateData2}; - _ -> - Err = - case (StateData#state.config)#config.allow_change_subj of - true -> - ?ERRT_FORBIDDEN( - Lang, - "Only moderators and participants " - "are allowed to change subject in this room"); - _ -> - ?ERRT_FORBIDDEN( - Lang, - "Only moderators " - "are allowed to change subject in this room") - end, - ejabberd_router:route( - StateData#state.jid, - From, - jlib:make_error_reply(Packet, Err)), - {next_state, normal_state, StateData} - end; - true -> - ErrText = "Visitors are not allowed to send messages to all occupants", - Err = jlib:make_error_reply( - Packet, ?ERRT_FORBIDDEN(Lang, ErrText)), - ejabberd_router:route( - StateData#state.jid, - From, Err), - {next_state, normal_state, StateData} - end; - false -> - ErrText = "Only occupants are allowed to send messages to the conference", - Err = jlib:make_error_reply( - Packet, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)), - ejabberd_router:route(StateData#state.jid, From, Err), - {next_state, normal_state, StateData} + Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), + case is_user_online(From, StateData) orelse + is_user_allowed_message_nonparticipant(From, StateData) + of + true -> + {FromNick, Role} = get_participant_data(From, + StateData), + if (Role == moderator) or (Role == participant) or + ((StateData#state.config)#config.moderated == false) -> + {NewStateData1, IsAllowed} = case check_subject(Packet) + of + false -> {StateData, true}; + Subject -> + case + can_change_subject(Role, + StateData) + of + true -> + NSD = + StateData#state{subject + = + Subject, + subject_author + = + FromNick}, + case + (NSD#state.config)#config.persistent + of + true -> + mod_muc:store_room(NSD#state.server_host, + NSD#state.host, + NSD#state.room, + make_opts(NSD)); + _ -> ok + end, + {NSD, true}; + _ -> {StateData, false} + end + end, + case IsAllowed of + true -> + lists:foreach( + fun({_LJID, Info}) -> + ejabberd_router:route( + jlib:jid_replace_resource( + StateData#state.jid, + FromNick), + Info#user.jid, + Packet) + end, + ?DICT:to_list(StateData#state.users)), + NewStateData2 = case has_body_or_subject(Packet) of + true -> + add_message_to_history(FromNick, From, + Packet, + NewStateData1); + false -> + NewStateData1 + end, + {next_state, normal_state, NewStateData2}; + _ -> + Err = case + (StateData#state.config)#config.allow_change_subj + of + true -> + ?ERRT_FORBIDDEN(Lang, + <<"Only moderators and participants are " + "allowed to change the subject in this " + "room">>); + _ -> + ?ERRT_FORBIDDEN(Lang, + <<"Only moderators are allowed to change " + "the subject in this room">>) + end, + ejabberd_router:route(StateData#state.jid, From, + jlib:make_error_reply(Packet, Err)), + {next_state, normal_state, StateData} + end; + true -> + ErrText = <<"Visitors are not allowed to send messages " + "to all occupants">>, + Err = jlib:make_error_reply(Packet, + ?ERRT_FORBIDDEN(Lang, ErrText)), + ejabberd_router:route(StateData#state.jid, From, Err), + {next_state, normal_state, StateData} + end; + false -> + ErrText = + <<"Only occupants are allowed to send messages " + "to the conference">>, + Err = jlib:make_error_reply(Packet, + ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)), + ejabberd_router:route(StateData#state.jid, From, Err), + {next_state, normal_state, StateData} end. -process_presence(From, Nick, {xmlelement, "presence", Attrs, _Els} = Packet, +%% @doc Check if this non participant can send message to room. +%% +%% XEP-0045 v1.23: +%% 7.9 Sending a Message to All Occupants +%% an implementation MAY allow users with certain privileges +%% (e.g., a room owner, room admin, or service-level admin) +%% to send messages to the room even if those users are not occupants. +is_user_allowed_message_nonparticipant(JID, + StateData) -> + case get_service_affiliation(JID, StateData) of + owner -> true; + _ -> false + end. + +%% @doc Get information of this participant, or default values. +%% If the JID is not a participant, return values for a service message. +get_participant_data(From, StateData) -> + case (?DICT):find(jlib:jid_tolower(From), + StateData#state.users) + of + {ok, #user{nick = FromNick, role = Role}} -> + {FromNick, Role}; + error -> {<<"">>, moderator} + end. + +process_presence(From, Nick, + #xmlel{name = <<"presence">>, attrs = Attrs} = Packet, StateData) -> - Type = xml:get_attr_s("type", Attrs), - Lang = xml:get_attr_s("xml:lang", Attrs), - StateData1 = - case Type of - "unavailable" -> - case is_user_online(From, StateData) of - true -> - NewState = - add_user_presence_un(From, Packet, StateData), - send_new_presence(From, NewState), - Reason = case xml:get_subtag(Packet, "status") of - false -> ""; - Status_el -> xml:get_tag_cdata(Status_el) - end, - remove_online_user(From, NewState, Reason); - _ -> - StateData - end; - "error" -> - case is_user_online(From, StateData) of - true -> - NewState = - add_user_presence_un( - From, - {xmlelement, "presence", - [{"type", "unavailable"}], []}, - StateData), - send_new_presence(From, NewState), - remove_online_user(From, NewState); - _ -> - StateData - end; - "" -> - case is_user_online(From, StateData) of - true -> - case is_nick_change(From, Nick, StateData) of - true -> - case {is_nick_exists(Nick, StateData), - mod_muc:can_use_nick( - StateData#state.host, From, Nick)} of - {true, _} -> - Lang = xml:get_attr_s("xml:lang", Attrs), - ErrText = "Nickname is already in use by another occupant", - Err = jlib:make_error_reply( - Packet, - ?ERRT_CONFLICT(Lang, ErrText)), - ejabberd_router:route( - jlib:jid_replace_resource( - StateData#state.jid, - Nick), % TODO: s/Nick/""/ - From, Err), - StateData; - {_, false} -> - ErrText = "Nickname is registered by another person", - Err = jlib:make_error_reply( - Packet, - ?ERRT_CONFLICT(Lang, ErrText)), - ejabberd_router:route( - % TODO: s/Nick/""/ - jlib:jid_replace_resource( - StateData#state.jid, - Nick), - From, Err), - StateData; - _ -> - change_nick(From, Nick, StateData) - end; - _ -> - NewState = - add_user_presence(From, Packet, StateData), - send_new_presence(From, NewState), - NewState - end; - _ -> - add_new_user(From, Nick, Packet, StateData) - end; - _ -> - StateData - end, - case (not (StateData1#state.config)#config.persistent) andalso - (?DICT:to_list(StateData1#state.users) == []) of - true -> - {stop, normal, StateData1}; - _ -> - {next_state, normal_state, StateData1} + Type = xml:get_attr_s(<<"type">>, Attrs), + Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), + StateData1 = case Type of + <<"unavailable">> -> + case is_user_online(From, StateData) of + true -> + NewPacket = case + {(StateData#state.config)#config.allow_visitor_status, + is_visitor(From, StateData)} + of + {false, true} -> + strip_status(Packet); + _ -> Packet + end, + NewState = add_user_presence_un(From, NewPacket, + StateData), + case (?DICT):find(Nick, StateData#state.nicks) of + {ok, [_, _ | _]} -> ok; + _ -> send_new_presence(From, NewState) + end, + Reason = case xml:get_subtag(NewPacket, + <<"status">>) + of + false -> <<"">>; + Status_el -> + xml:get_tag_cdata(Status_el) + end, + remove_online_user(From, NewState, Reason); + _ -> StateData + end; + <<"error">> -> + case is_user_online(From, StateData) of + true -> + ErrorText = + <<"This participant is kicked from the " + "room because he sent an error presence">>, + expulse_participant(Packet, From, StateData, + translate:translate(Lang, + ErrorText)); + _ -> StateData + end; + <<"">> -> + case is_user_online(From, StateData) of + true -> + case is_nick_change(From, Nick, StateData) of + true -> + case {nick_collision(From, Nick, StateData), + mod_muc:can_use_nick(StateData#state.server_host, + StateData#state.host, + From, Nick), + {(StateData#state.config)#config.allow_visitor_nickchange, + is_visitor(From, StateData)}} + of + {_, _, {false, true}} -> + ErrText = + <<"Visitors are not allowed to change their " + "nicknames in this room">>, + Err = jlib:make_error_reply(Packet, + ?ERRT_NOT_ALLOWED(Lang, + ErrText)), + ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, + Nick), + From, Err), + StateData; + {true, _, _} -> + Lang = xml:get_attr_s(<<"xml:lang">>, + Attrs), + ErrText = + <<"That nickname is already in use by another " + "occupant">>, + Err = jlib:make_error_reply(Packet, + ?ERRT_CONFLICT(Lang, + ErrText)), + ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, + Nick), % TODO: s/Nick/""/ + From, Err), + StateData; + {_, false, _} -> + ErrText = + <<"That nickname is registered by another " + "person">>, + Err = jlib:make_error_reply(Packet, + ?ERRT_CONFLICT(Lang, + ErrText)), + ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, + Nick), + From, Err), + StateData; + _ -> change_nick(From, Nick, StateData) + end; + _NotNickChange -> + Stanza = case + {(StateData#state.config)#config.allow_visitor_status, + is_visitor(From, StateData)} + of + {false, true} -> + strip_status(Packet); + _Allowed -> Packet + end, + NewState = add_user_presence(From, Stanza, + StateData), + send_new_presence(From, NewState), + NewState + end; + _ -> add_new_user(From, Nick, Packet, StateData) + end; + _ -> StateData + end, + close_room_if_temporary_and_empty(StateData1). + +close_room_if_temporary_and_empty(StateData1) -> + case not (StateData1#state.config)#config.persistent + andalso (?DICT):to_list(StateData1#state.users) == [] + of + true -> + ?INFO_MSG("Destroyed MUC room ~s because it's temporary " + "and empty", + [jlib:jid_to_string(StateData1#state.jid)]), + add_to_log(room_existence, destroyed, StateData1), + {stop, normal, StateData1}; + _ -> {next_state, normal_state, StateData1} end. is_user_online(JID, StateData) -> LJID = jlib:jid_tolower(JID), - ?DICT:is_key(LJID, StateData#state.users). + (?DICT):is_key(LJID, StateData#state.users). + +%% Check if the user is occupant of the room, or at least is an admin or owner. +is_occupant_or_admin(JID, StateData) -> + FAffiliation = get_affiliation(JID, StateData), + FRole = get_role(JID, StateData), + case FRole /= none orelse + FAffiliation == member orelse + FAffiliation == admin orelse FAffiliation == owner + of + true -> true; + _ -> false + end. + +%%% +%%% Handle IQ queries of vCard +%%% +is_user_online_iq(StanzaId, JID, StateData) + when JID#jid.lresource /= <<"">> -> + {is_user_online(JID, StateData), StanzaId, JID}; +is_user_online_iq(StanzaId, JID, StateData) + when JID#jid.lresource == <<"">> -> + try stanzaid_unpack(StanzaId) of + {OriginalId, Resource} -> + JIDWithResource = jlib:jid_replace_resource(JID, + Resource), + {is_user_online(JIDWithResource, StateData), OriginalId, + JIDWithResource} + catch + _:_ -> {is_user_online(JID, StateData), StanzaId, JID} + end. + +handle_iq_vcard(FromFull, ToJID, StanzaId, NewId, + Packet) -> + ToBareJID = jlib:jid_remove_resource(ToJID), + IQ = jlib:iq_query_info(Packet), + handle_iq_vcard2(FromFull, ToJID, ToBareJID, StanzaId, + NewId, IQ, Packet). + +handle_iq_vcard2(_FromFull, ToJID, ToBareJID, StanzaId, + _NewId, #iq{type = get, xmlns = ?NS_VCARD}, Packet) + when ToBareJID /= ToJID -> + {ToBareJID, change_stanzaid(StanzaId, ToJID, Packet)}; +handle_iq_vcard2(_FromFull, ToJID, _ToBareJID, + _StanzaId, NewId, _IQ, Packet) -> + {ToJID, change_stanzaid(NewId, Packet)}. + +stanzaid_pack(OriginalId, Resource) -> + <<"berd", + (jlib:encode_base64(<<"ejab\000", + OriginalId/binary, "\000", + Resource/binary>>))/binary>>. + +stanzaid_unpack(<<"berd", StanzaIdBase64/binary>>) -> + StanzaId = jlib:decode_base64(StanzaIdBase64), + [<<"ejab">>, OriginalId, Resource] = + str:tokens(StanzaId, <<"\000">>), + {OriginalId, Resource}. + +change_stanzaid(NewId, Packet) -> + #xmlel{name = Name, attrs = Attrs, children = Els} = + jlib:remove_attr(<<"id">>, Packet), + #xmlel{name = Name, attrs = [{<<"id">>, NewId} | Attrs], + children = Els}. + +change_stanzaid(PreviousId, ToJID, Packet) -> + NewId = stanzaid_pack(PreviousId, ToJID#jid.lresource), + change_stanzaid(NewId, Packet). + +%%% +%%% role_to_list(Role) -> case Role of - moderator -> "moderator"; - participant -> "participant"; - visitor -> "visitor"; - none -> "none" + moderator -> <<"moderator">>; + participant -> <<"participant">>; + visitor -> <<"visitor">>; + none -> <<"none">> end. affiliation_to_list(Affiliation) -> case Affiliation of - owner -> "owner"; - admin -> "admin"; - member -> "member"; - outcast -> "outcast"; - none -> "none" + owner -> <<"owner">>; + admin -> <<"admin">>; + member -> <<"member">>; + outcast -> <<"outcast">>; + none -> <<"none">> end. list_to_role(Role) -> case Role of - "moderator" -> moderator; - "participant" -> participant; - "visitor" -> visitor; - "none" -> none + <<"moderator">> -> moderator; + <<"participant">> -> participant; + <<"visitor">> -> visitor; + <<"none">> -> none end. list_to_affiliation(Affiliation) -> case Affiliation of - "owner" -> owner; - "admin" -> admin; - "member" -> member; - "outcast" -> outcast; - "none" -> none + <<"owner">> -> owner; + <<"admin">> -> admin; + <<"member">> -> member; + <<"outcast">> -> outcast; + <<"none">> -> none end. +%% Decide the fate of the message and its sender +%% Returns: continue_delivery | forget_message | {expulse_sender, Reason} +decide_fate_message(<<"error">>, Packet, From, + StateData) -> + PD = case check_error_kick(Packet) of + %% If this is an error stanza and its condition matches a criteria + true -> + Reason = + io_lib:format("This participant is considered a ghost " + "and is expulsed: ~s", + [jlib:jid_to_string(From)]), + {expulse_sender, Reason}; + false -> continue_delivery + end, + case PD of + {expulse_sender, R} -> + case is_user_online(From, StateData) of + true -> {expulse_sender, R}; + false -> forget_message + end; + Other -> Other + end; +decide_fate_message(_, _, _, _) -> continue_delivery. +%% Check if the elements of this error stanza indicate +%% that the sender is a dead participant. +%% If so, return true to kick the participant. +check_error_kick(Packet) -> + case get_error_condition(Packet) of + <<"gone">> -> true; + <<"internal-server-error">> -> true; + <<"item-not-found">> -> true; + <<"jid-malformed">> -> true; + <<"recipient-unavailable">> -> true; + <<"redirect">> -> true; + <<"remote-server-not-found">> -> true; + <<"remote-server-timeout">> -> true; + <<"service-unavailable">> -> true; + _ -> false + end. + +get_error_condition(Packet) -> + case catch get_error_condition2(Packet) of + {condition, ErrorCondition} -> ErrorCondition; + {'EXIT', _} -> <<"badformed error stanza">> + end. + +get_error_condition2(Packet) -> + #xmlel{children = EEls} = xml:get_subtag(Packet, + <<"error">>), + [Condition] = [Name + || #xmlel{name = Name, + attrs = [{<<"xmlns">>, ?NS_STANZAS}], + children = []} + <- EEls], + {condition, Condition}. + +expulse_participant(Packet, From, StateData, Reason1) -> + ErrorCondition = get_error_condition(Packet), + Reason2 = iolist_to_binary( + io_lib:format(binary_to_list(Reason1) ++ ": " ++ "~s", + [ErrorCondition])), + NewState = add_user_presence_un(From, + #xmlel{name = <<"presence">>, + attrs = + [{<<"type">>, + <<"unavailable">>}], + children = + [#xmlel{name = <<"status">>, + attrs = [], + children = + [{xmlcdata, + Reason2}]}]}, + StateData), + send_new_presence(From, NewState), + remove_online_user(From, NewState). set_affiliation(JID, Affiliation, StateData) -> - LJID = jlib:jid_remove_resource(jlib:jid_tolower(JID)), - Affiliations = case Affiliation of - none -> - ?DICT:erase(LJID, - StateData#state.affiliations); - _ -> - ?DICT:store(LJID, - Affiliation, - StateData#state.affiliations) - end, - StateData#state{affiliations = Affiliations}. + set_affiliation(JID, Affiliation, StateData, <<"">>). -set_affiliation_and_reason(JID, Affiliation, Reason, StateData) -> +set_affiliation(JID, Affiliation, StateData, Reason) -> LJID = jlib:jid_remove_resource(jlib:jid_tolower(JID)), Affiliations = case Affiliation of - none -> - ?DICT:erase(LJID, - StateData#state.affiliations); - _ -> - ?DICT:store(LJID, - {Affiliation, Reason}, + none -> + (?DICT):erase(LJID, StateData#state.affiliations); + _ -> + (?DICT):store(LJID, {Affiliation, Reason}, StateData#state.affiliations) end, StateData#state{affiliations = Affiliations}. get_affiliation(JID, StateData) -> - {_AccessRoute, _AccessCreate, AccessAdmin, _AccessPersistent} = StateData#state.access, - Res = - case acl:match_rule(StateData#state.server_host, AccessAdmin, JID) of - allow -> - owner; + {_AccessRoute, _AccessCreate, AccessAdmin, + _AccessPersistent} = + StateData#state.access, + Res = case acl:match_rule(StateData#state.server_host, + AccessAdmin, JID) + of + allow -> owner; _ -> LJID = jlib:jid_tolower(JID), - case ?DICT:find(LJID, StateData#state.affiliations) of - {ok, Affiliation} -> - Affiliation; - _ -> - LJID1 = jlib:jid_remove_resource(LJID), - case ?DICT:find(LJID1, StateData#state.affiliations) of - {ok, Affiliation} -> - Affiliation; - _ -> - LJID2 = setelement(1, LJID, ""), - case ?DICT:find(LJID2, StateData#state.affiliations) of - {ok, Affiliation} -> - Affiliation; - _ -> - LJID3 = jlib:jid_remove_resource(LJID2), - case ?DICT:find(LJID3, StateData#state.affiliations) of - {ok, Affiliation} -> - Affiliation; - _ -> - none - end - end - end + case (?DICT):find(LJID, StateData#state.affiliations) of + {ok, Affiliation} -> Affiliation; + _ -> + LJID1 = jlib:jid_remove_resource(LJID), + case (?DICT):find(LJID1, StateData#state.affiliations) + of + {ok, Affiliation} -> Affiliation; + _ -> + LJID2 = setelement(1, LJID, <<"">>), + case (?DICT):find(LJID2, + StateData#state.affiliations) + of + {ok, Affiliation} -> Affiliation; + _ -> + LJID3 = jlib:jid_remove_resource(LJID2), + case (?DICT):find(LJID3, + StateData#state.affiliations) + of + {ok, Affiliation} -> Affiliation; + _ -> none + end + end + end end - end, + end, case Res of - {A, _Reason} -> - A; - _ -> - Res + {A, _Reason} -> A; + _ -> Res end. get_service_affiliation(JID, StateData) -> - {_AccessRoute, _AccessCreate, AccessAdmin, _AccessPersistent} = + {_AccessRoute, _AccessCreate, AccessAdmin, + _AccessPersistent} = StateData#state.access, - case acl:match_rule(StateData#state.server_host, AccessAdmin, JID) of - allow -> - owner; - _ -> - none + case acl:match_rule(StateData#state.server_host, + AccessAdmin, JID) + of + allow -> owner; + _ -> none end. set_role(JID, Role, StateData) -> LJID = jlib:jid_tolower(JID), LJIDs = case LJID of - {U, S, ""} -> - ?DICT:fold( - fun(J, _, Js) -> - case J of - {U, S, _} -> - [J | Js]; - _ -> - Js - end - end, [], StateData#state.users); - _ -> - case ?DICT:is_key(LJID, StateData#state.users) of - true -> - [LJID]; - _ -> - [] - end + {U, S, <<"">>} -> + (?DICT):fold(fun (J, _, Js) -> + case J of + {U, S, _} -> [J | Js]; + _ -> Js + end + end, + [], StateData#state.users); + _ -> + case (?DICT):is_key(LJID, StateData#state.users) of + true -> [LJID]; + _ -> [] + end end, - Users = case Role of - none -> - lists:foldl(fun(J, Us) -> - ?DICT:erase(J, - Us) - end, StateData#state.users, LJIDs); - _ -> - lists:foldl(fun(J, Us) -> - {ok, User} = ?DICT:find(J, Us), - ?DICT:store(J, - User#user{role = Role}, - Us) - end, StateData#state.users, LJIDs) - end, - StateData#state{users = Users}. + {Users, Nicks} = case Role of + none -> + lists:foldl(fun (J, {Us, Ns}) -> + NewNs = case (?DICT):find(J, Us) + of + {ok, + #user{nick = Nick}} -> + (?DICT):erase(Nick, + Ns); + _ -> Ns + end, + {(?DICT):erase(J, Us), NewNs} + end, + {StateData#state.users, + StateData#state.nicks}, + LJIDs); + _ -> + {lists:foldl(fun (J, Us) -> + {ok, User} = (?DICT):find(J, + Us), + (?DICT):store(J, + User#user{role = + Role}, + Us) + end, + StateData#state.users, LJIDs), + StateData#state.nicks} + end, + StateData#state{users = Users, nicks = Nicks}. get_role(JID, StateData) -> LJID = jlib:jid_tolower(JID), - case ?DICT:find(LJID, StateData#state.users) of - {ok, #user{role = Role}} -> - Role; - _ -> - none + case (?DICT):find(LJID, StateData#state.users) of + {ok, #user{role = Role}} -> Role; + _ -> none end. get_default_role(Affiliation, StateData) -> case Affiliation of - owner -> moderator; - admin -> moderator; - member -> participant; - outcast -> none; - none -> - case (StateData#state.config)#config.members_only of - true -> - none; - _ -> - case (StateData#state.config)#config.members_by_default of - true -> - participant; - _ -> - visitor - end - end + owner -> moderator; + admin -> moderator; + member -> participant; + outcast -> none; + none -> + case (StateData#state.config)#config.members_only of + true -> none; + _ -> + case (StateData#state.config)#config.members_by_default + of + true -> participant; + _ -> visitor + end + end end. +is_visitor(Jid, StateData) -> + get_role(Jid, StateData) =:= visitor. + +is_moderator(Jid, StateData) -> + get_role(Jid, StateData) =:= moderator. + get_max_users(StateData) -> MaxUsers = (StateData#state.config)#config.max_users, ServiceMaxUsers = get_service_max_users(StateData), - if - MaxUsers =< ServiceMaxUsers -> MaxUsers; - true -> ServiceMaxUsers + if MaxUsers =< ServiceMaxUsers -> MaxUsers; + true -> ServiceMaxUsers end. get_service_max_users(StateData) -> gen_mod:get_module_opt(StateData#state.server_host, - mod_muc, max_users, ?MAX_USERS_DEFAULT). + mod_muc, max_users, + fun(I) when is_integer(I), I>0 -> I end, + ?MAX_USERS_DEFAULT). get_max_users_admin_threshold(StateData) -> gen_mod:get_module_opt(StateData#state.server_host, - mod_muc, max_users_admin_threshold, 5). + mod_muc, max_users_admin_threshold, + fun(I) when is_integer(I), I>0 -> I end, + 5). get_user_activity(JID, StateData) -> - case ?DICT:find(jlib:jid_tolower(JID), - StateData#state.activity) of - {ok, A} -> A; - error -> - MessageShaper = - shaper:new(gen_mod:get_module_opt( - StateData#state.server_host, - mod_muc, user_message_shaper, none)), - PresenceShaper = - shaper:new(gen_mod:get_module_opt( - StateData#state.server_host, - mod_muc, user_presence_shaper, none)), - #activity{message_shaper = MessageShaper, - presence_shaper = PresenceShaper} + case treap:lookup(jlib:jid_tolower(JID), + StateData#state.activity) + of + {ok, _P, A} -> A; + error -> + MessageShaper = + shaper:new(gen_mod:get_module_opt(StateData#state.server_host, + mod_muc, user_message_shaper, + fun(A) when is_atom(A) -> A end, + none)), + PresenceShaper = + shaper:new(gen_mod:get_module_opt(StateData#state.server_host, + mod_muc, user_presence_shaper, + fun(A) when is_atom(A) -> A end, + none)), + #activity{message_shaper = MessageShaper, + presence_shaper = PresenceShaper} + end. + +store_user_activity(JID, UserActivity, StateData) -> + MinMessageInterval = + gen_mod:get_module_opt(StateData#state.server_host, + mod_muc, min_message_interval, + fun(I) when is_integer(I), I>=0 -> I end, + 0), + MinPresenceInterval = + gen_mod:get_module_opt(StateData#state.server_host, + mod_muc, min_presence_interval, + fun(I) when is_integer(I), I>=0 -> I end, + 0), + Key = jlib:jid_tolower(JID), + Now = now_to_usec(now()), + Activity1 = clean_treap(StateData#state.activity, + {1, -Now}), + Activity = case treap:lookup(Key, Activity1) of + {ok, _P, _A} -> treap:delete(Key, Activity1); + error -> Activity1 + end, + StateData1 = case MinMessageInterval == 0 andalso + MinPresenceInterval == 0 andalso + UserActivity#activity.message_shaper == none andalso + UserActivity#activity.presence_shaper == none + andalso + UserActivity#activity.message == undefined andalso + UserActivity#activity.presence == undefined + of + true -> StateData#state{activity = Activity}; + false -> + case UserActivity#activity.message == undefined andalso + UserActivity#activity.presence == undefined + of + true -> + {_, MessageShaperInterval} = + shaper:update(UserActivity#activity.message_shaper, + 100000), + {_, PresenceShaperInterval} = + shaper:update(UserActivity#activity.presence_shaper, + 100000), + Delay = lists:max([MessageShaperInterval, + PresenceShaperInterval, + MinMessageInterval * 1000, + MinPresenceInterval * 1000]) + * 1000, + Priority = {1, -(Now + Delay)}, + StateData#state{activity = + treap:insert(Key, Priority, + UserActivity, + Activity)}; + false -> + Priority = {0, 0}, + StateData#state{activity = + treap:insert(Key, Priority, + UserActivity, + Activity)} + end + end, + StateData1. + +clean_treap(Treap, CleanPriority) -> + case treap:is_empty(Treap) of + true -> Treap; + false -> + {_Key, Priority, _Value} = treap:get_root(Treap), + if Priority > CleanPriority -> + clean_treap(treap:delete_root(Treap), CleanPriority); + true -> Treap + end end. prepare_room_queue(StateData) -> case queue:out(StateData#state.room_queue) of - {{value, {message, From}}, _RoomQueue} -> - Activity = get_user_activity(From, StateData), - Packet = Activity#activity.message, - Size = lists:flatlength(xml:element_to_string(Packet)), - {RoomShaper, RoomShaperInterval} = - shaper:update(StateData#state.room_shaper, Size), - erlang:send_after( - RoomShaperInterval, self(), - process_room_queue), - StateData#state{ - room_shaper = RoomShaper}; - {{value, {presence, From}}, _RoomQueue} -> - Activity = get_user_activity(From, StateData), - {_Nick, Packet} = Activity#activity.presence, - Size = lists:flatlength(xml:element_to_string(Packet)), - {RoomShaper, RoomShaperInterval} = - shaper:update(StateData#state.room_shaper, Size), - erlang:send_after( - RoomShaperInterval, self(), - process_room_queue), - StateData#state{ - room_shaper = RoomShaper}; - {empty, _} -> - StateData + {{value, {message, From}}, _RoomQueue} -> + Activity = get_user_activity(From, StateData), + Packet = Activity#activity.message, + Size = element_size(Packet), + {RoomShaper, RoomShaperInterval} = + shaper:update(StateData#state.room_shaper, Size), + erlang:send_after(RoomShaperInterval, self(), + process_room_queue), + StateData#state{room_shaper = RoomShaper}; + {{value, {presence, From}}, _RoomQueue} -> + Activity = get_user_activity(From, StateData), + {_Nick, Packet} = Activity#activity.presence, + Size = element_size(Packet), + {RoomShaper, RoomShaperInterval} = + shaper:update(StateData#state.room_shaper, Size), + erlang:send_after(RoomShaperInterval, self(), + process_room_queue), + StateData#state{room_shaper = RoomShaper}; + {empty, _} -> StateData end. - add_online_user(JID, Nick, Role, StateData) -> LJID = jlib:jid_tolower(JID), - Users = ?DICT:store(LJID, - #user{jid = JID, - nick = Nick, - role = Role}, - StateData#state.users), + Users = (?DICT):store(LJID, + #user{jid = JID, nick = Nick, role = Role}, + StateData#state.users), add_to_log(join, Nick, StateData), - StateData#state{users = Users}. + Nicks = (?DICT):update(Nick, + fun (Entry) -> + case lists:member(LJID, Entry) of + true -> Entry; + false -> [LJID | Entry] + end + end, + [LJID], StateData#state.nicks), + tab_add_online_user(JID, StateData), + StateData#state{users = Users, nicks = Nicks}. remove_online_user(JID, StateData) -> - remove_online_user(JID, StateData, ""). + remove_online_user(JID, StateData, <<"">>). remove_online_user(JID, StateData, Reason) -> LJID = jlib:jid_tolower(JID), - {ok, #user{nick = Nick}} = - ?DICT:find(LJID, StateData#state.users), + {ok, #user{nick = Nick}} = (?DICT):find(LJID, + StateData#state.users), add_to_log(leave, {Nick, Reason}, StateData), - Users = ?DICT:erase(LJID, StateData#state.users), - StateData#state{users = Users}. + tab_remove_online_user(JID, StateData), + Users = (?DICT):erase(LJID, StateData#state.users), + Nicks = case (?DICT):find(Nick, StateData#state.nicks) + of + {ok, [LJID]} -> + (?DICT):erase(Nick, StateData#state.nicks); + {ok, U} -> + (?DICT):store(Nick, U -- [LJID], StateData#state.nicks); + error -> StateData#state.nicks + end, + StateData#state{users = Users, nicks = Nicks}. +filter_presence(#xmlel{name = <<"presence">>, + attrs = Attrs, children = Els}) -> + FEls = lists:filter(fun (El) -> + case El of + {xmlcdata, _} -> false; + #xmlel{attrs = Attrs1} -> + XMLNS = xml:get_attr_s(<<"xmlns">>, + Attrs1), + NS_MUC = ?NS_MUC, + Size = byte_size(NS_MUC), + case XMLNS of + <> -> + false; + _ -> + true + end + end + end, + Els), + #xmlel{name = <<"presence">>, attrs = Attrs, + children = FEls}. -filter_presence({xmlelement, "presence", Attrs, Els}) -> - FEls = lists:filter( - fun(El) -> - case El of - {xmlcdata, _} -> - false; - {xmlelement, _Name1, Attrs1, _Els1} -> - XMLNS = xml:get_attr_s("xmlns", Attrs1), - case XMLNS of - "http://jabber.org/protocol/muc" ++ _ -> - false; - _ -> - true - end - end - end, Els), - {xmlelement, "presence", Attrs, FEls}. - +strip_status(#xmlel{name = <<"presence">>, + attrs = Attrs, children = Els}) -> + FEls = lists:filter(fun (#xmlel{name = <<"status">>}) -> + false; + (_) -> true + end, + Els), + #xmlel{name = <<"presence">>, attrs = Attrs, + children = FEls}. add_user_presence(JID, Presence, StateData) -> LJID = jlib:jid_tolower(JID), FPresence = filter_presence(Presence), - Users = - ?DICT:update( - LJID, - fun(#user{} = User) -> - User#user{last_presence = FPresence} - end, StateData#state.users), + Users = (?DICT):update(LJID, + fun (#user{} = User) -> + User#user{last_presence = FPresence} + end, + StateData#state.users), StateData#state{users = Users}. add_user_presence_un(JID, Presence, StateData) -> LJID = jlib:jid_tolower(JID), FPresence = filter_presence(Presence), - Users = - ?DICT:update( - LJID, - fun(#user{} = User) -> - User#user{last_presence = FPresence, - role = none} - end, StateData#state.users), + Users = (?DICT):update(LJID, + fun (#user{} = User) -> + User#user{last_presence = FPresence, + role = none} + end, + StateData#state.users), StateData#state{users = Users}. +%% Find and return a list of the full JIDs of the users of Nick. +%% Return jid record. +find_jids_by_nick(Nick, StateData) -> + case (?DICT):find(Nick, StateData#state.nicks) of + {ok, [User]} -> [jlib:make_jid(User)]; + {ok, Users} -> [jlib:make_jid(LJID) || LJID <- Users]; + error -> false + end. -is_nick_exists(Nick, StateData) -> - ?DICT:fold(fun(_, #user{nick = N}, B) -> - B orelse (N == Nick) - end, false, StateData#state.users). - +%% Find and return the full JID of the user of Nick with +%% highest-priority presence. Return jid record. find_jid_by_nick(Nick, StateData) -> - ?DICT:fold(fun(_, #user{jid = JID, nick = N}, R) -> - case Nick of - N -> JID; - _ -> R - end - end, false, StateData#state.users). + case (?DICT):find(Nick, StateData#state.nicks) of + {ok, [User]} -> jlib:make_jid(User); + {ok, [FirstUser | Users]} -> + #user{last_presence = FirstPresence} = + (?DICT):fetch(FirstUser, StateData#state.users), + {LJID, _} = lists:foldl(fun (Compare, + {HighestUser, HighestPresence}) -> + #user{last_presence = P1} = + (?DICT):fetch(Compare, + StateData#state.users), + case higher_presence(P1, + HighestPresence) + of + true -> {Compare, P1}; + false -> + {HighestUser, HighestPresence} + end + end, + {FirstUser, FirstPresence}, Users), + jlib:make_jid(LJID); + error -> false + end. + +higher_presence(Pres1, Pres2) -> + Pri1 = get_priority_from_presence(Pres1), + Pri2 = get_priority_from_presence(Pres2), + Pri1 > Pri2. + +get_priority_from_presence(PresencePacket) -> + case xml:get_subtag(PresencePacket, <<"priority">>) of + false -> 0; + SubEl -> + case catch + jlib:binary_to_integer(xml:get_tag_cdata(SubEl)) + of + P when is_integer(P) -> P; + _ -> 0 + end + end. + +find_nick_by_jid(Jid, StateData) -> + [{_, #user{nick = Nick}}] = lists:filter(fun ({_, + #user{jid = FJid}}) -> + FJid == Jid + end, + (?DICT):to_list(StateData#state.users)), + Nick. is_nick_change(JID, Nick, StateData) -> LJID = jlib:jid_tolower(JID), case Nick of - "" -> - false; - _ -> - {ok, #user{nick = OldNick}} = - ?DICT:find(LJID, StateData#state.users), - Nick /= OldNick + <<"">> -> false; + _ -> + {ok, #user{nick = OldNick}} = (?DICT):find(LJID, + StateData#state.users), + Nick /= OldNick end. -add_new_user(From, Nick, {xmlelement, _, Attrs, Els} = Packet, StateData) -> - Lang = xml:get_attr_s("xml:lang", Attrs), +nick_collision(User, Nick, StateData) -> + UserOfNick = find_jid_by_nick(Nick, StateData), + UserOfNick /= false andalso + jlib:jid_remove_resource(jlib:jid_tolower(UserOfNick)) + /= jlib:jid_remove_resource(jlib:jid_tolower(User)). + +add_new_user(From, Nick, + #xmlel{attrs = Attrs, children = Els} = Packet, + StateData) -> + Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), MaxUsers = get_max_users(StateData), - MaxAdminUsers = MaxUsers + get_max_users_admin_threshold(StateData), - NUsers = dict:fold(fun(_, _, Acc) -> Acc + 1 end, 0, + MaxAdminUsers = MaxUsers + + get_max_users_admin_threshold(StateData), + NUsers = dict:fold(fun (_, _, Acc) -> Acc + 1 end, 0, StateData#state.users), Affiliation = get_affiliation(From, StateData), - ServiceAffiliation = get_service_affiliation(From, StateData), - case {ServiceAffiliation == owner orelse - MaxUsers == none orelse - ((Affiliation == admin orelse Affiliation == owner) andalso - NUsers < MaxAdminUsers) orelse - NUsers < MaxUsers, - is_nick_exists(Nick, StateData), - mod_muc:can_use_nick(StateData#state.host, From, Nick), - get_default_role(Affiliation, StateData)} of - {false, _, _, _} -> - % max user reached and user is not admin or owner - Err = jlib:make_error_reply( - Packet, - ?ERR_SERVICE_UNAVAILABLE), - ejabberd_router:route( % TODO: s/Nick/""/ - jlib:jid_replace_resource(StateData#state.jid, Nick), - From, Err), - StateData; - {_, _, _, none} -> - Err = jlib:make_error_reply( - Packet, - case Affiliation of - outcast -> - ErrText = "You have been banned from this room", - ?ERRT_FORBIDDEN(Lang, ErrText); - _ -> - ErrText = "Membership required to enter this room", - ?ERRT_REGISTRATION_REQUIRED(Lang, ErrText) - end), - ejabberd_router:route( % TODO: s/Nick/""/ - jlib:jid_replace_resource(StateData#state.jid, Nick), - From, Err), - StateData; - {_, true, _, _} -> - ErrText = "Nickname is already in use by another occupant", - Err = jlib:make_error_reply(Packet, ?ERRT_CONFLICT(Lang, ErrText)), - ejabberd_router:route( - % TODO: s/Nick/""/ - jlib:jid_replace_resource(StateData#state.jid, Nick), - From, Err), - StateData; - {_, _, false, _} -> - ErrText = "Nickname is registered by another person", - Err = jlib:make_error_reply(Packet, ?ERRT_CONFLICT(Lang, ErrText)), - ejabberd_router:route( - % TODO: s/Nick/""/ - jlib:jid_replace_resource(StateData#state.jid, Nick), - From, Err), - StateData; - {_, _, _, Role} -> - case check_password(Affiliation, Els, StateData) of - true -> - NewState = - add_user_presence( - From, Packet, - add_online_user(From, Nick, Role, StateData)), - if not (NewState#state.config)#config.anonymous -> - WPacket = {xmlelement, "message", [{"type", "groupchat"}], - [{xmlelement, "body", [], - [{xmlcdata, translate:translate( - Lang, - "This room is not anonymous")}]}, - {xmlelement, "x", [{"xmlns", ?NS_MUC_USER}], - [{xmlelement, "status", [{"code", "100"}], []}]}]}, - ejabberd_router:route( - StateData#state.jid, - From, WPacket); - true -> - ok - end, - send_existing_presences(From, NewState), - send_new_presence(From, NewState), - Shift = count_stanza_shift(Nick, Els, NewState), - case send_history(From, Shift, NewState) of - true -> - ok; - _ -> - send_subject(From, Lang, StateData) - end, - case NewState#state.just_created of - true -> - NewState#state{just_created = false}; - false -> - NewState - end; - nopass -> - ErrText = "Password required to enter this room", - Err = jlib:make_error_reply( - Packet, ?ERRT_NOT_AUTHORIZED(Lang, ErrText)), - ejabberd_router:route( % TODO: s/Nick/""/ - jlib:jid_replace_resource( - StateData#state.jid, Nick), - From, Err), - StateData; - _ -> - ErrText = "Incorrect password", - Err = jlib:make_error_reply( - Packet, ?ERRT_NOT_AUTHORIZED(Lang, ErrText)), - ejabberd_router:route( % TODO: s/Nick/""/ - jlib:jid_replace_resource( - StateData#state.jid, Nick), - From, Err), - StateData - end + ServiceAffiliation = get_service_affiliation(From, + StateData), + NConferences = tab_count_user(From), + MaxConferences = + gen_mod:get_module_opt(StateData#state.server_host, + mod_muc, max_user_conferences, + fun(I) when is_integer(I), I>0 -> I end, + 10), + Collision = nick_collision(From, Nick, StateData), + case {(ServiceAffiliation == owner orelse + (Affiliation == admin orelse Affiliation == owner) + andalso NUsers < MaxAdminUsers + orelse NUsers < MaxUsers) + andalso NConferences < MaxConferences, + Collision, + mod_muc:can_use_nick(StateData#state.server_host, + StateData#state.host, From, Nick), + get_default_role(Affiliation, StateData)} + of + {false, _, _, _} -> + Err = jlib:make_error_reply(Packet, + ?ERR_SERVICE_UNAVAILABLE), + ejabberd_router:route % TODO: s/Nick/""/ + (jlib:jid_replace_resource(StateData#state.jid, Nick), + From, Err), + StateData; + {_, _, _, none} -> + Err = jlib:make_error_reply(Packet, + case Affiliation of + outcast -> + ErrText = + <<"You have been banned from this room">>, + ?ERRT_FORBIDDEN(Lang, ErrText); + _ -> + ErrText = + <<"Membership is required to enter this room">>, + ?ERRT_REGISTRATION_REQUIRED(Lang, + ErrText) + end), + ejabberd_router:route % TODO: s/Nick/""/ + (jlib:jid_replace_resource(StateData#state.jid, Nick), + From, Err), + StateData; + {_, true, _, _} -> + ErrText = <<"That nickname is already in use by another occupant">>, + Err = jlib:make_error_reply(Packet, + ?ERRT_CONFLICT(Lang, ErrText)), + ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, + Nick), + From, Err), + StateData; + {_, _, false, _} -> + ErrText = <<"That nickname is registered by another person">>, + Err = jlib:make_error_reply(Packet, + ?ERRT_CONFLICT(Lang, ErrText)), + ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, + Nick), + From, Err), + StateData; + {_, _, _, Role} -> + case check_password(ServiceAffiliation, Affiliation, + Els, From, StateData) + of + true -> + NewState = add_user_presence(From, Packet, + add_online_user(From, Nick, Role, + StateData)), + if not (NewState#state.config)#config.anonymous -> + WPacket = #xmlel{name = <<"message">>, + attrs = [{<<"type">>, <<"groupchat">>}], + children = + [#xmlel{name = <<"body">>, + attrs = [], + children = + [{xmlcdata, + translate:translate(Lang, + <<"This room is not anonymous">>)}]}, + #xmlel{name = <<"x">>, + attrs = + [{<<"xmlns">>, + ?NS_MUC_USER}], + children = + [#xmlel{name = + <<"status">>, + attrs = + [{<<"code">>, + <<"100">>}], + children = + []}]}]}, + ejabberd_router:route(StateData#state.jid, From, WPacket); + true -> ok + end, + send_existing_presences(From, NewState), + send_new_presence(From, NewState), + Shift = count_stanza_shift(Nick, Els, NewState), + case send_history(From, Shift, NewState) of + true -> ok; + _ -> send_subject(From, Lang, StateData) + end, + case NewState#state.just_created of + true -> NewState#state{just_created = false}; + false -> + Robots = (?DICT):erase(From, StateData#state.robots), + NewState#state{robots = Robots} + end; + nopass -> + ErrText = <<"A password is required to enter this room">>, + Err = jlib:make_error_reply(Packet, + ?ERRT_NOT_AUTHORIZED(Lang, + ErrText)), + ejabberd_router:route % TODO: s/Nick/""/ + (jlib:jid_replace_resource(StateData#state.jid, + Nick), + From, Err), + StateData; + captcha_required -> + SID = xml:get_attr_s(<<"id">>, Attrs), + RoomJID = StateData#state.jid, + To = jlib:jid_replace_resource(RoomJID, Nick), + Limiter = {From#jid.luser, From#jid.lserver}, + case ejabberd_captcha:create_captcha(SID, RoomJID, To, + Lang, Limiter, From) + of + {ok, ID, CaptchaEls} -> + MsgPkt = #xmlel{name = <<"message">>, + attrs = [{<<"id">>, ID}], + children = CaptchaEls}, + Robots = (?DICT):store(From, {Nick, Packet}, + StateData#state.robots), + ejabberd_router:route(RoomJID, From, MsgPkt), + StateData#state{robots = Robots}; + {error, limit} -> + ErrText = <<"Too many CAPTCHA requests">>, + Err = jlib:make_error_reply(Packet, + ?ERRT_RESOURCE_CONSTRAINT(Lang, + ErrText)), + ejabberd_router:route % TODO: s/Nick/""/ + (jlib:jid_replace_resource(StateData#state.jid, + Nick), + From, Err), + StateData; + _ -> + ErrText = <<"Unable to generate a CAPTCHA">>, + Err = jlib:make_error_reply(Packet, + ?ERRT_INTERNAL_SERVER_ERROR(Lang, + ErrText)), + ejabberd_router:route % TODO: s/Nick/""/ + (jlib:jid_replace_resource(StateData#state.jid, + Nick), + From, Err), + StateData + end; + _ -> + ErrText = <<"Incorrect password">>, + Err = jlib:make_error_reply(Packet, + ?ERRT_NOT_AUTHORIZED(Lang, + ErrText)), + ejabberd_router:route % TODO: s/Nick/""/ + (jlib:jid_replace_resource(StateData#state.jid, + Nick), + From, Err), + StateData + end end. -check_password(owner, _Els, _StateData) -> +check_password(owner, _Affiliation, _Els, _From, + _StateData) -> + %% Don't check pass if user is owner in MUC service (access_admin option) true; -check_password(_Affiliation, Els, StateData) -> - case (StateData#state.config)#config.password_protected of - false -> - true; - true -> - Pass = extract_password(Els), - case Pass of - false -> - nopass; - _ -> - case (StateData#state.config)#config.password of - Pass -> - true; - _ -> - false - end - end +check_password(_ServiceAffiliation, Affiliation, Els, + From, StateData) -> + case (StateData#state.config)#config.password_protected + of + false -> check_captcha(Affiliation, From, StateData); + true -> + Pass = extract_password(Els), + case Pass of + false -> nopass; + _ -> + case (StateData#state.config)#config.password of + Pass -> true; + _ -> false + end + end end. -extract_password([]) -> - false; -extract_password([{xmlelement, _Name, Attrs, _SubEls} = El | Els]) -> - case xml:get_attr_s("xmlns", Attrs) of - ?NS_MUC -> - case xml:get_subtag(El, "password") of - false -> - false; - SubEl -> - xml:get_tag_cdata(SubEl) - end; - _ -> - extract_password(Els) +check_captcha(Affiliation, From, StateData) -> + case (StateData#state.config)#config.captcha_protected + andalso ejabberd_captcha:is_feature_available() + of + true when Affiliation == none -> + case (?DICT):find(From, StateData#state.robots) of + {ok, passed} -> true; + _ -> + WList = + (StateData#state.config)#config.captcha_whitelist, + #jid{luser = U, lserver = S, lresource = R} = From, + case (?SETS):is_element({U, S, R}, WList) of + true -> true; + false -> + case (?SETS):is_element({U, S, <<"">>}, WList) of + true -> true; + false -> + case (?SETS):is_element({<<"">>, S, <<"">>}, WList) + of + true -> true; + false -> captcha_required + end + end + end + end; + _ -> true + end. + +extract_password([]) -> false; +extract_password([#xmlel{attrs = Attrs} = El | Els]) -> + case xml:get_attr_s(<<"xmlns">>, Attrs) of + ?NS_MUC -> + case xml:get_subtag(El, <<"password">>) of + false -> false; + SubEl -> xml:get_tag_cdata(SubEl) + end; + _ -> extract_password(Els) end; -extract_password([_ | Els]) -> - extract_password(Els). +extract_password([_ | Els]) -> extract_password(Els). count_stanza_shift(Nick, Els, StateData) -> HL = lqueue_to_list(StateData#state.history), - Since = extract_history(Els, "since"), + Since = extract_history(Els, <<"since">>), Shift0 = case Since of - false -> - 0; - _ -> - Sin = calendar:datetime_to_gregorian_seconds(Since), - count_seconds_shift(Sin, HL) + false -> 0; + _ -> + Sin = calendar:datetime_to_gregorian_seconds(Since), + count_seconds_shift(Sin, HL) end, - Seconds = extract_history(Els, "seconds"), + Seconds = extract_history(Els, <<"seconds">>), Shift1 = case Seconds of - false -> - 0; - _ -> - Sec = calendar:datetime_to_gregorian_seconds( - calendar:now_to_universal_time(now())) - Seconds, - count_seconds_shift(Sec, HL) + false -> 0; + _ -> + Sec = + calendar:datetime_to_gregorian_seconds(calendar:now_to_universal_time(now())) + - Seconds, + count_seconds_shift(Sec, HL) end, - MaxStanzas = extract_history(Els, "maxstanzas"), + MaxStanzas = extract_history(Els, <<"maxstanzas">>), Shift2 = case MaxStanzas of - false -> - 0; - _ -> - count_maxstanzas_shift(MaxStanzas, HL) + false -> 0; + _ -> count_maxstanzas_shift(MaxStanzas, HL) end, - MaxChars = extract_history(Els, "maxchars"), + MaxChars = extract_history(Els, <<"maxchars">>), Shift3 = case MaxChars of - false -> - 0; - _ -> - count_maxchars_shift(Nick, MaxChars, HL) + false -> 0; + _ -> count_maxchars_shift(Nick, MaxChars, HL) end, lists:max([Shift0, Shift1, Shift2, Shift3]). count_seconds_shift(Seconds, HistoryList) -> - lists:sum( - lists:map( - fun({_Nick, _Packet, _HaveSubject, TimeStamp, _Size}) -> - T = calendar:datetime_to_gregorian_seconds(TimeStamp), - if - T < Seconds -> - 1; - true -> - 0 - end - end, HistoryList)). + lists:sum(lists:map(fun ({_Nick, _Packet, _HaveSubject, + TimeStamp, _Size}) -> + T = + calendar:datetime_to_gregorian_seconds(TimeStamp), + if T < Seconds -> 1; + true -> 0 + end + end, + HistoryList)). count_maxstanzas_shift(MaxStanzas, HistoryList) -> S = length(HistoryList) - MaxStanzas, - if - S =< 0 -> - 0; - true -> - S + if S =< 0 -> 0; + true -> S end. count_maxchars_shift(Nick, MaxSize, HistoryList) -> - NLen = string:len(Nick) + 1, - Sizes = lists:map( - fun({_Nick, _Packet, _HaveSubject, _TimeStamp, Size}) -> - Size + NLen - end, HistoryList), + NLen = byte_size(Nick) + 1, + Sizes = lists:map(fun ({_Nick, _Packet, _HaveSubject, + _TimeStamp, Size}) -> + Size + NLen + end, + HistoryList), calc_shift(MaxSize, Sizes). calc_shift(MaxSize, Sizes) -> Total = lists:sum(Sizes), calc_shift(MaxSize, Total, 0, Sizes). -calc_shift(_MaxSize, _Size, Shift, []) -> - Shift; +calc_shift(_MaxSize, _Size, Shift, []) -> Shift; calc_shift(MaxSize, Size, Shift, [S | TSizes]) -> - if - MaxSize >= Size -> - Shift; - true -> - calc_shift(MaxSize, Size - S, Shift + 1, TSizes) + if MaxSize >= Size -> Shift; + true -> calc_shift(MaxSize, Size - S, Shift + 1, TSizes) end. -extract_history([], _Type) -> - false; -extract_history([{xmlelement, _Name, Attrs, _SubEls} = El | Els], Type) -> - case xml:get_attr_s("xmlns", Attrs) of - ?NS_MUC -> - AttrVal = xml:get_path_s(El, - [{elem, "history"}, {attr, Type}]), - case Type of - "since" -> - case jlib:datetime_string_to_timestamp(AttrVal) of - undefined -> - false; - TS -> - calendar:now_to_universal_time(TS) - end; - _ -> - case catch list_to_integer(AttrVal) of - IntVal when is_integer(IntVal) and (IntVal >= 0) -> - IntVal; - _ -> - false - end - end; - _ -> - extract_history(Els, Type) +extract_history([], _Type) -> false; +extract_history([#xmlel{attrs = Attrs} = El | Els], + Type) -> + case xml:get_attr_s(<<"xmlns">>, Attrs) of + ?NS_MUC -> + AttrVal = xml:get_path_s(El, + [{elem, <<"history">>}, {attr, Type}]), + case Type of + <<"since">> -> + case jlib:datetime_string_to_timestamp(AttrVal) of + undefined -> false; + TS -> calendar:now_to_universal_time(TS) + end; + _ -> + case catch jlib:binary_to_integer(AttrVal) of + IntVal when is_integer(IntVal) and (IntVal >= 0) -> + IntVal; + _ -> false + end + end; + _ -> extract_history(Els, Type) end; extract_history([_ | Els], Type) -> extract_history(Els, Type). - send_update_presence(JID, StateData) -> + send_update_presence(JID, <<"">>, StateData). + +send_update_presence(JID, Reason, StateData) -> LJID = jlib:jid_tolower(JID), LJIDs = case LJID of - {U, S, ""} -> - ?DICT:fold( - fun(J, _, Js) -> - case J of - {U, S, _} -> - [J | Js]; - _ -> - Js - end - end, [], StateData#state.users); - _ -> - case ?DICT:is_key(LJID, StateData#state.users) of - true -> - [LJID]; - _ -> - [] - end + {U, S, <<"">>} -> + (?DICT):fold(fun (J, _, Js) -> + case J of + {U, S, _} -> [J | Js]; + _ -> Js + end + end, + [], StateData#state.users); + _ -> + case (?DICT):is_key(LJID, StateData#state.users) of + true -> [LJID]; + _ -> [] + end end, - lists:foreach(fun(J) -> - send_new_presence(J, StateData) - end, LJIDs). + lists:foreach(fun (J) -> + send_new_presence(J, Reason, StateData) + end, + LJIDs). send_new_presence(NJID, StateData) -> - {ok, #user{jid = RealJID, - nick = Nick, - role = Role, - last_presence = Presence}} = - ?DICT:find(jlib:jid_tolower(NJID), StateData#state.users), - Affiliation = get_affiliation(NJID, StateData), + send_new_presence(NJID, <<"">>, StateData). + +send_new_presence(NJID, Reason, StateData) -> + #user{nick = Nick} = + (?DICT):fetch(jlib:jid_tolower(NJID), + StateData#state.users), + LJID = find_jid_by_nick(Nick, StateData), + {ok, + #user{jid = RealJID, role = Role, + last_presence = Presence}} = + (?DICT):find(jlib:jid_tolower(LJID), + StateData#state.users), + Affiliation = get_affiliation(LJID, StateData), SAffiliation = affiliation_to_list(Affiliation), SRole = role_to_list(Role), - lists:foreach( - fun({_LJID, Info}) -> - ItemAttrs = - case (Info#user.role == moderator) orelse - ((StateData#state.config)#config.anonymous == false) of - true -> - [{"jid", jlib:jid_to_string(RealJID)}, - {"affiliation", SAffiliation}, - {"role", SRole}]; - _ -> - [{"affiliation", SAffiliation}, - {"role", SRole}] + lists:foreach(fun ({_LJID, Info}) -> + ItemAttrs = case Info#user.role == moderator orelse + (StateData#state.config)#config.anonymous + == false + of + true -> + [{<<"jid">>, + jlib:jid_to_string(RealJID)}, + {<<"affiliation">>, SAffiliation}, + {<<"role">>, SRole}]; + _ -> + [{<<"affiliation">>, SAffiliation}, + {<<"role">>, SRole}] + end, + ItemEls = case Reason of + <<"">> -> []; + _ -> + [#xmlel{name = <<"reason">>, + attrs = [], + children = + [{xmlcdata, Reason}]}] + end, + Status = case StateData#state.just_created of + true -> + [#xmlel{name = <<"status">>, + attrs = + [{<<"code">>, <<"201">>}], + children = []}]; + false -> [] + end, + Status2 = case + (StateData#state.config)#config.anonymous + == false + andalso NJID == Info#user.jid + of + true -> + [#xmlel{name = <<"status">>, + attrs = + [{<<"code">>, <<"100">>}], + children = []} + | Status]; + false -> Status + end, + Status3 = case NJID == Info#user.jid of + true -> + [#xmlel{name = <<"status">>, + attrs = + [{<<"code">>, <<"110">>}], + children = []} + | Status2]; + false -> Status2 + end, + Packet = xml:append_subtags(Presence, + [#xmlel{name = <<"x">>, + attrs = + [{<<"xmlns">>, + ?NS_MUC_USER}], + children = + [#xmlel{name = + <<"item">>, + attrs + = + ItemAttrs, + children + = + ItemEls} + | Status3]}]), + ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, + Nick), + Info#user.jid, Packet) end, - Status = case StateData#state.just_created of - true -> - [{xmlelement, "status", [{"code", "201"}], []}]; - false -> - [] - end, - Packet = append_subtags( - Presence, - [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}], - [{xmlelement, "item", ItemAttrs, []} | Status]}]), - ejabberd_router:route( - jlib:jid_replace_resource(StateData#state.jid, Nick), - Info#user.jid, - Packet) - end, ?DICT:to_list(StateData#state.users)). - + (?DICT):to_list(StateData#state.users)). send_existing_presences(ToJID, StateData) -> LToJID = jlib:jid_tolower(ToJID), - {ok, #user{jid = RealToJID, - role = Role}} = - ?DICT:find(LToJID, StateData#state.users), - lists:foreach( - fun({LJID, #user{jid = FromJID, - nick = FromNick, - role = FromRole, - last_presence = Presence - }}) -> - case RealToJID of - FromJID -> - ok; - _ -> - FromAffiliation = get_affiliation(LJID, StateData), - ItemAttrs = - case (Role == moderator) orelse - ((StateData#state.config)#config.anonymous == - false) of - true -> - [{"jid", jlib:jid_to_string(FromJID)}, - {"affiliation", - affiliation_to_list(FromAffiliation)}, - {"role", role_to_list(FromRole)}]; - _ -> - [{"affiliation", - affiliation_to_list(FromAffiliation)}, - {"role", role_to_list(FromRole)}] - end, - Packet = append_subtags( - Presence, - [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}], - [{xmlelement, "item", ItemAttrs, []}]}]), - ejabberd_router:route( - jlib:jid_replace_resource( - StateData#state.jid, FromNick), - RealToJID, - Packet) - end - end, ?DICT:to_list(StateData#state.users)). - - -append_subtags({xmlelement, Name, Attrs, SubTags1}, SubTags2) -> - {xmlelement, Name, Attrs, SubTags1 ++ SubTags2}. - + {ok, #user{jid = RealToJID, role = Role}} = + (?DICT):find(LToJID, StateData#state.users), + lists:foreach(fun ({FromNick, _Users}) -> + LJID = find_jid_by_nick(FromNick, StateData), + #user{jid = FromJID, role = FromRole, + last_presence = Presence} = + (?DICT):fetch(jlib:jid_tolower(LJID), + StateData#state.users), + case RealToJID of + FromJID -> ok; + _ -> + FromAffiliation = get_affiliation(LJID, + StateData), + ItemAttrs = case Role == moderator orelse + (StateData#state.config)#config.anonymous + == false + of + true -> + [{<<"jid">>, + jlib:jid_to_string(FromJID)}, + {<<"affiliation">>, + affiliation_to_list(FromAffiliation)}, + {<<"role">>, + role_to_list(FromRole)}]; + _ -> + [{<<"affiliation">>, + affiliation_to_list(FromAffiliation)}, + {<<"role">>, + role_to_list(FromRole)}] + end, + Packet = xml:append_subtags(Presence, + [#xmlel{name = + <<"x">>, + attrs = + [{<<"xmlns">>, + ?NS_MUC_USER}], + children = + [#xmlel{name + = + <<"item">>, + attrs + = + ItemAttrs, + children + = + []}]}]), + ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, + FromNick), + RealToJID, Packet) + end + end, + (?DICT):to_list(StateData#state.nicks)). now_to_usec({MSec, Sec, USec}) -> - (MSec*1000000 + Sec)*1000000 + USec. - + (MSec * 1000000 + Sec) * 1000000 + USec. change_nick(JID, Nick, StateData) -> LJID = jlib:jid_tolower(JID), - {ok, #user{nick = OldNick}} = - ?DICT:find(LJID, StateData#state.users), - Users = - ?DICT:update( - LJID, - fun(#user{} = User) -> - User#user{nick = Nick} - end, StateData#state.users), - NewStateData = StateData#state{users = Users}, - send_nick_changing(JID, OldNick, NewStateData), + {ok, #user{nick = OldNick}} = (?DICT):find(LJID, + StateData#state.users), + Users = (?DICT):update(LJID, + fun (#user{} = User) -> User#user{nick = Nick} end, + StateData#state.users), + OldNickUsers = (?DICT):fetch(OldNick, + StateData#state.nicks), + NewNickUsers = case (?DICT):find(Nick, + StateData#state.nicks) + of + {ok, U} -> U; + error -> [] + end, + SendOldUnavailable = length(OldNickUsers) == 1, + SendNewAvailable = SendOldUnavailable orelse + NewNickUsers == [], + Nicks = case OldNickUsers of + [LJID] -> + (?DICT):store(Nick, [LJID | NewNickUsers], + (?DICT):erase(OldNick, StateData#state.nicks)); + [_ | _] -> + (?DICT):store(Nick, [LJID | NewNickUsers], + (?DICT):store(OldNick, OldNickUsers -- [LJID], + StateData#state.nicks)) + end, + NewStateData = StateData#state{users = Users, + nicks = Nicks}, + send_nick_changing(JID, OldNick, NewStateData, + SendOldUnavailable, SendNewAvailable), add_to_log(nickchange, {OldNick, Nick}, StateData), NewStateData. -send_nick_changing(JID, OldNick, StateData) -> - {ok, #user{jid = RealJID, - nick = Nick, - role = Role, - last_presence = Presence}} = - ?DICT:find(jlib:jid_tolower(JID), StateData#state.users), +send_nick_changing(JID, OldNick, StateData, + SendOldUnavailable, SendNewAvailable) -> + {ok, + #user{jid = RealJID, nick = Nick, role = Role, + last_presence = Presence}} = + (?DICT):find(jlib:jid_tolower(JID), + StateData#state.users), Affiliation = get_affiliation(JID, StateData), SAffiliation = affiliation_to_list(Affiliation), SRole = role_to_list(Role), - lists:foreach( - fun({_LJID, Info}) -> - ItemAttrs1 = - case (Info#user.role == moderator) orelse - ((StateData#state.config)#config.anonymous == false) of - true -> - [{"jid", jlib:jid_to_string(RealJID)}, - {"affiliation", SAffiliation}, - {"role", SRole}, - {"nick", Nick}]; - _ -> - [{"affiliation", SAffiliation}, - {"role", SRole}, - {"nick", Nick}] + lists:foreach(fun ({_LJID, Info}) -> + ItemAttrs1 = case Info#user.role == moderator orelse + (StateData#state.config)#config.anonymous + == false + of + true -> + [{<<"jid">>, + jlib:jid_to_string(RealJID)}, + {<<"affiliation">>, SAffiliation}, + {<<"role">>, SRole}, + {<<"nick">>, Nick}]; + _ -> + [{<<"affiliation">>, SAffiliation}, + {<<"role">>, SRole}, + {<<"nick">>, Nick}] + end, + ItemAttrs2 = case Info#user.role == moderator orelse + (StateData#state.config)#config.anonymous + == false + of + true -> + [{<<"jid">>, + jlib:jid_to_string(RealJID)}, + {<<"affiliation">>, SAffiliation}, + {<<"role">>, SRole}]; + _ -> + [{<<"affiliation">>, SAffiliation}, + {<<"role">>, SRole}] + end, + Status110 = case JID == Info#user.jid of + true -> + [#xmlel{name = <<"status">>, + attrs = [{<<"code">>, <<"110">>}] + }]; + false -> + [] + end, + Packet1 = #xmlel{name = <<"presence">>, + attrs = + [{<<"type">>, + <<"unavailable">>}], + children = + [#xmlel{name = <<"x">>, + attrs = + [{<<"xmlns">>, + ?NS_MUC_USER}], + children = + [#xmlel{name = + <<"item">>, + attrs = + ItemAttrs1, + children = + []}, + #xmlel{name = + <<"status">>, + attrs = + [{<<"code">>, + <<"303">>}], + children = + []}|Status110]}]}, + Packet2 = xml:append_subtags(Presence, + [#xmlel{name = <<"x">>, + attrs = + [{<<"xmlns">>, + ?NS_MUC_USER}], + children = + [#xmlel{name + = + <<"item">>, + attrs + = + ItemAttrs2, + children + = + []}|Status110]}]), + if SendOldUnavailable -> + ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, + OldNick), + Info#user.jid, Packet1); + true -> ok + end, + if SendNewAvailable -> + ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, + Nick), + Info#user.jid, Packet2); + true -> ok + end end, - ItemAttrs2 = - case (Info#user.role == moderator) orelse - ((StateData#state.config)#config.anonymous == false) of - true -> - [{"jid", jlib:jid_to_string(RealJID)}, - {"affiliation", SAffiliation}, - {"role", SRole}]; - _ -> - [{"affiliation", SAffiliation}, - {"role", SRole}] - end, - Packet1 = - {xmlelement, "presence", [{"type", "unavailable"}], - [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}], - [{xmlelement, "item", ItemAttrs1, []}, - {xmlelement, "status", [{"code", "303"}], []}]}]}, - Packet2 = append_subtags( - Presence, - [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}], - [{xmlelement, "item", ItemAttrs2, []}]}]), - ejabberd_router:route( - jlib:jid_replace_resource(StateData#state.jid, OldNick), - Info#user.jid, - Packet1), - ejabberd_router:route( - jlib:jid_replace_resource(StateData#state.jid, Nick), - Info#user.jid, - Packet2) - end, ?DICT:to_list(StateData#state.users)). - + (?DICT):to_list(StateData#state.users)). lqueue_new(Max) -> - #lqueue{queue = queue:new(), - len = 0, - max = Max}. + #lqueue{queue = queue:new(), len = 0, max = Max}. %% If the message queue limit is set to 0, do not store messages. -lqueue_in(_Item, LQ = #lqueue{max = 0}) -> - LQ; +lqueue_in(_Item, LQ = #lqueue{max = 0}) -> LQ; %% Otherwise, rotate messages in the queue store. -lqueue_in(Item, #lqueue{queue = Q1, len = Len, max = Max}) -> +lqueue_in(Item, + #lqueue{queue = Q1, len = Len, max = Max}) -> Q2 = queue:in(Item, Q1), - if - Len >= Max -> - Q3 = lqueue_cut(Q2, Len - Max + 1), - #lqueue{queue = Q3, len = Max, max = Max}; - true -> - #lqueue{queue = Q2, len = Len + 1, max = Max} + if Len >= Max -> + Q3 = lqueue_cut(Q2, Len - Max + 1), + #lqueue{queue = Q3, len = Max, max = Max}; + true -> #lqueue{queue = Q2, len = Len + 1, max = Max} end. -lqueue_cut(Q, 0) -> - Q; +lqueue_cut(Q, 0) -> Q; lqueue_cut(Q, N) -> - {_, Q1} = queue:out(Q), - lqueue_cut(Q1, N - 1). + {_, Q1} = queue:out(Q), lqueue_cut(Q1, N - 1). lqueue_to_list(#lqueue{queue = Q1}) -> queue:to_list(Q1). -add_message_to_history(FromNick, Packet, StateData) -> - HaveSubject = case xml:get_subtag(Packet, "subject") of - false -> - false; - _ -> - true +add_message_to_history(FromNick, FromJID, Packet, StateData) -> + HaveSubject = case xml:get_subtag(Packet, <<"subject">>) + of + false -> false; + _ -> true end, TimeStamp = calendar:now_to_universal_time(now()), - TSPacket = append_subtags(Packet, - [jlib:timestamp_to_xml(TimeStamp)]), - SPacket = jlib:replace_from_to( - jlib:jid_replace_resource(StateData#state.jid, FromNick), - StateData#state.jid, - TSPacket), - Size = lists:flatlength(xml:element_to_string(SPacket)), - Q1 = lqueue_in({FromNick, TSPacket, HaveSubject, TimeStamp, Size}, + SenderJid = case + (StateData#state.config)#config.anonymous + of + true -> StateData#state.jid; + false -> FromJID + end, + TSPacket = xml:append_subtags(Packet, + [jlib:timestamp_to_xml(TimeStamp, utc, + SenderJid, <<"">>), + jlib:timestamp_to_xml(TimeStamp)]), + SPacket = + jlib:replace_from_to(jlib:jid_replace_resource(StateData#state.jid, + FromNick), + StateData#state.jid, TSPacket), + Size = element_size(SPacket), + Q1 = lqueue_in({FromNick, TSPacket, HaveSubject, + TimeStamp, Size}, StateData#state.history), add_to_log(text, {FromNick, Packet}, StateData), StateData#state{history = Q1}. send_history(JID, Shift, StateData) -> - lists:foldl( - fun({Nick, Packet, HaveSubject, _TimeStamp, _Size}, B) -> - ejabberd_router:route( - jlib:jid_replace_resource(StateData#state.jid, Nick), - JID, - Packet), - B or HaveSubject - end, false, lists:nthtail(Shift, lqueue_to_list(StateData#state.history))). - + lists:foldl(fun ({Nick, Packet, HaveSubject, _TimeStamp, + _Size}, + B) -> + ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, + Nick), + JID, Packet), + B or HaveSubject + end, + false, + lists:nthtail(Shift, + lqueue_to_list(StateData#state.history))). send_subject(JID, Lang, StateData) -> case StateData#state.subject_author of - "" -> - ok; - Nick -> - Subject = StateData#state.subject, - Packet = {xmlelement, "message", [{"type", "groupchat"}], - [{xmlelement, "subject", [], [{xmlcdata, Subject}]}, - {xmlelement, "body", [], - [{xmlcdata, - Nick ++ - translate:translate(Lang, - " has set the subject to: ") ++ - Subject}]}]}, - ejabberd_router:route( - StateData#state.jid, - JID, - Packet) + <<"">> -> ok; + Nick -> + Subject = StateData#state.subject, + Packet = #xmlel{name = <<"message">>, + attrs = [{<<"type">>, <<"groupchat">>}], + children = + [#xmlel{name = <<"subject">>, attrs = [], + children = [{xmlcdata, Subject}]}, + #xmlel{name = <<"body">>, attrs = [], + children = + [{xmlcdata, + <>))/binary, + Subject/binary>>}]}]}, + ejabberd_router:route(StateData#state.jid, JID, Packet) end. check_subject(Packet) -> - case xml:get_subtag(Packet, "subject") of - false -> - false; - SubjEl -> - xml:get_tag_cdata(SubjEl) + case xml:get_subtag(Packet, <<"subject">>) of + false -> false; + SubjEl -> xml:get_tag_cdata(SubjEl) end. can_change_subject(Role, StateData) -> - case (StateData#state.config)#config.allow_change_subj of - true -> - (Role == moderator) orelse (Role == participant); - _ -> - Role == moderator + case (StateData#state.config)#config.allow_change_subj + of + true -> Role == moderator orelse Role == participant; + _ -> Role == moderator end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Admin stuff process_iq_admin(From, set, Lang, SubEl, StateData) -> - {xmlelement, _, _, Items} = SubEl, + #xmlel{children = Items} = SubEl, process_admin_items_set(From, Items, Lang, StateData); - process_iq_admin(From, get, Lang, SubEl, StateData) -> - case xml:get_subtag(SubEl, "item") of - false -> - {error, ?ERR_BAD_REQUEST}; - Item -> - FAffiliation = get_affiliation(From, StateData), - FRole = get_role(From, StateData), - case xml:get_tag_attr("role", Item) of - false -> - case xml:get_tag_attr("affiliation", Item) of - false -> - {error, ?ERR_BAD_REQUEST}; - {value, StrAffiliation} -> - case catch list_to_affiliation(StrAffiliation) of - {'EXIT', _} -> - {error, ?ERR_BAD_REQUEST}; - SAffiliation -> - if - (FAffiliation == owner) or - (FAffiliation == admin) -> - Items = items_with_affiliation( - SAffiliation, StateData), - {result, Items, StateData}; - true -> - ErrText = "Administrator privileges required", - {error, ?ERRT_FORBIDDEN(Lang, ErrText)} - end + case xml:get_subtag(SubEl, <<"item">>) of + false -> {error, ?ERR_BAD_REQUEST}; + Item -> + FAffiliation = get_affiliation(From, StateData), + FRole = get_role(From, StateData), + case xml:get_tag_attr(<<"role">>, Item) of + false -> + case xml:get_tag_attr(<<"affiliation">>, Item) of + false -> {error, ?ERR_BAD_REQUEST}; + {value, StrAffiliation} -> + case catch list_to_affiliation(StrAffiliation) of + {'EXIT', _} -> {error, ?ERR_BAD_REQUEST}; + SAffiliation -> + if (FAffiliation == owner) or + (FAffiliation == admin) or + ((FAffiliation == member) and (SAffiliation == member)) -> + Items = items_with_affiliation(SAffiliation, + StateData), + {result, Items, StateData}; + true -> + ErrText = + <<"Administrator privileges required">>, + {error, ?ERRT_FORBIDDEN(Lang, ErrText)} end - end; - {value, StrRole} -> - case catch list_to_role(StrRole) of - {'EXIT', _} -> - {error, ?ERR_BAD_REQUEST}; - SRole -> - if - FRole == moderator -> - Items = items_with_role(SRole, StateData), - {result, Items, StateData}; - true -> - ErrText = "Moderator privileges required", - {error, ?ERRT_FORBIDDEN(Lang, ErrText)} - end - end - end + end + end; + {value, StrRole} -> + case catch list_to_role(StrRole) of + {'EXIT', _} -> {error, ?ERR_BAD_REQUEST}; + SRole -> + if FRole == moderator -> + Items = items_with_role(SRole, StateData), + {result, Items, StateData}; + true -> + ErrText = <<"Moderator privileges required">>, + {error, ?ERRT_FORBIDDEN(Lang, ErrText)} + end + end + end end. - items_with_role(SRole, StateData) -> - lists:map( - fun({_, U}) -> - user_to_item(U, StateData) - end, search_role(SRole, StateData)). + lists:map(fun ({_, U}) -> user_to_item(U, StateData) + end, + search_role(SRole, StateData)). items_with_affiliation(SAffiliation, StateData) -> - lists:map( - fun({JID, {Affiliation, Reason}}) -> - {xmlelement, "item", - [{"affiliation", affiliation_to_list(Affiliation)}, - {"jid", jlib:jid_to_string(JID)}], - [{xmlelement, "reason", [], [{xmlcdata, Reason}]}]}; - ({JID, Affiliation}) -> - {xmlelement, "item", - [{"affiliation", affiliation_to_list(Affiliation)}, - {"jid", jlib:jid_to_string(JID)}], - []} - end, search_affiliation(SAffiliation, StateData)). + lists:map(fun ({JID, {Affiliation, Reason}}) -> + #xmlel{name = <<"item">>, + attrs = + [{<<"affiliation">>, + affiliation_to_list(Affiliation)}, + {<<"jid">>, jlib:jid_to_string(JID)}], + children = + [#xmlel{name = <<"reason">>, attrs = [], + children = [{xmlcdata, Reason}]}]}; + ({JID, Affiliation}) -> + #xmlel{name = <<"item">>, + attrs = + [{<<"affiliation">>, + affiliation_to_list(Affiliation)}, + {<<"jid">>, jlib:jid_to_string(JID)}], + children = []} + end, + search_affiliation(SAffiliation, StateData)). -user_to_item(#user{role = Role, - nick = Nick, - jid = JID - }, StateData) -> +user_to_item(#user{role = Role, nick = Nick, jid = JID}, + StateData) -> Affiliation = get_affiliation(JID, StateData), - {xmlelement, "item", - [{"role", role_to_list(Role)}, - {"affiliation", affiliation_to_list(Affiliation)}, - {"nick", Nick}, - {"jid", jlib:jid_to_string(JID)}], - []}. + #xmlel{name = <<"item">>, + attrs = + [{<<"role">>, role_to_list(Role)}, + {<<"affiliation">>, affiliation_to_list(Affiliation)}, + {<<"nick">>, Nick}, + {<<"jid">>, jlib:jid_to_string(JID)}], + children = []}. search_role(Role, StateData) -> - lists:filter( - fun({_, #user{role = R}}) -> - Role == R - end, ?DICT:to_list(StateData#state.users)). + lists:filter(fun ({_, #user{role = R}}) -> Role == R + end, + (?DICT):to_list(StateData#state.users)). search_affiliation(Affiliation, StateData) -> - lists:filter( - fun({_, A}) -> - case A of - {A1, _Reason} -> - Affiliation == A1; - _ -> - Affiliation == A - end - end, ?DICT:to_list(StateData#state.affiliations)). - + lists:filter(fun ({_, A}) -> + case A of + {A1, _Reason} -> Affiliation == A1; + _ -> Affiliation == A + end + end, + (?DICT):to_list(StateData#state.affiliations)). process_admin_items_set(UJID, Items, Lang, StateData) -> UAffiliation = get_affiliation(UJID, StateData), URole = get_role(UJID, StateData), - case find_changed_items(UJID, UAffiliation, URole, Items, Lang, StateData, []) of - {result, Res} -> - ?INFO_MSG("Processing MUC admin query from ~s:~n ~p~n", - [jlib:jid_to_string(UJID), Res]), - NSD = - lists:foldl( - fun(E, SD) -> - case catch ( - case E of - {JID, role, none, Reason} -> - catch send_kickban_presence( - JID, Reason, "307", SD), - set_role(JID, none, SD); - {JID, affiliation, none, Reason} -> - case (SD#state.config)#config.members_only of - true -> - catch send_kickban_presence( - JID, Reason, "321", SD), - SD1 = set_affiliation(JID, none, SD), - set_role(JID, none, SD1); - _ -> - SD1 = set_affiliation(JID, none, SD), - send_update_presence(JID, SD1), - SD1 - end; - {JID, affiliation, outcast, Reason} -> - catch send_kickban_presence( - JID, Reason, "301", SD), - set_affiliation_and_reason( - JID, outcast, Reason, - set_role(JID, none, SD)); - {JID, affiliation, A, _Reason} when - (A == admin) or (A == owner) -> - SD1 = set_affiliation(JID, A, SD), - SD2 = set_role(JID, moderator, SD1), - send_update_presence(JID, SD2), - SD2; - {JID, affiliation, member, _Reason} -> - SD1 = set_affiliation( - JID, member, SD), - SD2 = set_role(JID, participant, SD1), - send_update_presence(JID, SD2), - SD2; - {JID, role, R, _Reason} -> - SD1 = set_role(JID, R, SD), - catch send_new_presence(JID, SD1), - SD1; - {JID, affiliation, A, _Reason} -> - SD1 = set_affiliation(JID, A, SD), - send_update_presence(JID, SD1), - SD1 - end - ) of - {'EXIT', ErrReason} -> - ?ERROR_MSG("MUC ITEMS SET ERR: ~p~n", - [ErrReason]), - SD; - NSD -> - NSD - end - end, StateData, Res), - case (NSD#state.config)#config.persistent of - true -> - mod_muc:store_room(NSD#state.host, NSD#state.room, - make_opts(NSD)); - _ -> - ok - end, - {result, [], NSD}; - Err -> - Err + case find_changed_items(UJID, UAffiliation, URole, + Items, Lang, StateData, []) + of + {result, Res} -> + ?INFO_MSG("Processing MUC admin query from ~s in " + "room ~s:~n ~p", + [jlib:jid_to_string(UJID), + jlib:jid_to_string(StateData#state.jid), Res]), + NSD = lists:foldl(fun (E, SD) -> + case catch case E of + {JID, affiliation, owner, _} + when JID#jid.luser == + <<"">> -> + %% If the provided JID does not have username, + %% forget the affiliation completely + SD; + {JID, role, none, Reason} -> + catch + send_kickban_presence(UJID, JID, + Reason, + <<"307">>, + SD), + set_role(JID, none, SD); + {JID, affiliation, none, + Reason} -> + case + (SD#state.config)#config.members_only + of + true -> + catch + send_kickban_presence(UJID, JID, + Reason, + <<"321">>, + none, + SD), + SD1 = + set_affiliation(JID, + none, + SD), + set_role(JID, none, + SD1); + _ -> + SD1 = + set_affiliation(JID, + none, + SD), + send_update_presence(JID, + SD1), + SD1 + end; + {JID, affiliation, outcast, + Reason} -> + catch + send_kickban_presence(UJID, JID, + Reason, + <<"301">>, + outcast, + SD), + set_affiliation(JID, + outcast, + set_role(JID, + none, + SD), + Reason); + {JID, affiliation, A, Reason} + when (A == admin) or + (A == owner) -> + SD1 = set_affiliation(JID, + A, + SD, + Reason), + SD2 = set_role(JID, + moderator, + SD1), + send_update_presence(JID, + Reason, + SD2), + SD2; + {JID, affiliation, member, + Reason} -> + SD1 = set_affiliation(JID, + member, + SD, + Reason), + SD2 = set_role(JID, + participant, + SD1), + send_update_presence(JID, + Reason, + SD2), + SD2; + {JID, role, Role, Reason} -> + SD1 = set_role(JID, Role, + SD), + catch + send_new_presence(JID, + Reason, + SD1), + SD1; + {JID, affiliation, A, + _Reason} -> + SD1 = set_affiliation(JID, + A, + SD), + send_update_presence(JID, + SD1), + SD1 + end + of + {'EXIT', ErrReason} -> + ?ERROR_MSG("MUC ITEMS SET ERR: ~p~n", + [ErrReason]), + SD; + NSD -> NSD + end + end, + StateData, lists:flatten(Res)), + case (NSD#state.config)#config.persistent of + true -> + mod_muc:store_room(NSD#state.server_host, + NSD#state.host, NSD#state.room, + make_opts(NSD)); + _ -> ok + end, + {result, [], NSD}; + Err -> Err end. - -find_changed_items(_UJID, _UAffiliation, _URole, [], _Lang, _StateData, Res) -> +find_changed_items(_UJID, _UAffiliation, _URole, [], + _Lang, _StateData, Res) -> {result, Res}; -find_changed_items(UJID, UAffiliation, URole, [{xmlcdata, _} | Items], - Lang, StateData, Res) -> - find_changed_items(UJID, UAffiliation, URole, Items, Lang, StateData, Res); find_changed_items(UJID, UAffiliation, URole, - [{xmlelement, "item", Attrs, _Els} = Item | Items], + [{xmlcdata, _} | Items], Lang, StateData, Res) -> + find_changed_items(UJID, UAffiliation, URole, Items, + Lang, StateData, Res); +find_changed_items(UJID, UAffiliation, URole, + [#xmlel{name = <<"item">>, attrs = Attrs} = Item + | Items], Lang, StateData, Res) -> - TJID = case xml:get_attr("jid", Attrs) of - {value, S} -> - case jlib:string_to_jid(S) of - error -> - ErrText = io_lib:format( - translate:translate( - Lang, - "JID ~s is invalid"), [S]), - {error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)}; - J -> - {value, J} - end; - _ -> - case xml:get_attr("nick", Attrs) of - {value, N} -> - case find_jid_by_nick(N, StateData) of - false -> - ErrText = - io_lib:format( - translate:translate( - Lang, - "Nickname ~s does not exist in the room"), - [N]), - {error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)}; - J -> - {value, J} - end; - _ -> - {error, ?ERR_BAD_REQUEST} - end + TJID = case xml:get_attr(<<"jid">>, Attrs) of + {value, S} -> + case jlib:string_to_jid(S) of + error -> + ErrText = iolist_to_binary( + io_lib:format(translate:translate( + Lang, + <<"Jabber ID ~s is invalid">>), + [S])), + {error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)}; + J -> {value, [J]} + end; + _ -> + case xml:get_attr(<<"nick">>, Attrs) of + {value, N} -> + case find_jids_by_nick(N, StateData) of + false -> + ErrText = iolist_to_binary( + io_lib:format( + translate:translate( + Lang, + <<"Nickname ~s does not exist in the room">>), + [N])), + {error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)}; + J -> {value, J} + end; + _ -> {error, ?ERR_BAD_REQUEST} + end end, case TJID of - {value, JID} -> - TAffiliation = get_affiliation(JID, StateData), - TRole = get_role(JID, StateData), - case xml:get_attr("role", Attrs) of - false -> - case xml:get_attr("affiliation", Attrs) of - false -> - {error, ?ERR_BAD_REQUEST}; - {value, StrAffiliation} -> - case catch list_to_affiliation(StrAffiliation) of - {'EXIT', _} -> - ErrText1 = - io_lib:format( - translate:translate( - Lang, - "Invalid affiliation: ~s"), - [StrAffiliation]), - {error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText1)}; - SAffiliation -> - CanChangeRA = - case can_change_ra( - UAffiliation, URole, - TAffiliation, TRole, - affiliation, SAffiliation) of - nothing -> - nothing; - true -> - true; - check_owner -> - case search_affiliation( - owner, StateData) of - [{OJID, _}] -> - jlib:jid_remove_resource(OJID) /= - jlib:jid_tolower(jlib:jid_remove_resource(UJID)); - _ -> - true - end; - _ -> - false - end, - case CanChangeRA of - nothing -> - find_changed_items( - UJID, - UAffiliation, URole, - Items, Lang, StateData, - Res); - true -> - find_changed_items( - UJID, - UAffiliation, URole, - Items, Lang, StateData, - [{jlib:jid_remove_resource(JID), - affiliation, - SAffiliation, - xml:get_path_s( - Item, [{elem, "reason"}, - cdata])} | Res]); - false -> - {error, ?ERR_NOT_ALLOWED} - end - end - end; - {value, StrRole} -> - case catch list_to_role(StrRole) of + {value, [JID | _] = JIDs} -> + TAffiliation = get_affiliation(JID, StateData), + TRole = get_role(JID, StateData), + case xml:get_attr(<<"role">>, Attrs) of + false -> + case xml:get_attr(<<"affiliation">>, Attrs) of + false -> {error, ?ERR_BAD_REQUEST}; + {value, StrAffiliation} -> + case catch list_to_affiliation(StrAffiliation) of {'EXIT', _} -> - ErrText1 = - io_lib:format( - translate:translate( - Lang, - "Invalid role: ~s"), - [StrRole]), - {error, ?ERRT_BAD_REQUEST(Lang, ErrText1)}; - SRole -> - CanChangeRA = - case can_change_ra( - UAffiliation, URole, - TAffiliation, TRole, - role, SRole) of - nothing -> - nothing; - true -> - true; - check_owner -> - case search_affiliation( - owner, StateData) of - [{OJID, _}] -> - jlib:jid_remove_resource(OJID) /= - jlib:jid_tolower(jlib:jid_remove_resource(UJID)); - _ -> - true - end; - _ -> - false - end, + ErrText1 = iolist_to_binary( + io_lib:format( + translate:translate( + Lang, + <<"Invalid affiliation: ~s">>), + [StrAffiliation])), + {error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText1)}; + SAffiliation -> + ServiceAf = get_service_affiliation(JID, StateData), + CanChangeRA = case can_change_ra(UAffiliation, + URole, + TAffiliation, + TRole, affiliation, + SAffiliation, + ServiceAf) + of + nothing -> nothing; + true -> true; + check_owner -> + case search_affiliation(owner, + StateData) + of + [{OJID, _}] -> + jlib:jid_remove_resource(OJID) + /= + jlib:jid_tolower(jlib:jid_remove_resource(UJID)); + _ -> true + end; + _ -> false + end, case CanChangeRA of - nothing -> - find_changed_items( - UJID, - UAffiliation, URole, - Items, Lang, StateData, - Res); - true -> - find_changed_items( - UJID, - UAffiliation, URole, - Items, Lang, StateData, - [{JID, role, SRole, - xml:get_path_s( - Item, [{elem, "reason"}, - cdata])} | Res]); - _ -> - {error, ?ERR_NOT_ALLOWED} + nothing -> + find_changed_items(UJID, UAffiliation, URole, + Items, Lang, StateData, + Res); + true -> + Reason = xml:get_path_s(Item, + [{elem, <<"reason">>}, + cdata]), + MoreRes = [{jlib:jid_remove_resource(Jidx), + affiliation, SAffiliation, Reason} + || Jidx <- JIDs], + find_changed_items(UJID, UAffiliation, URole, + Items, Lang, StateData, + [MoreRes | Res]); + false -> {error, ?ERR_NOT_ALLOWED} end - end - end; - Err -> - Err + end + end; + {value, StrRole} -> + case catch list_to_role(StrRole) of + {'EXIT', _} -> + ErrText1 = iolist_to_binary( + io_lib:format(translate:translate( + Lang, + <<"Invalid role: ~s">>), + [StrRole])), + {error, ?ERRT_BAD_REQUEST(Lang, ErrText1)}; + SRole -> + ServiceAf = get_service_affiliation(JID, StateData), + CanChangeRA = case can_change_ra(UAffiliation, URole, + TAffiliation, TRole, + role, SRole, ServiceAf) + of + nothing -> nothing; + true -> true; + check_owner -> + case search_affiliation(owner, + StateData) + of + [{OJID, _}] -> + jlib:jid_remove_resource(OJID) + /= + jlib:jid_tolower(jlib:jid_remove_resource(UJID)); + _ -> true + end; + _ -> false + end, + case CanChangeRA of + nothing -> + find_changed_items(UJID, UAffiliation, URole, Items, + Lang, StateData, Res); + true -> + Reason = xml:get_path_s(Item, + [{elem, <<"reason">>}, + cdata]), + MoreRes = [{Jidx, role, SRole, Reason} + || Jidx <- JIDs], + find_changed_items(UJID, UAffiliation, URole, Items, + Lang, StateData, + [MoreRes | Res]); + _ -> {error, ?ERR_NOT_ALLOWED} + end + end + end; + Err -> Err end; find_changed_items(_UJID, _UAffiliation, _URole, _Items, _Lang, _StateData, _Res) -> {error, ?ERR_BAD_REQUEST}. - -can_change_ra(_FAffiliation, _FRole, - TAffiliation, _TRole, - affiliation, Value) - when (TAffiliation == Value) -> +can_change_ra(_FAffiliation, _FRole, owner, _TRole, + affiliation, owner, owner) -> + %% A room owner tries to add as persistent owner a + %% participant that is already owner because he is MUC admin + true; +can_change_ra(_FAffiliation, _FRole, _TAffiliation, + _TRole, _RoleorAffiliation, _Value, owner) -> + %% Nobody can decrease MUC admin's role/affiliation + false; +can_change_ra(_FAffiliation, _FRole, TAffiliation, + _TRole, affiliation, Value, _ServiceAf) + when TAffiliation == Value -> nothing; -can_change_ra(_FAffiliation, _FRole, - _TAffiliation, TRole, - role, Value) - when (TRole == Value) -> +can_change_ra(_FAffiliation, _FRole, _TAffiliation, + TRole, role, Value, _ServiceAf) + when TRole == Value -> nothing; -can_change_ra(FAffiliation, _FRole, - outcast, _TRole, - affiliation, none) - when (FAffiliation == owner) or (FAffiliation == admin) -> +can_change_ra(FAffiliation, _FRole, outcast, _TRole, + affiliation, none, _ServiceAf) + when (FAffiliation == owner) or + (FAffiliation == admin) -> true; -can_change_ra(FAffiliation, _FRole, - outcast, _TRole, - affiliation, member) - when (FAffiliation == owner) or (FAffiliation == admin) -> +can_change_ra(FAffiliation, _FRole, outcast, _TRole, + affiliation, member, _ServiceAf) + when (FAffiliation == owner) or + (FAffiliation == admin) -> true; -can_change_ra(owner, _FRole, - outcast, _TRole, - affiliation, admin) -> +can_change_ra(owner, _FRole, outcast, _TRole, + affiliation, admin, _ServiceAf) -> true; -can_change_ra(owner, _FRole, - outcast, _TRole, - affiliation, owner) -> +can_change_ra(owner, _FRole, outcast, _TRole, + affiliation, owner, _ServiceAf) -> true; -can_change_ra(FAffiliation, _FRole, - none, _TRole, - affiliation, outcast) - when (FAffiliation == owner) or (FAffiliation == admin) -> +can_change_ra(FAffiliation, _FRole, none, _TRole, + affiliation, outcast, _ServiceAf) + when (FAffiliation == owner) or + (FAffiliation == admin) -> true; -can_change_ra(FAffiliation, _FRole, - none, _TRole, - affiliation, member) - when (FAffiliation == owner) or (FAffiliation == admin) -> +can_change_ra(FAffiliation, _FRole, none, _TRole, + affiliation, member, _ServiceAf) + when (FAffiliation == owner) or + (FAffiliation == admin) -> true; -can_change_ra(owner, _FRole, - none, _TRole, - affiliation, admin) -> +can_change_ra(owner, _FRole, none, _TRole, affiliation, + admin, _ServiceAf) -> true; -can_change_ra(owner, _FRole, - none, _TRole, - affiliation, owner) -> +can_change_ra(owner, _FRole, none, _TRole, affiliation, + owner, _ServiceAf) -> true; -can_change_ra(FAffiliation, _FRole, - member, _TRole, - affiliation, outcast) - when (FAffiliation == owner) or (FAffiliation == admin) -> +can_change_ra(FAffiliation, _FRole, member, _TRole, + affiliation, outcast, _ServiceAf) + when (FAffiliation == owner) or + (FAffiliation == admin) -> true; -can_change_ra(FAffiliation, _FRole, - member, _TRole, - affiliation, none) - when (FAffiliation == owner) or (FAffiliation == admin) -> +can_change_ra(FAffiliation, _FRole, member, _TRole, + affiliation, none, _ServiceAf) + when (FAffiliation == owner) or + (FAffiliation == admin) -> true; -can_change_ra(owner, _FRole, - member, _TRole, - affiliation, admin) -> +can_change_ra(owner, _FRole, member, _TRole, + affiliation, admin, _ServiceAf) -> true; -can_change_ra(owner, _FRole, - member, _TRole, - affiliation, owner) -> +can_change_ra(owner, _FRole, member, _TRole, + affiliation, owner, _ServiceAf) -> true; -can_change_ra(owner, _FRole, - admin, _TRole, - affiliation, _Affiliation) -> +can_change_ra(owner, _FRole, admin, _TRole, affiliation, + _Affiliation, _ServiceAf) -> true; -can_change_ra(owner, _FRole, - owner, _TRole, - affiliation, _Affiliation) -> +can_change_ra(owner, _FRole, owner, _TRole, affiliation, + _Affiliation, _ServiceAf) -> check_owner; -can_change_ra(_FAffiliation, _FRole, - _TAffiliation, _TRole, - affiliation, _Value) -> +can_change_ra(_FAffiliation, _FRole, _TAffiliation, + _TRole, affiliation, _Value, _ServiceAf) -> false; -can_change_ra(_FAffiliation, moderator, - _TAffiliation, visitor, - role, none) -> +can_change_ra(_FAffiliation, moderator, _TAffiliation, + visitor, role, none, _ServiceAf) -> true; -can_change_ra(_FAffiliation, moderator, - _TAffiliation, visitor, - role, participant) -> +can_change_ra(_FAffiliation, moderator, _TAffiliation, + visitor, role, participant, _ServiceAf) -> true; -can_change_ra(FAffiliation, _FRole, - _TAffiliation, visitor, - role, moderator) - when (FAffiliation == owner) or (FAffiliation == admin) -> +can_change_ra(FAffiliation, _FRole, _TAffiliation, + visitor, role, moderator, _ServiceAf) + when (FAffiliation == owner) or + (FAffiliation == admin) -> true; -can_change_ra(_FAffiliation, moderator, - _TAffiliation, participant, - role, none) -> +can_change_ra(_FAffiliation, moderator, _TAffiliation, + participant, role, none, _ServiceAf) -> true; -can_change_ra(_FAffiliation, moderator, - _TAffiliation, participant, - role, visitor) -> +can_change_ra(_FAffiliation, moderator, _TAffiliation, + participant, role, visitor, _ServiceAf) -> true; -can_change_ra(FAffiliation, _FRole, - _TAffiliation, participant, - role, moderator) - when (FAffiliation == owner) or (FAffiliation == admin) -> +can_change_ra(FAffiliation, _FRole, _TAffiliation, + participant, role, moderator, _ServiceAf) + when (FAffiliation == owner) or + (FAffiliation == admin) -> true; -can_change_ra(_FAffiliation, _FRole, - owner, moderator, - role, visitor) -> +can_change_ra(_FAffiliation, _FRole, owner, moderator, + role, visitor, _ServiceAf) -> false; -can_change_ra(owner, _FRole, - _TAffiliation, moderator, - role, visitor) -> +can_change_ra(owner, _FRole, _TAffiliation, moderator, + role, visitor, _ServiceAf) -> true; -can_change_ra(_FAffiliation, _FRole, - admin, moderator, - role, visitor) -> +can_change_ra(_FAffiliation, _FRole, admin, moderator, + role, visitor, _ServiceAf) -> false; -can_change_ra(admin, _FRole, - _TAffiliation, moderator, - role, visitor) -> +can_change_ra(admin, _FRole, _TAffiliation, moderator, + role, visitor, _ServiceAf) -> true; -can_change_ra(_FAffiliation, _FRole, - owner, moderator, - role, participant) -> +can_change_ra(_FAffiliation, _FRole, owner, moderator, + role, participant, _ServiceAf) -> false; -can_change_ra(owner, _FRole, - _TAffiliation, moderator, - role, participant) -> +can_change_ra(owner, _FRole, _TAffiliation, moderator, + role, participant, _ServiceAf) -> true; -can_change_ra(_FAffiliation, _FRole, - admin, moderator, - role, participant) -> +can_change_ra(_FAffiliation, _FRole, admin, moderator, + role, participant, _ServiceAf) -> false; -can_change_ra(admin, _FRole, - _TAffiliation, moderator, - role, participant) -> +can_change_ra(admin, _FRole, _TAffiliation, moderator, + role, participant, _ServiceAf) -> true; -can_change_ra(_FAffiliation, _FRole, - _TAffiliation, _TRole, - role, _Value) -> +can_change_ra(_FAffiliation, _FRole, _TAffiliation, + _TRole, role, _Value, _ServiceAf) -> false. +send_kickban_presence(UJID, JID, Reason, Code, StateData) -> + NewAffiliation = get_affiliation(JID, StateData), + send_kickban_presence(UJID, JID, Reason, Code, NewAffiliation, + StateData). -send_kickban_presence(JID, Reason, Code, StateData) -> +send_kickban_presence(UJID, JID, Reason, Code, NewAffiliation, + StateData) -> LJID = jlib:jid_tolower(JID), LJIDs = case LJID of - {U, S, ""} -> - ?DICT:fold( - fun(J, _, Js) -> - case J of - {U, S, _} -> - [J | Js]; - _ -> - Js - end - end, [], StateData#state.users); - _ -> - case ?DICT:is_key(LJID, StateData#state.users) of - true -> - [LJID]; - _ -> - [] - end + {U, S, <<"">>} -> + (?DICT):fold(fun (J, _, Js) -> + case J of + {U, S, _} -> [J | Js]; + _ -> Js + end + end, + [], StateData#state.users); + _ -> + case (?DICT):is_key(LJID, StateData#state.users) of + true -> [LJID]; + _ -> [] + end end, - lists:foreach(fun(J) -> - {ok, #user{nick = Nick}} = - ?DICT:find(J, StateData#state.users), + lists:foreach(fun (J) -> + {ok, #user{nick = Nick}} = (?DICT):find(J, + StateData#state.users), add_to_log(kickban, {Nick, Reason, Code}, StateData), - send_kickban_presence1(J, Reason, Code, StateData) - end, LJIDs). + tab_remove_online_user(J, StateData), + send_kickban_presence1(UJID, J, Reason, Code, + NewAffiliation, StateData) + end, + LJIDs). -send_kickban_presence1(UJID, Reason, Code, StateData) -> - {ok, #user{jid = _RealJID, - nick = Nick}} = - ?DICT:find(jlib:jid_tolower(UJID), StateData#state.users), - Affiliation = get_affiliation(UJID, StateData), +send_kickban_presence1(MJID, UJID, Reason, Code, Affiliation, + StateData) -> + {ok, #user{jid = RealJID, nick = Nick}} = + (?DICT):find(jlib:jid_tolower(UJID), + StateData#state.users), SAffiliation = affiliation_to_list(Affiliation), - lists:foreach( - fun({_LJID, Info}) -> - ItemAttrs = [{"affiliation", SAffiliation}, - {"role", "none"}], - ItemEls = case Reason of - "" -> - []; - _ -> - [{xmlelement, "reason", [], - [{xmlcdata, Reason}]}] - end, - Packet = {xmlelement, "presence", [{"type", "unavailable"}], - [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}], - [{xmlelement, "item", ItemAttrs, ItemEls}, - {xmlelement, "status", [{"code", Code}], []}]}]}, - ejabberd_router:route( - jlib:jid_replace_resource(StateData#state.jid, Nick), - Info#user.jid, - Packet) - end, ?DICT:to_list(StateData#state.users)). - - + BannedJIDString = jlib:jid_to_string(RealJID), + case MJID /= <<"">> of + true -> + {ok, #user{nick = ActorNick}} = + (?DICT):find(jlib:jid_tolower(MJID), + StateData#state.users); + false -> + ActorNick = <<"">> + end, + lists:foreach(fun ({_LJID, Info}) -> + JidAttrList = case Info#user.role == moderator orelse + (StateData#state.config)#config.anonymous + == false + of + true -> + [{<<"jid">>, BannedJIDString}]; + false -> [] + end, + ItemAttrs = [{<<"affiliation">>, SAffiliation}, + {<<"role">>, <<"none">>}] + ++ JidAttrList, + ItemEls = case Reason of + <<"">> -> []; + _ -> + [#xmlel{name = <<"reason">>, + attrs = [], + children = + [{xmlcdata, Reason}]}] + end, + ItemElsActor = case MJID of + <<"">> -> []; + _ -> [#xmlel{name = <<"actor">>, + attrs = + [{<<"nick">>, ActorNick}]}] + end, + Packet = #xmlel{name = <<"presence">>, + attrs = + [{<<"type">>, <<"unavailable">>}], + children = + [#xmlel{name = <<"x">>, + attrs = + [{<<"xmlns">>, + ?NS_MUC_USER}], + children = + [#xmlel{name = + <<"item">>, + attrs = + ItemAttrs, + children = + ItemElsActor ++ ItemEls}, + #xmlel{name = + <<"status">>, + attrs = + [{<<"code">>, + Code}], + children = + []}]}]}, + ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, + Nick), + Info#user.jid, Packet) + end, + (?DICT):to_list(StateData#state.users)). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Owner stuff @@ -2347,660 +3117,1411 @@ send_kickban_presence1(UJID, Reason, Code, StateData) -> process_iq_owner(From, set, Lang, SubEl, StateData) -> FAffiliation = get_affiliation(From, StateData), case FAffiliation of - owner -> - {xmlelement, _Name, _Attrs, Els} = SubEl, - case xml:remove_cdata(Els) of - [{xmlelement, "x", _Attrs1, _Els1} = XEl] -> - case {xml:get_tag_attr_s("xmlns", XEl), - xml:get_tag_attr_s("type", XEl)} of - {?NS_XDATA, "cancel"} -> - {result, [], StateData}; - {?NS_XDATA, "submit"} -> - case {check_allowed_log_change(XEl, StateData, From), - check_allowed_persistent_change(XEl, StateData, From)} of - {allow, allow} -> set_config(XEl, StateData); - _ -> {error, ?ERR_BAD_REQUEST} - end; - _ -> - {error, ?ERR_BAD_REQUEST} - end; - [{xmlelement, "destroy", _Attrs1, _Els1} = SubEl1] -> - destroy_room(SubEl1, StateData); - Items -> - process_admin_items_set(From, Items, Lang, StateData) - end; - _ -> - ErrText = "Owner privileges required", - {error, ?ERRT_FORBIDDEN(Lang, ErrText)} + owner -> + #xmlel{children = Els} = SubEl, + case xml:remove_cdata(Els) of + [#xmlel{name = <<"x">>} = XEl] -> + case {xml:get_tag_attr_s(<<"xmlns">>, XEl), + xml:get_tag_attr_s(<<"type">>, XEl)} + of + {?NS_XDATA, <<"cancel">>} -> {result, [], StateData}; + {?NS_XDATA, <<"submit">>} -> + case is_allowed_log_change(XEl, StateData, From) andalso + is_allowed_persistent_change(XEl, StateData, From) + andalso + is_allowed_room_name_desc_limits(XEl, StateData) + andalso + is_password_settings_correct(XEl, StateData) + of + true -> set_config(XEl, StateData); + false -> {error, ?ERR_NOT_ACCEPTABLE} + end; + _ -> {error, ?ERR_BAD_REQUEST} + end; + [#xmlel{name = <<"destroy">>} = SubEl1] -> + ?INFO_MSG("Destroyed MUC room ~s by the owner ~s", + [jlib:jid_to_string(StateData#state.jid), + jlib:jid_to_string(From)]), + add_to_log(room_existence, destroyed, StateData), + destroy_room(SubEl1, StateData); + Items -> + process_admin_items_set(From, Items, Lang, StateData) + end; + _ -> + ErrText = <<"Owner privileges required">>, + {error, ?ERRT_FORBIDDEN(Lang, ErrText)} end; - process_iq_owner(From, get, Lang, SubEl, StateData) -> FAffiliation = get_affiliation(From, StateData), case FAffiliation of - owner -> - {xmlelement, _Name, _Attrs, Els} = SubEl, - case xml:remove_cdata(Els) of - [] -> - get_config(Lang, StateData, From); - [Item] -> - case xml:get_tag_attr("affiliation", Item) of - false -> - {error, ?ERR_BAD_REQUEST}; - {value, StrAffiliation} -> - case catch list_to_affiliation(StrAffiliation) of - {'EXIT', _} -> - ErrText = - io_lib:format( - translate:translate( - Lang, - "Invalid affiliation: ~s"), - [StrAffiliation]), - {error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)}; - SAffiliation -> - Items = items_with_affiliation( - SAffiliation, StateData), - {result, Items, StateData} - end - end; - _ -> - {error, ?ERR_FEATURE_NOT_IMPLEMENTED} - end; - _ -> - ErrText = "Owner privileges required", - {error, ?ERRT_FORBIDDEN(Lang, ErrText)} + owner -> + #xmlel{children = Els} = SubEl, + case xml:remove_cdata(Els) of + [] -> get_config(Lang, StateData, From); + [Item] -> + case xml:get_tag_attr(<<"affiliation">>, Item) of + false -> {error, ?ERR_BAD_REQUEST}; + {value, StrAffiliation} -> + case catch list_to_affiliation(StrAffiliation) of + {'EXIT', _} -> + ErrText = iolist_to_binary( + io_lib:format( + translate:translate( + Lang, + <<"Invalid affiliation: ~s">>), + [StrAffiliation])), + {error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)}; + SAffiliation -> + Items = items_with_affiliation(SAffiliation, + StateData), + {result, Items, StateData} + end + end; + _ -> {error, ?ERR_FEATURE_NOT_IMPLEMENTED} + end; + _ -> + ErrText = <<"Owner privileges required">>, + {error, ?ERRT_FORBIDDEN(Lang, ErrText)} end. -check_allowed_log_change(XEl, StateData, From) -> - case lists:keymember("muc#roomconfig_enablelogging", 1, - jlib:parse_xdata_submit(XEl)) of - false -> - allow; - true -> - mod_muc_log:check_access_log( - StateData#state.server_host, From) +is_allowed_log_change(XEl, StateData, From) -> + case lists:keymember(<<"muc#roomconfig_enablelogging">>, + 1, jlib:parse_xdata_submit(XEl)) + of + false -> true; + true -> + allow == + mod_muc_log:check_access_log(StateData#state.server_host, + From) end. -check_allowed_persistent_change(XEl, StateData, From) -> - case lists:keymember("muc#roomconfig_persistentroom", 1, - jlib:parse_xdata_submit(XEl)) of - false -> - allow; - true -> - {_AccessRoute, _AccessCreate, _AccessAdmin, AccessPersistent} = StateData#state.access, - acl:match_rule(StateData#state.server_host, AccessPersistent, From) +is_allowed_persistent_change(XEl, StateData, From) -> + case + lists:keymember(<<"muc#roomconfig_persistentroom">>, 1, + jlib:parse_xdata_submit(XEl)) + of + false -> true; + true -> + {_AccessRoute, _AccessCreate, _AccessAdmin, + AccessPersistent} = + StateData#state.access, + allow == + acl:match_rule(StateData#state.server_host, + AccessPersistent, From) + end. + +%% Check if the Room Name and Room Description defined in the Data Form +%% are conformant to the configured limits +is_allowed_room_name_desc_limits(XEl, StateData) -> + IsNameAccepted = case + lists:keysearch(<<"muc#roomconfig_roomname">>, 1, + jlib:parse_xdata_submit(XEl)) + of + {value, {_, [N]}} -> + byte_size(N) =< + gen_mod:get_module_opt(StateData#state.server_host, + mod_muc, max_room_name, + fun(infinity) -> infinity; + (I) when is_integer(I), + I>0 -> I + end, infinity); + _ -> true + end, + IsDescAccepted = case + lists:keysearch(<<"muc#roomconfig_roomdesc">>, 1, + jlib:parse_xdata_submit(XEl)) + of + {value, {_, [D]}} -> + byte_size(D) =< + gen_mod:get_module_opt(StateData#state.server_host, + mod_muc, max_room_desc, + fun(infinity) -> infinity; + (I) when is_integer(I), + I>0 -> + I + end, infinity); + _ -> true + end, + IsNameAccepted and IsDescAccepted. + +%% Return false if: +%% "the password for a password-protected room is blank" +is_password_settings_correct(XEl, StateData) -> + Config = StateData#state.config, + OldProtected = Config#config.password_protected, + OldPassword = Config#config.password, + NewProtected = case + lists:keysearch(<<"muc#roomconfig_passwordprotectedroom">>, + 1, jlib:parse_xdata_submit(XEl)) + of + {value, {_, [<<"1">>]}} -> true; + {value, {_, [<<"0">>]}} -> false; + _ -> undefined + end, + NewPassword = case + lists:keysearch(<<"muc#roomconfig_roomsecret">>, 1, + jlib:parse_xdata_submit(XEl)) + of + {value, {_, [P]}} -> P; + _ -> undefined + end, + case {OldProtected, NewProtected, OldPassword, + NewPassword} + of + {true, undefined, <<"">>, undefined} -> false; + {true, undefined, _, <<"">>} -> false; + {_, true, <<"">>, undefined} -> false; + {_, true, _, <<"">>} -> false; + _ -> true end. -define(XFIELD(Type, Label, Var, Val), - {xmlelement, "field", [{"type", Type}, - {"label", translate:translate(Lang, Label)}, - {"var", Var}], - [{xmlelement, "value", [], [{xmlcdata, Val}]}]}). + #xmlel{name = <<"field">>, + attrs = + [{<<"type">>, Type}, + {<<"label">>, translate:translate(Lang, Label)}, + {<<"var">>, Var}], + children = + [#xmlel{name = <<"value">>, attrs = [], + children = [{xmlcdata, Val}]}]}). -define(BOOLXFIELD(Label, Var, Val), - ?XFIELD("boolean", Label, Var, + ?XFIELD(<<"boolean">>, Label, Var, case Val of - true -> "1"; - _ -> "0" + true -> <<"1">>; + _ -> <<"0">> end)). -define(STRINGXFIELD(Label, Var, Val), - ?XFIELD("text-single", Label, Var, Val)). + ?XFIELD(<<"text-single">>, Label, Var, Val)). -define(PRIVATEXFIELD(Label, Var, Val), - ?XFIELD("text-private", Label, Var, Val)). + ?XFIELD(<<"text-private">>, Label, Var, Val)). +-define(JIDMULTIXFIELD(Label, Var, JIDList), + #xmlel{name = <<"field">>, + attrs = + [{<<"type">>, <<"jid-multi">>}, + {<<"label">>, translate:translate(Lang, Label)}, + {<<"var">>, Var}], + children = + [#xmlel{name = <<"value">>, attrs = [], + children = [{xmlcdata, jlib:jid_to_string(JID)}]} + || JID <- JIDList]}). + +get_default_room_maxusers(RoomState) -> + DefRoomOpts = + gen_mod:get_module_opt(RoomState#state.server_host, + mod_muc, default_room_options, + fun(L) when is_list(L) -> L end, + []), + RoomState2 = set_opts(DefRoomOpts, RoomState), + (RoomState2#state.config)#config.max_users. get_config(Lang, StateData, From) -> - {_AccessRoute, _AccessCreate, _AccessAdmin, AccessPersistent} = StateData#state.access, + {_AccessRoute, _AccessCreate, _AccessAdmin, + AccessPersistent} = + StateData#state.access, ServiceMaxUsers = get_service_max_users(StateData), + DefaultRoomMaxUsers = + get_default_room_maxusers(StateData), Config = StateData#state.config, - Res = - [{xmlelement, "title", [], - [{xmlcdata, translate:translate(Lang, "Configuration for ") ++ - jlib:jid_to_string(StateData#state.jid)}]}, - {xmlelement, "field", [{"type", "hidden"}, - {"var", "FORM_TYPE"}], - [{xmlelement, "value", [], - [{xmlcdata, "http://jabber.org/protocol/muc#roomconfig"}]}]}, - ?STRINGXFIELD("Room title", - "muc#roomconfig_roomname", - Config#config.title) - ] ++ - case acl:match_rule(StateData#state.server_host, AccessPersistent, From) of - allow -> - [?BOOLXFIELD( - "Make room persistent", - "muc#roomconfig_persistentroom", - Config#config.persistent)]; - _ -> [] - end ++ [ - ?BOOLXFIELD("Make room public searchable", - "muc#roomconfig_publicroom", - Config#config.public), - ?BOOLXFIELD("Make participants list public", - "public_list", - Config#config.public_list), - ?BOOLXFIELD("Make room password protected", - "muc#roomconfig_passwordprotectedroom", - Config#config.password_protected), - ?PRIVATEXFIELD("Password", - "muc#roomconfig_roomsecret", - case Config#config.password_protected of - true -> Config#config.password; - false -> "" - end), - {xmlelement, "field", - [{"type", "list-single"}, - {"label", translate:translate(Lang, "Maximum Number of Occupants")}, - {"var", "muc#roomconfig_maxusers"}], - [{xmlelement, "value", [], [{xmlcdata, - case get_max_users(StateData) of - N when is_integer(N) -> - erlang:integer_to_list(N); - _ -> "none" - end - }]}] ++ - if - is_integer(ServiceMaxUsers) -> []; - true -> - [{xmlelement, "option", - [{"label", translate:translate(Lang, "No limit")}], - [{xmlelement, "value", [], [{xmlcdata, "none"}]}]}] - end ++ - [{xmlelement, "option", [{"label", erlang:integer_to_list(N)}], - [{xmlelement, "value", [], - [{xmlcdata, erlang:integer_to_list(N)}]}]} || - N <- ?MAX_USERS_DEFAULT_LIST, N =< ServiceMaxUsers] - }, - {xmlelement, "field", - [{"type", "list-single"}, - {"label", translate:translate(Lang, "Present real JIDs to")}, - {"var", "muc#roomconfig_whois"}], - [{xmlelement, "value", [], [{xmlcdata, - if Config#config.anonymous -> - "moderators"; - true -> - "anyone" - end}]}, - {xmlelement, "option", [{"label", translate:translate(Lang, "moderators only")}], - [{xmlelement, "value", [], [{xmlcdata, "moderators"}]}]}, - {xmlelement, "option", [{"label", translate:translate(Lang, "anyone")}], - [{xmlelement, "value", [], [{xmlcdata, "anyone"}]}]}]}, - ?BOOLXFIELD("Make room members-only", - "muc#roomconfig_membersonly", - Config#config.members_only), - ?BOOLXFIELD("Make room moderated", - "muc#roomconfig_moderatedroom", - Config#config.moderated), - ?BOOLXFIELD("Default users as participants", - "members_by_default", - Config#config.members_by_default), - ?BOOLXFIELD("Allow users to change subject", - "muc#roomconfig_changesubject", - Config#config.allow_change_subj), - ?BOOLXFIELD("Allow users to send private messages", - "allow_private_messages", - Config#config.allow_private_messages), - ?BOOLXFIELD("Allow users to query other users", - "allow_query_users", - Config#config.allow_query_users), - ?BOOLXFIELD("Allow users to send invites", - "muc#roomconfig_allowinvites", - Config#config.allow_user_invites) - ] ++ - case mod_muc_log:check_access_log( - StateData#state.server_host, From) of - allow -> - [?BOOLXFIELD( - "Enable logging", - "muc#roomconfig_enablelogging", - Config#config.logging)]; - _ -> [] - end, - {result, [{xmlelement, "instructions", [], - [{xmlcdata, - translate:translate( - Lang, "You need an x:data capable client to configure room")}]}, - {xmlelement, "x", [{"xmlns", ?NS_XDATA}, - {"type", "form"}], - Res}], + {MaxUsersRoomInteger, MaxUsersRoomString} = case + get_max_users(StateData) + of + N when is_integer(N) -> + {N, + jlib:integer_to_binary(N)}; + _ -> {0, <<"none">>} + end, + Res = [#xmlel{name = <<"title">>, attrs = [], + children = + [{xmlcdata, + iolist_to_binary( + io_lib:format( + translate:translate( + Lang, + <<"Configuration of room ~s">>), + [jlib:jid_to_string(StateData#state.jid)]))}]}, + #xmlel{name = <<"field">>, + attrs = + [{<<"type">>, <<"hidden">>}, + {<<"var">>, <<"FORM_TYPE">>}], + children = + [#xmlel{name = <<"value">>, attrs = [], + children = + [{xmlcdata, + <<"http://jabber.org/protocol/muc#roomconfig">>}]}]}, + ?STRINGXFIELD(<<"Room title">>, + <<"muc#roomconfig_roomname">>, (Config#config.title)), + ?STRINGXFIELD(<<"Room description">>, + <<"muc#roomconfig_roomdesc">>, + (Config#config.description))] + ++ + case acl:match_rule(StateData#state.server_host, + AccessPersistent, From) + of + allow -> + [?BOOLXFIELD(<<"Make room persistent">>, + <<"muc#roomconfig_persistentroom">>, + (Config#config.persistent))]; + _ -> [] + end + ++ + [?BOOLXFIELD(<<"Make room public searchable">>, + <<"muc#roomconfig_publicroom">>, + (Config#config.public)), + ?BOOLXFIELD(<<"Make participants list public">>, + <<"public_list">>, (Config#config.public_list)), + ?BOOLXFIELD(<<"Make room password protected">>, + <<"muc#roomconfig_passwordprotectedroom">>, + (Config#config.password_protected)), + ?PRIVATEXFIELD(<<"Password">>, + <<"muc#roomconfig_roomsecret">>, + case Config#config.password_protected of + true -> Config#config.password; + false -> <<"">> + end), + #xmlel{name = <<"field">>, + attrs = + [{<<"type">>, <<"list-single">>}, + {<<"label">>, + translate:translate(Lang, + <<"Maximum Number of Occupants">>)}, + {<<"var">>, <<"muc#roomconfig_maxusers">>}], + children = + [#xmlel{name = <<"value">>, attrs = [], + children = [{xmlcdata, MaxUsersRoomString}]}] + ++ + if is_integer(ServiceMaxUsers) -> []; + true -> + [#xmlel{name = <<"option">>, + attrs = + [{<<"label">>, + translate:translate(Lang, + <<"No limit">>)}], + children = + [#xmlel{name = <<"value">>, + attrs = [], + children = + [{xmlcdata, + <<"none">>}]}]}] + end + ++ + [#xmlel{name = <<"option">>, + attrs = + [{<<"label">>, + jlib:integer_to_binary(N)}], + children = + [#xmlel{name = <<"value">>, + attrs = [], + children = + [{xmlcdata, + jlib:integer_to_binary(N)}]}]} + || N + <- lists:usort([ServiceMaxUsers, + DefaultRoomMaxUsers, + MaxUsersRoomInteger + | ?MAX_USERS_DEFAULT_LIST]), + N =< ServiceMaxUsers]}, + #xmlel{name = <<"field">>, + attrs = + [{<<"type">>, <<"list-single">>}, + {<<"label">>, + translate:translate(Lang, + <<"Present real Jabber IDs to">>)}, + {<<"var">>, <<"muc#roomconfig_whois">>}], + children = + [#xmlel{name = <<"value">>, attrs = [], + children = + [{xmlcdata, + if Config#config.anonymous -> + <<"moderators">>; + true -> <<"anyone">> + end}]}, + #xmlel{name = <<"option">>, + attrs = + [{<<"label">>, + translate:translate(Lang, + <<"moderators only">>)}], + children = + [#xmlel{name = <<"value">>, attrs = [], + children = + [{xmlcdata, + <<"moderators">>}]}]}, + #xmlel{name = <<"option">>, + attrs = + [{<<"label">>, + translate:translate(Lang, + <<"anyone">>)}], + children = + [#xmlel{name = <<"value">>, attrs = [], + children = + [{xmlcdata, + <<"anyone">>}]}]}]}, + ?BOOLXFIELD(<<"Make room members-only">>, + <<"muc#roomconfig_membersonly">>, + (Config#config.members_only)), + ?BOOLXFIELD(<<"Make room moderated">>, + <<"muc#roomconfig_moderatedroom">>, + (Config#config.moderated)), + ?BOOLXFIELD(<<"Default users as participants">>, + <<"members_by_default">>, + (Config#config.members_by_default)), + ?BOOLXFIELD(<<"Allow users to change the subject">>, + <<"muc#roomconfig_changesubject">>, + (Config#config.allow_change_subj)), + ?BOOLXFIELD(<<"Allow users to send private messages">>, + <<"allow_private_messages">>, + (Config#config.allow_private_messages)), + #xmlel{name = <<"field">>, + attrs = + [{<<"type">>, <<"list-single">>}, + {<<"label">>, + translate:translate(Lang, + <<"Allow visitors to send private messages to">>)}, + {<<"var">>, + <<"allow_private_messages_from_visitors">>}], + children = + [#xmlel{name = <<"value">>, attrs = [], + children = + [{xmlcdata, + case + Config#config.allow_private_messages_from_visitors + of + anyone -> <<"anyone">>; + moderators -> <<"moderators">>; + nobody -> <<"nobody">> + end}]}, + #xmlel{name = <<"option">>, + attrs = + [{<<"label">>, + translate:translate(Lang, + <<"nobody">>)}], + children = + [#xmlel{name = <<"value">>, attrs = [], + children = + [{xmlcdata, <<"nobody">>}]}]}, + #xmlel{name = <<"option">>, + attrs = + [{<<"label">>, + translate:translate(Lang, + <<"moderators only">>)}], + children = + [#xmlel{name = <<"value">>, attrs = [], + children = + [{xmlcdata, + <<"moderators">>}]}]}, + #xmlel{name = <<"option">>, + attrs = + [{<<"label">>, + translate:translate(Lang, + <<"anyone">>)}], + children = + [#xmlel{name = <<"value">>, attrs = [], + children = + [{xmlcdata, + <<"anyone">>}]}]}]}, + ?BOOLXFIELD(<<"Allow users to query other users">>, + <<"allow_query_users">>, + (Config#config.allow_query_users)), + ?BOOLXFIELD(<<"Allow users to send invites">>, + <<"muc#roomconfig_allowinvites">>, + (Config#config.allow_user_invites)), + ?BOOLXFIELD(<<"Allow visitors to send status text in " + "presence updates">>, + <<"muc#roomconfig_allowvisitorstatus">>, + (Config#config.allow_visitor_status)), + ?BOOLXFIELD(<<"Allow visitors to change nickname">>, + <<"muc#roomconfig_allowvisitornickchange">>, + (Config#config.allow_visitor_nickchange)), + ?BOOLXFIELD(<<"Allow visitors to send voice requests">>, + <<"muc#roomconfig_allowvoicerequests">>, + (Config#config.allow_voice_requests)), + ?STRINGXFIELD(<<"Minimum interval between voice requests " + "(in seconds)">>, + <<"muc#roomconfig_voicerequestmininterval">>, + (jlib:integer_to_binary(Config#config.voice_request_min_interval)))] + ++ + case ejabberd_captcha:is_feature_available() of + true -> + [?BOOLXFIELD(<<"Make room CAPTCHA protected">>, + <<"captcha_protected">>, + (Config#config.captcha_protected))]; + false -> [] + end + ++ + [?JIDMULTIXFIELD(<<"Exclude Jabber IDs from CAPTCHA challenge">>, + <<"muc#roomconfig_captcha_whitelist">>, + ((?SETS):to_list(Config#config.captcha_whitelist)))] + ++ + case + mod_muc_log:check_access_log(StateData#state.server_host, + From) + of + allow -> + [?BOOLXFIELD(<<"Enable logging">>, + <<"muc#roomconfig_enablelogging">>, + (Config#config.logging))]; + _ -> [] + end, + {result, + [#xmlel{name = <<"instructions">>, attrs = [], + children = + [{xmlcdata, + translate:translate(Lang, + <<"You need an x:data capable client to " + "configure room">>)}]}, + #xmlel{name = <<"x">>, + attrs = + [{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"form">>}], + children = Res}], StateData}. - - set_config(XEl, StateData) -> XData = jlib:parse_xdata_submit(XEl), case XData of - invalid -> - {error, ?ERR_BAD_REQUEST}; - _ -> - case set_xoption(XData, StateData#state.config) of - #config{} = Config -> - Res = change_config(Config, StateData), - {result, _, NSD} = Res, - add_to_log(roomconfig_change, [], NSD), - Res; - Err -> - Err - end + invalid -> {error, ?ERR_BAD_REQUEST}; + _ -> + case set_xoption(XData, StateData#state.config) of + #config{} = Config -> + Res = change_config(Config, StateData), + {result, _, NSD} = Res, + Type = case {(StateData#state.config)#config.logging, + Config#config.logging} + of + {true, false} -> roomconfig_change_disabledlogging; + {false, true} -> roomconfig_change_enabledlogging; + {_, _} -> roomconfig_change + end, + Users = [{U#user.jid, U#user.nick, U#user.role} + || {_, U} <- (?DICT):to_list(StateData#state.users)], + add_to_log(Type, Users, NSD), + Res; + Err -> Err + end end. -define(SET_BOOL_XOPT(Opt, Val), case Val of - "0" -> set_xoption(Opts, Config#config{Opt = false}); - "false" -> set_xoption(Opts, Config#config{Opt = false}); - "1" -> set_xoption(Opts, Config#config{Opt = true}); - "true" -> set_xoption(Opts, Config#config{Opt = true}); - _ -> {error, ?ERR_BAD_REQUEST} + <<"0">> -> + set_xoption(Opts, Config#config{Opt = false}); + <<"false">> -> + set_xoption(Opts, Config#config{Opt = false}); + <<"1">> -> set_xoption(Opts, Config#config{Opt = true}); + <<"true">> -> + set_xoption(Opts, Config#config{Opt = true}); + _ -> {error, ?ERR_BAD_REQUEST} end). -define(SET_NAT_XOPT(Opt, Val), - case catch list_to_integer(Val) of - I when is_integer(I), - I > 0 -> - set_xoption(Opts, Config#config{Opt = I}); - _ -> - {error, ?ERR_BAD_REQUEST} + case catch jlib:binary_to_integer(Val) of + I when is_integer(I), I > 0 -> + set_xoption(Opts, Config#config{Opt = I}); + _ -> {error, ?ERR_BAD_REQUEST} end). -define(SET_STRING_XOPT(Opt, Val), set_xoption(Opts, Config#config{Opt = Val})). +-define(SET_JIDMULTI_XOPT(Opt, Vals), + begin + Set = lists:foldl(fun ({U, S, R}, Set1) -> + (?SETS):add_element({U, S, R}, Set1); + (#jid{luser = U, lserver = S, lresource = R}, + Set1) -> + (?SETS):add_element({U, S, R}, Set1); + (_, Set1) -> Set1 + end, + (?SETS):empty(), Vals), + set_xoption(Opts, Config#config{Opt = Set}) + end). -set_xoption([], Config) -> - Config; -set_xoption([{"muc#roomconfig_roomname", [Val]} | Opts], Config) -> +set_xoption([], Config) -> Config; +set_xoption([{<<"muc#roomconfig_roomname">>, [Val]} + | Opts], + Config) -> ?SET_STRING_XOPT(title, Val); -set_xoption([{"muc#roomconfig_changesubject", [Val]} | Opts], Config) -> +set_xoption([{<<"muc#roomconfig_roomdesc">>, [Val]} + | Opts], + Config) -> + ?SET_STRING_XOPT(description, Val); +set_xoption([{<<"muc#roomconfig_changesubject">>, [Val]} + | Opts], + Config) -> ?SET_BOOL_XOPT(allow_change_subj, Val); -set_xoption([{"allow_query_users", [Val]} | Opts], Config) -> +set_xoption([{<<"allow_query_users">>, [Val]} | Opts], + Config) -> ?SET_BOOL_XOPT(allow_query_users, Val); -set_xoption([{"allow_private_messages", [Val]} | Opts], Config) -> +set_xoption([{<<"allow_private_messages">>, [Val]} + | Opts], + Config) -> ?SET_BOOL_XOPT(allow_private_messages, Val); -set_xoption([{"muc#roomconfig_publicroom", [Val]} | Opts], Config) -> +set_xoption([{<<"allow_private_messages_from_visitors">>, + [Val]} + | Opts], + Config) -> + case Val of + <<"anyone">> -> + ?SET_STRING_XOPT(allow_private_messages_from_visitors, + anyone); + <<"moderators">> -> + ?SET_STRING_XOPT(allow_private_messages_from_visitors, + moderators); + <<"nobody">> -> + ?SET_STRING_XOPT(allow_private_messages_from_visitors, + nobody); + _ -> {error, ?ERR_BAD_REQUEST} + end; +set_xoption([{<<"muc#roomconfig_allowvisitorstatus">>, + [Val]} + | Opts], + Config) -> + ?SET_BOOL_XOPT(allow_visitor_status, Val); +set_xoption([{<<"muc#roomconfig_allowvisitornickchange">>, + [Val]} + | Opts], + Config) -> + ?SET_BOOL_XOPT(allow_visitor_nickchange, Val); +set_xoption([{<<"muc#roomconfig_publicroom">>, [Val]} + | Opts], + Config) -> ?SET_BOOL_XOPT(public, Val); -set_xoption([{"public_list", [Val]} | Opts], Config) -> +set_xoption([{<<"public_list">>, [Val]} | Opts], + Config) -> ?SET_BOOL_XOPT(public_list, Val); -set_xoption([{"muc#roomconfig_persistentroom", [Val]} | Opts], Config) -> +set_xoption([{<<"muc#roomconfig_persistentroom">>, + [Val]} + | Opts], + Config) -> ?SET_BOOL_XOPT(persistent, Val); -set_xoption([{"muc#roomconfig_moderatedroom", [Val]} | Opts], Config) -> +set_xoption([{<<"muc#roomconfig_moderatedroom">>, [Val]} + | Opts], + Config) -> ?SET_BOOL_XOPT(moderated, Val); -set_xoption([{"members_by_default", [Val]} | Opts], Config) -> +set_xoption([{<<"members_by_default">>, [Val]} | Opts], + Config) -> ?SET_BOOL_XOPT(members_by_default, Val); -set_xoption([{"muc#roomconfig_membersonly", [Val]} | Opts], Config) -> +set_xoption([{<<"muc#roomconfig_membersonly">>, [Val]} + | Opts], + Config) -> ?SET_BOOL_XOPT(members_only, Val); -set_xoption([{"muc#roomconfig_allowinvites", [Val]} | Opts], Config) -> +set_xoption([{<<"captcha_protected">>, [Val]} | Opts], + Config) -> + ?SET_BOOL_XOPT(captcha_protected, Val); +set_xoption([{<<"muc#roomconfig_allowinvites">>, [Val]} + | Opts], + Config) -> ?SET_BOOL_XOPT(allow_user_invites, Val); -set_xoption([{"muc#roomconfig_passwordprotectedroom", [Val]} | Opts], Config) -> +set_xoption([{<<"muc#roomconfig_passwordprotectedroom">>, + [Val]} + | Opts], + Config) -> ?SET_BOOL_XOPT(password_protected, Val); -set_xoption([{"muc#roomconfig_roomsecret", [Val]} | Opts], Config) -> +set_xoption([{<<"muc#roomconfig_roomsecret">>, [Val]} + | Opts], + Config) -> ?SET_STRING_XOPT(password, Val); -set_xoption([{"anonymous", [Val]} | Opts], Config) -> +set_xoption([{<<"anonymous">>, [Val]} | Opts], + Config) -> ?SET_BOOL_XOPT(anonymous, Val); -set_xoption([{"muc#roomconfig_whois", [Val]} | Opts], Config) -> +set_xoption([{<<"muc#roomconfig_allowvoicerequests">>, + [Val]} + | Opts], + Config) -> + ?SET_BOOL_XOPT(allow_voice_requests, Val); +set_xoption([{<<"muc#roomconfig_voicerequestmininterval">>, + [Val]} + | Opts], + Config) -> + ?SET_NAT_XOPT(voice_request_min_interval, Val); +set_xoption([{<<"muc#roomconfig_whois">>, [Val]} + | Opts], + Config) -> case Val of - "moderators" -> - ?SET_BOOL_XOPT(anonymous, "1"); - "anyone" -> - ?SET_BOOL_XOPT(anonymous, "0"); - _ -> - {error, ?ERR_BAD_REQUEST} + <<"moderators">> -> + ?SET_BOOL_XOPT(anonymous, + (iolist_to_binary(integer_to_list(1)))); + <<"anyone">> -> + ?SET_BOOL_XOPT(anonymous, + (iolist_to_binary(integer_to_list(0)))); + _ -> {error, ?ERR_BAD_REQUEST} end; -set_xoption([{"muc#roomconfig_maxusers", [Val]} | Opts], Config) -> +set_xoption([{<<"muc#roomconfig_maxusers">>, [Val]} + | Opts], + Config) -> case Val of - "none" -> - ?SET_STRING_XOPT(max_users, none); - _ -> - ?SET_NAT_XOPT(max_users, Val) + <<"none">> -> ?SET_STRING_XOPT(max_users, none); + _ -> ?SET_NAT_XOPT(max_users, Val) end; -set_xoption([{"muc#roomconfig_enablelogging", [Val]} | Opts], Config) -> +set_xoption([{<<"muc#roomconfig_enablelogging">>, [Val]} + | Opts], + Config) -> ?SET_BOOL_XOPT(logging, Val); -set_xoption([{"FORM_TYPE", _} | Opts], Config) -> - %% Ignore our FORM_TYPE +set_xoption([{<<"muc#roomconfig_captcha_whitelist">>, + Vals} + | Opts], + Config) -> + JIDs = [jlib:string_to_jid(Val) || Val <- Vals], + ?SET_JIDMULTI_XOPT(captcha_whitelist, JIDs); +set_xoption([{<<"FORM_TYPE">>, _} | Opts], Config) -> set_xoption(Opts, Config); set_xoption([_ | _Opts], _Config) -> {error, ?ERR_BAD_REQUEST}. - change_config(Config, StateData) -> NSD = StateData#state{config = Config}, case {(StateData#state.config)#config.persistent, - Config#config.persistent} of - {_, true} -> - mod_muc:store_room(NSD#state.host, NSD#state.room, make_opts(NSD)); - {true, false} -> - mod_muc:forget_room(NSD#state.host, NSD#state.room); - {false, false} -> - ok + Config#config.persistent} + of + {_, true} -> + mod_muc:store_room(NSD#state.server_host, + NSD#state.host, NSD#state.room, make_opts(NSD)); + {true, false} -> + mod_muc:forget_room(NSD#state.server_host, + NSD#state.host, NSD#state.room); + {false, false} -> ok end, case {(StateData#state.config)#config.members_only, - Config#config.members_only} of - {false, true} -> - NSD1 = remove_nonmembers(NSD), - {result, [], NSD1}; - _ -> - {result, [], NSD} + Config#config.members_only} + of + {false, true} -> + NSD1 = remove_nonmembers(NSD), {result, [], NSD1}; + _ -> {result, [], NSD} end. remove_nonmembers(StateData) -> - lists:foldl( - fun({_LJID, #user{jid = JID}}, SD) -> - Affiliation = get_affiliation(JID, SD), - case Affiliation of - none -> - catch send_kickban_presence( - JID, "", "322", SD), - set_role(JID, none, SD); - _ -> - SD - end - end, StateData, ?DICT:to_list(StateData#state.users)). + lists:foldl(fun ({_LJID, #user{jid = JID}}, SD) -> + Affiliation = get_affiliation(JID, SD), + case Affiliation of + none -> + catch send_kickban_presence(<<"">>, JID, <<"">>, + <<"322">>, SD), + set_role(JID, none, SD); + _ -> SD + end + end, + StateData, (?DICT):to_list(StateData#state.users)). - --define(CASE_CONFIG_OPT(Opt), - Opt -> StateData#state{ - config = (StateData#state.config)#config{Opt = Val}}). - -set_opts([], StateData) -> - StateData; +set_opts([], StateData) -> StateData; set_opts([{Opt, Val} | Opts], StateData) -> NSD = case Opt of - ?CASE_CONFIG_OPT(title); - ?CASE_CONFIG_OPT(allow_change_subj); - ?CASE_CONFIG_OPT(allow_query_users); - ?CASE_CONFIG_OPT(allow_private_messages); - ?CASE_CONFIG_OPT(public); - ?CASE_CONFIG_OPT(public_list); - ?CASE_CONFIG_OPT(persistent); - ?CASE_CONFIG_OPT(moderated); - ?CASE_CONFIG_OPT(members_by_default); - ?CASE_CONFIG_OPT(members_only); - ?CASE_CONFIG_OPT(allow_user_invites); - ?CASE_CONFIG_OPT(password_protected); - ?CASE_CONFIG_OPT(password); - ?CASE_CONFIG_OPT(anonymous); - ?CASE_CONFIG_OPT(logging); - max_users -> - ServiceMaxUsers = get_service_max_users(StateData), - MaxUsers = if - Val =< ServiceMaxUsers -> Val; - true -> ServiceMaxUsers - end, - StateData#state{ - config = (StateData#state.config)#config{ - max_users = MaxUsers}}; - affiliations -> - StateData#state{affiliations = ?DICT:from_list(Val)}; - subject -> - StateData#state{subject = Val}; - subject_author -> - StateData#state{subject_author = Val}; - _ -> StateData + title -> + StateData#state{config = + (StateData#state.config)#config{title = + Val}}; + description -> + StateData#state{config = + (StateData#state.config)#config{description + = Val}}; + allow_change_subj -> + StateData#state{config = + (StateData#state.config)#config{allow_change_subj + = Val}}; + allow_query_users -> + StateData#state{config = + (StateData#state.config)#config{allow_query_users + = Val}}; + allow_private_messages -> + StateData#state{config = + (StateData#state.config)#config{allow_private_messages + = Val}}; + allow_private_messages_from_visitors -> + StateData#state{config = + (StateData#state.config)#config{allow_private_messages_from_visitors + = Val}}; + allow_visitor_nickchange -> + StateData#state{config = + (StateData#state.config)#config{allow_visitor_nickchange + = Val}}; + allow_visitor_status -> + StateData#state{config = + (StateData#state.config)#config{allow_visitor_status + = Val}}; + public -> + StateData#state{config = + (StateData#state.config)#config{public = + Val}}; + public_list -> + StateData#state{config = + (StateData#state.config)#config{public_list + = Val}}; + persistent -> + StateData#state{config = + (StateData#state.config)#config{persistent = + Val}}; + moderated -> + StateData#state{config = + (StateData#state.config)#config{moderated = + Val}}; + members_by_default -> + StateData#state{config = + (StateData#state.config)#config{members_by_default + = Val}}; + members_only -> + StateData#state{config = + (StateData#state.config)#config{members_only + = Val}}; + allow_user_invites -> + StateData#state{config = + (StateData#state.config)#config{allow_user_invites + = Val}}; + password_protected -> + StateData#state{config = + (StateData#state.config)#config{password_protected + = Val}}; + captcha_protected -> + StateData#state{config = + (StateData#state.config)#config{captcha_protected + = Val}}; + password -> + StateData#state{config = + (StateData#state.config)#config{password = + Val}}; + anonymous -> + StateData#state{config = + (StateData#state.config)#config{anonymous = + Val}}; + logging -> + StateData#state{config = + (StateData#state.config)#config{logging = + Val}}; + captcha_whitelist -> + StateData#state{config = + (StateData#state.config)#config{captcha_whitelist + = + (?SETS):from_list(Val)}}; + allow_voice_requests -> + StateData#state{config = + (StateData#state.config)#config{allow_voice_requests + = Val}}; + voice_request_min_interval -> + StateData#state{config = + (StateData#state.config)#config{voice_request_min_interval + = Val}}; + max_users -> + ServiceMaxUsers = get_service_max_users(StateData), + MaxUsers = if Val =< ServiceMaxUsers -> Val; + true -> ServiceMaxUsers + end, + StateData#state{config = + (StateData#state.config)#config{max_users = + MaxUsers}}; + vcard -> + StateData#state{config = + (StateData#state.config)#config{vcard = + Val}}; + affiliations -> + StateData#state{affiliations = (?DICT):from_list(Val)}; + subject -> StateData#state{subject = Val}; + subject_author -> StateData#state{subject_author = Val}; + _ -> StateData end, set_opts(Opts, NSD). -define(MAKE_CONFIG_OPT(Opt), {Opt, Config#config.Opt}). + make_opts(StateData) -> Config = StateData#state.config, - [ - ?MAKE_CONFIG_OPT(title), + [?MAKE_CONFIG_OPT(title), ?MAKE_CONFIG_OPT(description), ?MAKE_CONFIG_OPT(allow_change_subj), ?MAKE_CONFIG_OPT(allow_query_users), ?MAKE_CONFIG_OPT(allow_private_messages), - ?MAKE_CONFIG_OPT(public), - ?MAKE_CONFIG_OPT(public_list), + ?MAKE_CONFIG_OPT(allow_private_messages_from_visitors), + ?MAKE_CONFIG_OPT(allow_visitor_status), + ?MAKE_CONFIG_OPT(allow_visitor_nickchange), + ?MAKE_CONFIG_OPT(public), ?MAKE_CONFIG_OPT(public_list), ?MAKE_CONFIG_OPT(persistent), ?MAKE_CONFIG_OPT(moderated), ?MAKE_CONFIG_OPT(members_by_default), ?MAKE_CONFIG_OPT(members_only), ?MAKE_CONFIG_OPT(allow_user_invites), ?MAKE_CONFIG_OPT(password_protected), - ?MAKE_CONFIG_OPT(password), - ?MAKE_CONFIG_OPT(anonymous), - ?MAKE_CONFIG_OPT(logging), - ?MAKE_CONFIG_OPT(max_users), - {affiliations, ?DICT:to_list(StateData#state.affiliations)}, + ?MAKE_CONFIG_OPT(captcha_protected), + ?MAKE_CONFIG_OPT(password), ?MAKE_CONFIG_OPT(anonymous), + ?MAKE_CONFIG_OPT(logging), ?MAKE_CONFIG_OPT(max_users), + ?MAKE_CONFIG_OPT(allow_voice_requests), + ?MAKE_CONFIG_OPT(voice_request_min_interval), + ?MAKE_CONFIG_OPT(vcard), + {captcha_whitelist, + (?SETS):to_list((StateData#state.config)#config.captcha_whitelist)}, + {affiliations, + (?DICT):to_list(StateData#state.affiliations)}, {subject, StateData#state.subject}, - {subject_author, StateData#state.subject_author} - ]. - - + {subject_author, StateData#state.subject_author}]. destroy_room(DEl, StateData) -> - lists:foreach( - fun({_LJID, Info}) -> - Nick = Info#user.nick, - ItemAttrs = [{"affiliation", "none"}, - {"role", "none"}], - Packet = {xmlelement, "presence", [{"type", "unavailable"}], - [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}], - [{xmlelement, "item", ItemAttrs, []}, DEl]}]}, - ejabberd_router:route( - jlib:jid_replace_resource(StateData#state.jid, Nick), - Info#user.jid, - Packet) - end, ?DICT:to_list(StateData#state.users)), + lists:foreach(fun ({_LJID, Info}) -> + Nick = Info#user.nick, + ItemAttrs = [{<<"affiliation">>, <<"none">>}, + {<<"role">>, <<"none">>}], + Packet = #xmlel{name = <<"presence">>, + attrs = + [{<<"type">>, <<"unavailable">>}], + children = + [#xmlel{name = <<"x">>, + attrs = + [{<<"xmlns">>, + ?NS_MUC_USER}], + children = + [#xmlel{name = + <<"item">>, + attrs = + ItemAttrs, + children = + []}, + DEl]}]}, + ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, + Nick), + Info#user.jid, Packet) + end, + (?DICT):to_list(StateData#state.users)), case (StateData#state.config)#config.persistent of - true -> - mod_muc:forget_room(StateData#state.host, StateData#state.room); - false -> - ok - end, + true -> + mod_muc:forget_room(StateData#state.server_host, + StateData#state.host, StateData#state.room); + false -> ok + end, {result, [], stop}. - - - %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Disco --define(FEATURE(Var), {xmlelement, "feature", [{"var", Var}], []}). +-define(FEATURE(Var), + #xmlel{name = <<"feature">>, attrs = [{<<"var">>, Var}], + children = []}). -define(CONFIG_OPT_TO_FEATURE(Opt, Fiftrue, Fiffalse), - case Opt of - true -> - ?FEATURE(Fiftrue); - false -> - ?FEATURE(Fiffalse) - end). + case Opt of + true -> ?FEATURE(Fiftrue); + false -> ?FEATURE(Fiffalse) + end). process_iq_disco_info(_From, set, _Lang, _StateData) -> {error, ?ERR_NOT_ALLOWED}; - process_iq_disco_info(_From, get, Lang, StateData) -> Config = StateData#state.config, - {result, [{xmlelement, "identity", - [{"category", "conference"}, - {"type", "text"}, - {"name", get_title(StateData)}], []}, - {xmlelement, "feature", - [{"var", ?NS_MUC}], []}, - ?CONFIG_OPT_TO_FEATURE(Config#config.public, - "muc_public", "muc_hidden"), - ?CONFIG_OPT_TO_FEATURE(Config#config.persistent, - "muc_persistent", "muc_temporary"), - ?CONFIG_OPT_TO_FEATURE(Config#config.members_only, - "muc_membersonly", "muc_open"), - ?CONFIG_OPT_TO_FEATURE(Config#config.anonymous, - "muc_semianonymous", "muc_nonanonymous"), - ?CONFIG_OPT_TO_FEATURE(Config#config.moderated, - "muc_moderated", "muc_unmoderated"), - ?CONFIG_OPT_TO_FEATURE(Config#config.password_protected, - "muc_passwordprotected", "muc_unsecured") - ] ++ iq_disco_info_extras(Lang, StateData), StateData}. + {result, + [#xmlel{name = <<"identity">>, + attrs = + [{<<"category">>, <<"conference">>}, + {<<"type">>, <<"text">>}, + {<<"name">>, get_title(StateData)}], + children = []}, + #xmlel{name = <<"feature">>, + attrs = [{<<"var">>, ?NS_VCARD}], children = []}, + #xmlel{name = <<"feature">>, + attrs = [{<<"var">>, ?NS_MUC}], children = []}, + ?CONFIG_OPT_TO_FEATURE((Config#config.public), + <<"muc_public">>, <<"muc_hidden">>), + ?CONFIG_OPT_TO_FEATURE((Config#config.persistent), + <<"muc_persistent">>, <<"muc_temporary">>), + ?CONFIG_OPT_TO_FEATURE((Config#config.members_only), + <<"muc_membersonly">>, <<"muc_open">>), + ?CONFIG_OPT_TO_FEATURE((Config#config.anonymous), + <<"muc_semianonymous">>, <<"muc_nonanonymous">>), + ?CONFIG_OPT_TO_FEATURE((Config#config.moderated), + <<"muc_moderated">>, <<"muc_unmoderated">>), + ?CONFIG_OPT_TO_FEATURE((Config#config.password_protected), + <<"muc_passwordprotected">>, <<"muc_unsecured">>)] + ++ iq_disco_info_extras(Lang, StateData), + StateData}. -define(RFIELDT(Type, Var, Val), - {xmlelement, "field", [{"type", Type}, {"var", Var}], - [{xmlelement, "value", [], [{xmlcdata, Val}]}]}). + #xmlel{name = <<"field">>, + attrs = [{<<"type">>, Type}, {<<"var">>, Var}], + children = + [#xmlel{name = <<"value">>, attrs = [], + children = [{xmlcdata, Val}]}]}). -define(RFIELD(Label, Var, Val), - {xmlelement, "field", [{"label", translate:translate(Lang, Label)}, - {"var", Var}], - [{xmlelement, "value", [], [{xmlcdata, Val}]}]}). + #xmlel{name = <<"field">>, + attrs = + [{<<"label">>, translate:translate(Lang, Label)}, + {<<"var">>, Var}], + children = + [#xmlel{name = <<"value">>, attrs = [], + children = [{xmlcdata, Val}]}]}). iq_disco_info_extras(Lang, StateData) -> - Len = length(?DICT:to_list(StateData#state.users)), - [{xmlelement, "x", [{"xmlns", ?NS_XDATA}, {"type", "result"}], - [?RFIELDT("hidden", "FORM_TYPE", - "http://jabber.org/protocol/muc#roominfo"), - ?RFIELD("Description", "muc#roominfo_description", - (StateData#state.config)#config.title), - %?RFIELD("Subject", "muc#roominfo_subject", StateData#state.subject), - ?RFIELD("Number of occupants", "muc#roominfo_occupants", - integer_to_list(Len)) - ]}]. + Len = (?DICT):size(StateData#state.users), + RoomDescription = + (StateData#state.config)#config.description, + [#xmlel{name = <<"x">>, + attrs = + [{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"result">>}], + children = + [?RFIELDT(<<"hidden">>, <<"FORM_TYPE">>, + <<"http://jabber.org/protocol/muc#roominfo">>), + ?RFIELD(<<"Room description">>, + <<"muc#roominfo_description">>, RoomDescription), + ?RFIELD(<<"Number of occupants">>, + <<"muc#roominfo_occupants">>, + (iolist_to_binary(integer_to_list(Len))))]}]. process_iq_disco_items(_From, set, _Lang, _StateData) -> {error, ?ERR_NOT_ALLOWED}; - process_iq_disco_items(From, get, _Lang, StateData) -> - FAffiliation = get_affiliation(From, StateData), - FRole = get_role(From, StateData), - case ((StateData#state.config)#config.public_list == true) orelse - (FRole /= none) orelse - (FAffiliation == admin) orelse - (FAffiliation == owner) of - true -> - UList = - lists:map( - fun({_LJID, Info}) -> - Nick = Info#user.nick, - {xmlelement, "item", - [{"jid", jlib:jid_to_string( - {StateData#state.room, - StateData#state.host, - Nick})}, - {"name", Nick}], []} - end, - ?DICT:to_list(StateData#state.users)), - {result, UList, StateData}; + case (StateData#state.config)#config.public_list of + true -> + {result, get_mucroom_disco_items(StateData), StateData}; + _ -> + case is_occupant_or_admin(From, StateData) of + true -> + {result, get_mucroom_disco_items(StateData), StateData}; + _ -> {error, ?ERR_FORBIDDEN} + end + end. + +process_iq_captcha(_From, get, _Lang, _SubEl, + _StateData) -> + {error, ?ERR_NOT_ALLOWED}; +process_iq_captcha(_From, set, _Lang, SubEl, + StateData) -> + case ejabberd_captcha:process_reply(SubEl) of + ok -> {result, [], StateData}; + _ -> {error, ?ERR_NOT_ACCEPTABLE} + end. + +process_iq_vcard(_From, get, _Lang, _SubEl, StateData) -> + #state{config = #config{vcard = VCardRaw}} = StateData, + case xml_stream:parse_element(VCardRaw) of + #xmlel{children = VCardEls} -> + {result, VCardEls, StateData}; + {error, _} -> + {result, [], StateData} + end; +process_iq_vcard(From, set, Lang, SubEl, StateData) -> + case get_affiliation(From, StateData) of + owner -> + VCardRaw = xml:element_to_binary(SubEl), + Config = StateData#state.config, + NewConfig = Config#config{vcard = VCardRaw}, + change_config(NewConfig, StateData); _ -> - {error, ?ERR_FORBIDDEN} + ErrText = <<"Owner privileges required">>, + {error, ?ERRT_FORBIDDEN(Lang, ErrText)} end. get_title(StateData) -> case (StateData#state.config)#config.title of - "" -> - StateData#state.room; - Name -> - Name + <<"">> -> StateData#state.room; + Name -> Name end. +get_roomdesc_reply(JID, StateData, Tail) -> + IsOccupantOrAdmin = is_occupant_or_admin(JID, + StateData), + if (StateData#state.config)#config.public or + IsOccupantOrAdmin -> + if (StateData#state.config)#config.public_list or + IsOccupantOrAdmin -> + {item, <<(get_title(StateData))/binary,Tail/binary>>}; + true -> {item, get_title(StateData)} + end; + true -> false + end. + +get_roomdesc_tail(StateData, Lang) -> + Desc = case (StateData#state.config)#config.public of + true -> <<"">>; + _ -> translate:translate(Lang, <<"private, ">>) + end, + Len = (?DICT):fold(fun (_, _, Acc) -> Acc + 1 end, 0, + StateData#state.users), + <<" (", Desc/binary, + (iolist_to_binary(integer_to_list(Len)))/binary, ")">>. + +get_mucroom_disco_items(StateData) -> + lists:map(fun ({_LJID, Info}) -> + Nick = Info#user.nick, + #xmlel{name = <<"item">>, + attrs = + [{<<"jid">>, + jlib:jid_to_string({StateData#state.room, + StateData#state.host, + Nick})}, + {<<"name">>, Nick}], + children = []} + end, + (?DICT):to_list(StateData#state.users)). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% Voice request support + +is_voice_request(Els) -> + lists:foldl(fun (#xmlel{name = <<"x">>, attrs = Attrs} = + El, + false) -> + case xml:get_attr_s(<<"xmlns">>, Attrs) of + ?NS_XDATA -> + case jlib:parse_xdata_submit(El) of + [_ | _] = Fields -> + case {lists:keysearch(<<"FORM_TYPE">>, 1, + Fields), + lists:keysearch(<<"muc#role">>, 1, + Fields)} + of + {{value, + {_, + [<<"http://jabber.org/protocol/muc#request">>]}}, + {value, {_, [<<"participant">>]}}} -> + true; + _ -> false + end; + _ -> false + end; + _ -> false + end; + (_, Acc) -> Acc + end, + false, Els). + +prepare_request_form(Requester, Nick, Lang) -> + #xmlel{name = <<"message">>, + attrs = [{<<"type">>, <<"normal">>}], + children = + [#xmlel{name = <<"x">>, + attrs = + [{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"form">>}], + children = + [#xmlel{name = <<"title">>, attrs = [], + children = + [{xmlcdata, + translate:translate(Lang, + <<"Voice request">>)}]}, + #xmlel{name = <<"instructions">>, attrs = [], + children = + [{xmlcdata, + translate:translate(Lang, + <<"Either approve or decline the voice " + "request.">>)}]}, + #xmlel{name = <<"field">>, + attrs = + [{<<"var">>, <<"FORM_TYPE">>}, + {<<"type">>, <<"hidden">>}], + children = + [#xmlel{name = <<"value">>, attrs = [], + children = + [{xmlcdata, + <<"http://jabber.org/protocol/muc#request">>}]}]}, + #xmlel{name = <<"field">>, + attrs = + [{<<"var">>, <<"muc#role">>}, + {<<"type">>, <<"hidden">>}], + children = + [#xmlel{name = <<"value">>, attrs = [], + children = + [{xmlcdata, + <<"participant">>}]}]}, + ?STRINGXFIELD(<<"User JID">>, <<"muc#jid">>, + (jlib:jid_to_string(Requester))), + ?STRINGXFIELD(<<"Nickname">>, <<"muc#roomnick">>, + Nick), + ?BOOLXFIELD(<<"Grant voice to this person?">>, + <<"muc#request_allow">>, + (jlib:binary_to_atom(<<"false">>)))]}]}. + +send_voice_request(From, StateData) -> + Moderators = search_role(moderator, StateData), + FromNick = find_nick_by_jid(From, StateData), + lists:foreach(fun ({_, User}) -> + ejabberd_router:route(StateData#state.jid, User#user.jid, + prepare_request_form(From, FromNick, + <<"">>)) + end, + Moderators). + +is_voice_approvement(Els) -> + lists:foldl(fun (#xmlel{name = <<"x">>, attrs = Attrs} = + El, + false) -> + case xml:get_attr_s(<<"xmlns">>, Attrs) of + ?NS_XDATA -> + case jlib:parse_xdata_submit(El) of + [_ | _] = Fs -> + case {lists:keysearch(<<"FORM_TYPE">>, 1, + Fs), + lists:keysearch(<<"muc#role">>, 1, + Fs), + lists:keysearch(<<"muc#request_allow">>, + 1, Fs)} + of + {{value, + {_, + [<<"http://jabber.org/protocol/muc#request">>]}}, + {value, {_, [<<"participant">>]}}, + {value, {_, [Flag]}}} + when Flag == <<"true">>; + Flag == <<"1">> -> + true; + _ -> false + end; + _ -> false + end; + _ -> false + end; + (_, Acc) -> Acc + end, + false, Els). + +extract_jid_from_voice_approvement(Els) -> + lists:foldl(fun (#xmlel{name = <<"x">>} = El, error) -> + Fields = case jlib:parse_xdata_submit(El) of + invalid -> []; + Res -> Res + end, + lists:foldl(fun ({<<"muc#jid">>, [JIDStr]}, error) -> + case jlib:string_to_jid(JIDStr) of + error -> error; + J -> {ok, J} + end; + (_, Acc) -> Acc + end, + error, Fields); + (_, Acc) -> Acc + end, + error, Els). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Invitation support +is_invitation(Els) -> + lists:foldl(fun (#xmlel{name = <<"x">>, attrs = Attrs} = + El, + false) -> + case xml:get_attr_s(<<"xmlns">>, Attrs) of + ?NS_MUC_USER -> + case xml:get_subtag(El, <<"invite">>) of + false -> false; + _ -> true + end; + _ -> false + end; + (_, Acc) -> Acc + end, + false, Els). + check_invitation(From, Els, Lang, StateData) -> FAffiliation = get_affiliation(From, StateData), - CanInvite = (StateData#state.config)#config.allow_user_invites - orelse (FAffiliation == admin) orelse (FAffiliation == owner), + CanInvite = + (StateData#state.config)#config.allow_user_invites + orelse + FAffiliation == admin orelse FAffiliation == owner, InviteEl = case xml:remove_cdata(Els) of - [{xmlelement, "x", _Attrs1, Els1} = XEl] -> - case xml:get_tag_attr_s("xmlns", XEl) of - ?NS_MUC_USER -> - ok; - _ -> - throw({error, ?ERR_BAD_REQUEST}) - end, - case xml:remove_cdata(Els1) of - [{xmlelement, "invite", _Attrs2, _Els2} = InviteEl1] -> - InviteEl1; - _ -> - throw({error, ?ERR_BAD_REQUEST}) - end; - _ -> - throw({error, ?ERR_BAD_REQUEST}) + [#xmlel{name = <<"x">>, children = Els1} = XEl] -> + case xml:get_tag_attr_s(<<"xmlns">>, XEl) of + ?NS_MUC_USER -> ok; + _ -> throw({error, ?ERR_BAD_REQUEST}) + end, + case xml:remove_cdata(Els1) of + [#xmlel{name = <<"invite">>} = InviteEl1] -> InviteEl1; + _ -> throw({error, ?ERR_BAD_REQUEST}) + end; + _ -> throw({error, ?ERR_BAD_REQUEST}) end, - JID = case jlib:string_to_jid( - xml:get_tag_attr_s("to", InviteEl)) of - error -> - throw({error, ?ERR_JID_MALFORMED}); - JID1 -> - JID1 + JID = case + jlib:string_to_jid(xml:get_tag_attr_s(<<"to">>, + InviteEl)) + of + error -> throw({error, ?ERR_JID_MALFORMED}); + JID1 -> JID1 end, case CanInvite of - false -> - throw({error, ?ERR_NOT_ALLOWED}); - true -> - Reason = - xml:get_path_s( - InviteEl, - [{elem, "reason"}, cdata]), - IEl = - [{xmlelement, "invite", - [{"from", - jlib:jid_to_string(From)}], - [{xmlelement, "reason", [], - [{xmlcdata, Reason}]}]}], - PasswdEl = - case (StateData#state.config)#config.password_protected of - true -> - [{xmlelement, "password", [], - [{xmlcdata, (StateData#state.config)#config.password}]}]; - _ -> - [] - end, - Body = - {xmlelement, "body", [], - [{xmlcdata, - lists:flatten( - io_lib:format( - translate:translate( - Lang, - "You have been invited to ~s by ~s"), - [jlib:jid_to_string({StateData#state.room, - StateData#state.host, - ""}), - jlib:jid_to_string(From)])) ++ - case (StateData#state.config)#config.password_protected of + false -> throw({error, ?ERR_NOT_ALLOWED}); + true -> + Reason = xml:get_path_s(InviteEl, + [{elem, <<"reason">>}, cdata]), + ContinueEl = case xml:get_path_s(InviteEl, + [{elem, <<"continue">>}]) + of + <<>> -> []; + Continue1 -> [Continue1] + end, + IEl = [#xmlel{name = <<"invite">>, + attrs = [{<<"from">>, jlib:jid_to_string(From)}], + children = + [#xmlel{name = <<"reason">>, attrs = [], + children = [{xmlcdata, Reason}]}] + ++ ContinueEl}], + PasswdEl = case + (StateData#state.config)#config.password_protected + of true -> - ", " ++ - translate:translate(Lang, "the password is") ++ - " '" ++ - (StateData#state.config)#config.password ++ "'"; - _ -> - "" - end ++ - case Reason of - "" -> ""; - _ -> " (" ++ Reason ++ ") " - end - }]}, - Msg = - {xmlelement, "message", - [{"type", "normal"}], - [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}], IEl ++ PasswdEl}, - {xmlelement, "x", - [{"xmlns", ?NS_XCONFERENCE}, - {"jid", jlib:jid_to_string( - {StateData#state.room, - StateData#state.host, - ""})}], - [{xmlcdata, Reason}]}, - Body]}, - ejabberd_router:route(StateData#state.jid, JID, Msg), - JID + [#xmlel{name = <<"password">>, attrs = [], + children = + [{xmlcdata, + (StateData#state.config)#config.password}]}]; + _ -> [] + end, + Body = #xmlel{name = <<"body">>, attrs = [], + children = + [{xmlcdata, + iolist_to_binary( + [io_lib:format( + translate:translate( + Lang, + <<"~s invites you to the room ~s">>), + [jlib:jid_to_string(From), + jlib:jid_to_string({StateData#state.room, + StateData#state.host, + <<"">>})]), + + case + (StateData#state.config)#config.password_protected + of + true -> + <<", ", + (translate:translate(Lang, + <<"the password is">>))/binary, + " '", + ((StateData#state.config)#config.password)/binary, + "'">>; + _ -> <<"">> + end + , + case Reason of + <<"">> -> <<"">>; + _ -> <<" (", Reason/binary, ") ">> + end])}]}, + Msg = #xmlel{name = <<"message">>, + attrs = [{<<"type">>, <<"normal">>}], + children = + [#xmlel{name = <<"x">>, + attrs = [{<<"xmlns">>, ?NS_MUC_USER}], + children = IEl ++ PasswdEl}, + #xmlel{name = <<"x">>, + attrs = + [{<<"xmlns">>, ?NS_XCONFERENCE}, + {<<"jid">>, + jlib:jid_to_string({StateData#state.room, + StateData#state.host, + <<"">>})}], + children = [{xmlcdata, Reason}]}, + Body]}, + ejabberd_router:route(StateData#state.jid, JID, Msg), + JID end. +%% Handle a message sent to the room by a non-participant. +%% If it is a decline, send to the inviter. +%% Otherwise, an error message is sent to the sender. +handle_roommessage_from_nonparticipant(Packet, Lang, + StateData, From) -> + case catch check_decline_invitation(Packet) of + {true, Decline_data} -> + send_decline_invitation(Decline_data, + StateData#state.jid, From); + _ -> + send_error_only_occupants(Packet, Lang, + StateData#state.jid, From) + end. + +%% Check in the packet is a decline. +%% If so, also returns the splitted packet. +%% This function must be catched, +%% because it crashes when the packet is not a decline message. +check_decline_invitation(Packet) -> + #xmlel{name = <<"message">>} = Packet, + XEl = xml:get_subtag(Packet, <<"x">>), + (?NS_MUC_USER) = xml:get_tag_attr_s(<<"xmlns">>, XEl), + DEl = xml:get_subtag(XEl, <<"decline">>), + ToString = xml:get_tag_attr_s(<<"to">>, DEl), + ToJID = jlib:string_to_jid(ToString), + {true, {Packet, XEl, DEl, ToJID}}. + +%% Send the decline to the inviter user. +%% The original stanza must be slightly modified. +send_decline_invitation({Packet, XEl, DEl, ToJID}, + RoomJID, FromJID) -> + FromString = + jlib:jid_to_string(jlib:jid_remove_resource(FromJID)), + #xmlel{name = <<"decline">>, attrs = DAttrs, + children = DEls} = + DEl, + DAttrs2 = lists:keydelete(<<"to">>, 1, DAttrs), + DAttrs3 = [{<<"from">>, FromString} | DAttrs2], + DEl2 = #xmlel{name = <<"decline">>, attrs = DAttrs3, + children = DEls}, + XEl2 = replace_subelement(XEl, DEl2), + Packet2 = replace_subelement(Packet, XEl2), + ejabberd_router:route(RoomJID, ToJID, Packet2). + +%% Given an element and a new subelement, +%% replace the instance of the subelement in element with the new subelement. +replace_subelement(#xmlel{name = Name, attrs = Attrs, + children = SubEls}, + NewSubEl) -> + {_, NameNewSubEl, _, _} = NewSubEl, + SubEls2 = lists:keyreplace(NameNewSubEl, 2, SubEls, NewSubEl), + #xmlel{name = Name, attrs = Attrs, children = SubEls2}. + +send_error_only_occupants(Packet, Lang, RoomJID, From) -> + ErrText = + <<"Only occupants are allowed to send messages " + "to the conference">>, + Err = jlib:make_error_reply(Packet, + ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)), + ejabberd_router:route(RoomJID, From, Err). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Logging +add_to_log(Type, Data, StateData) + when Type == roomconfig_change_disabledlogging -> + mod_muc_log:add_to_log(StateData#state.server_host, + roomconfig_change, Data, StateData#state.jid, + make_opts(StateData)); add_to_log(Type, Data, StateData) -> case (StateData#state.config)#config.logging of - true -> - mod_muc_log:add_to_log( - StateData#state.server_host, Type, Data, - StateData#state.jid, make_opts(StateData)); - false -> - ok + true -> + mod_muc_log:add_to_log(StateData#state.server_host, + Type, Data, StateData#state.jid, + make_opts(StateData)); + false -> ok end. +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Users number checking + +tab_add_online_user(JID, StateData) -> + {LUser, LServer, LResource} = jlib:jid_tolower(JID), + US = {LUser, LServer}, + Room = StateData#state.room, + Host = StateData#state.host, + catch ets:insert(muc_online_users, + #muc_online_users{us = US, resource = LResource, + room = Room, host = Host}). + +tab_remove_online_user(JID, StateData) -> + {LUser, LServer, LResource} = jlib:jid_tolower(JID), + US = {LUser, LServer}, + Room = StateData#state.room, + Host = StateData#state.host, + catch ets:delete_object(muc_online_users, + #muc_online_users{us = US, resource = LResource, + room = Room, host = Host}). + +tab_count_user(JID) -> + {LUser, LServer, _} = jlib:jid_tolower(JID), + US = {LUser, LServer}, + case catch ets:select(muc_online_users, + [{#muc_online_users{us = US, _ = '_'}, [], [[]]}]) + of + Res when is_list(Res) -> length(Res); + _ -> 0 + end. + +element_size(El) -> + byte_size(xml:element_to_binary(El)). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% Multicast +%% Detect messange stanzas that don't have meaninful content -send_multiple(From, Server, Users, Packet) -> - JIDs = [ User#user.jid || {_, User} <- ?DICT:to_list(Users)], - ejabberd_router_multicast:route_multicast(From, Server, JIDs, Packet). +has_body_or_subject(Packet) -> + [] /= lists:dropwhile(fun + (#xmlel{name = <<"body">>}) -> false; + (#xmlel{name = <<"subject">>}) -> false; + (_) -> true + end, Packet#xmlel.children).