From 4d8f7706240a1603468968f47fc7b150b788d62f Mon Sep 17 00:00:00 2001 From: Evgeniy Khramtsov Date: Mon, 8 Apr 2013 11:12:54 +0200 Subject: Switch to rebar build tool Use dynamic Rebar configuration Make iconv dependency optional Disable transient_supervisors compile option Add hipe compilation support Only compile ibrowse and lhttpc when needed Make it possible to generate an OTP application release Add --enable-debug compile option Add --enable-all compiler option Add --enable-tools configure option Add --with-erlang configure option. Add --enable-erlang-version-check configure option. Add lager support Improve the test suite --- src/mod_muc.erl | 1128 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1128 insertions(+) create mode 100644 src/mod_muc.erl (limited to 'src/mod_muc.erl') diff --git a/src/mod_muc.erl b/src/mod_muc.erl new file mode 100644 index 000000000..05becd20b --- /dev/null +++ b/src/mod_muc.erl @@ -0,0 +1,1128 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_muc.erl +%%% Author : Alexey Shchepin +%%% Purpose : MUC support (XEP-0045) +%%% Created : 19 Mar 2003 by Alexey Shchepin +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2013 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., 59 Temple Place, Suite 330, Boston, MA +%%% 02111-1307 USA +%%% +%%%---------------------------------------------------------------------- + +-module(mod_muc). + +-author('alexey@process-one.net'). + +-behaviour(gen_server). + +-behaviour(gen_mod). + +%% API +-export([start_link/2, + start/2, + stop/1, + room_destroyed/4, + store_room/4, + restore_room/3, + forget_room/3, + create_room/5, + process_iq_disco_items/4, + broadcast_service_message/2, + can_use_nick/4]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, + handle_info/2, terminate/2, code_change/3]). + +-include("ejabberd.hrl"). +-include("logger.hrl"). + +-include("jlib.hrl"). + +-record(muc_room, {name_host = {<<"">>, <<"">>} :: {binary(), binary()} | + {'_', binary()}, + opts = [] :: list() | '_'}). + +-record(muc_online_room, + {name_host = {<<"">>, <<"">>} :: {binary(), binary()} | '$1', + pid = self() :: pid() | '$2' | '_'}). + +-record(muc_registered, + {us_host = {{<<"">>, <<"">>}, <<"">>} :: {{binary(), binary()}, binary()} | '$1', + nick = <<"">> :: binary()}). + +-record(state, + {host = <<"">> :: binary(), + server_host = <<"">> :: binary(), + access = {none, none, none, none} :: {atom(), atom(), atom(), atom()}, + history_size = 20 :: non_neg_integer(), + default_room_opts = [] :: list(), + room_shaper = none :: shaper:shaper()}). + +-define(PROCNAME, ejabberd_mod_muc). + +%%==================================================================== +%% API +%%==================================================================== +%%-------------------------------------------------------------------- +%% Function: start_link() -> {ok,Pid} | ignore | {error,Error} +%% Description: Starts the server +%%-------------------------------------------------------------------- +start_link(Host, Opts) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + gen_server:start_link({local, Proc}, ?MODULE, + [Host, Opts], []). + +start(Host, Opts) -> + start_supervisor(Host), + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]}, + temporary, 1000, worker, [?MODULE]}, + supervisor:start_child(ejabberd_sup, ChildSpec). + +stop(Host) -> + stop_supervisor(Host), + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + gen_server:call(Proc, stop), + supervisor:delete_child(ejabberd_sup, Proc). + +%% This function is called by a room in three situations: +%% A) The owner of the room destroyed it +%% B) The only participant of a temporary room leaves it +%% C) mod_muc:stop was called, and each room is being terminated +%% In this case, the mod_muc process died before the room processes +%% So the message sending must be catched +room_destroyed(Host, Room, Pid, ServerHost) -> + catch gen_mod:get_module_proc(ServerHost, ?PROCNAME) ! + {room_destroyed, {Room, Host}, Pid}, + ok. + +%% @doc Create a room. +%% If Opts = default, the default room options are used. +%% Else use the passed options as defined in mod_muc_room. +create_room(Host, Name, From, Nick, Opts) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + gen_server:call(Proc, {create, Name, From, Nick, Opts}). + +store_room(ServerHost, Host, Name, Opts) -> + LServer = jlib:nameprep(ServerHost), + store_room(LServer, Host, Name, Opts, + gen_mod:db_type(LServer, ?MODULE)). + +store_room(_LServer, Host, Name, Opts, mnesia) -> + F = fun () -> + mnesia:write(#muc_room{name_host = {Name, Host}, + opts = Opts}) + end, + mnesia:transaction(F); +store_room(LServer, Host, Name, Opts, odbc) -> + SName = ejabberd_odbc:escape(Name), + SHost = ejabberd_odbc:escape(Host), + SOpts = ejabberd_odbc:encode_term(Opts), + F = fun () -> + odbc_queries:update_t(<<"muc_room">>, + [<<"name">>, <<"host">>, <<"opts">>], + [SName, SHost, SOpts], + [<<"name='">>, SName, <<"' and host='">>, + SHost, <<"'">>]) + end, + ejabberd_odbc:sql_transaction(LServer, F). + +restore_room(ServerHost, Host, Name) -> + LServer = jlib:nameprep(ServerHost), + restore_room(LServer, Host, Name, + gen_mod:db_type(LServer, ?MODULE)). + +restore_room(_LServer, Host, Name, mnesia) -> + case catch mnesia:dirty_read(muc_room, {Name, Host}) of + [#muc_room{opts = Opts}] -> Opts; + _ -> error + end; +restore_room(LServer, Host, Name, odbc) -> + SName = ejabberd_odbc:escape(Name), + SHost = ejabberd_odbc:escape(Host), + case catch ejabberd_odbc:sql_query(LServer, + [<<"select opts from muc_room where name='">>, + SName, <<"' and host='">>, SHost, + <<"';">>]) + of + {selected, [<<"opts">>], [[Opts]]} -> + opts_to_binary(ejabberd_odbc:decode_term(Opts)); + _ -> error + end. + +forget_room(ServerHost, Host, Name) -> + LServer = jlib:nameprep(ServerHost), + forget_room(LServer, Host, Name, + gen_mod:db_type(LServer, ?MODULE)). + +forget_room(_LServer, Host, Name, mnesia) -> + F = fun () -> mnesia:delete({muc_room, {Name, Host}}) + end, + mnesia:transaction(F); +forget_room(LServer, Host, Name, odbc) -> + SName = ejabberd_odbc:escape(Name), + SHost = ejabberd_odbc:escape(Host), + F = fun () -> + ejabberd_odbc:sql_query_t([<<"delete from muc_room where name='">>, + SName, <<"' and host='">>, SHost, + <<"';">>]) + end, + ejabberd_odbc:sql_transaction(LServer, F). + +process_iq_disco_items(Host, From, To, + #iq{lang = Lang} = IQ) -> + Rsm = jlib:rsm_decode(IQ), + Res = IQ#iq{type = result, + sub_el = + [#xmlel{name = <<"query">>, + attrs = [{<<"xmlns">>, ?NS_DISCO_ITEMS}], + children = iq_disco_items(Host, From, Lang, Rsm)}]}, + ejabberd_router:route(To, From, jlib:iq_to_xml(Res)). + +can_use_nick(_ServerHost, _Host, _JID, <<"">>) -> false; +can_use_nick(ServerHost, Host, JID, Nick) -> + LServer = jlib:nameprep(ServerHost), + can_use_nick(LServer, Host, JID, Nick, + gen_mod:db_type(LServer, ?MODULE)). + +can_use_nick(_LServer, Host, JID, Nick, mnesia) -> + {LUser, LServer, _} = jlib:jid_tolower(JID), + LUS = {LUser, LServer}, + case catch mnesia:dirty_select(muc_registered, + [{#muc_registered{us_host = '$1', + nick = Nick, _ = '_'}, + [{'==', {element, 2, '$1'}, Host}], + ['$_']}]) + of + {'EXIT', _Reason} -> true; + [] -> true; + [#muc_registered{us_host = {U, _Host}}] -> U == LUS + end; +can_use_nick(LServer, Host, JID, Nick, odbc) -> + SJID = + jlib:jid_to_string(jlib:jid_tolower(jlib:jid_remove_resource(JID))), + SNick = ejabberd_odbc:escape(Nick), + SHost = ejabberd_odbc:escape(Host), + case catch ejabberd_odbc:sql_query(LServer, + [<<"select jid from muc_registered ">>, + <<"where nick='">>, SNick, + <<"' and host='">>, SHost, <<"';">>]) + of + {selected, [<<"jid">>], [[SJID1]]} -> SJID == SJID1; + _ -> true + end. + +%%==================================================================== +%% gen_server callbacks +%%==================================================================== + +%%-------------------------------------------------------------------- +%% Function: init(Args) -> {ok, State} | +%% {ok, State, Timeout} | +%% ignore | +%% {stop, Reason} +%% Description: Initiates the server +%%-------------------------------------------------------------------- +init([Host, Opts]) -> + MyHost = gen_mod:get_opt_host(Host, Opts, + <<"conference.@HOST@">>), + case gen_mod:db_type(Opts) of + mnesia -> + mnesia:create_table(muc_room, + [{disc_copies, [node()]}, + {attributes, + record_info(fields, muc_room)}]), + mnesia:create_table(muc_registered, + [{disc_copies, [node()]}, + {attributes, + record_info(fields, muc_registered)}]), + update_tables(MyHost), + mnesia:add_table_index(muc_registered, nick); + _ -> + ok + end, + mnesia:create_table(muc_online_room, + [{ram_copies, [node()]}, + {attributes, record_info(fields, muc_online_room)}]), + mnesia:add_table_copy(muc_online_room, node(), ram_copies), + catch ets:new(muc_online_users, [bag, named_table, public, {keypos, 2}]), + clean_table_from_bad_node(node(), MyHost), + mnesia:subscribe(system), + Access = gen_mod:get_opt(access, Opts, fun(A) -> A end, all), + AccessCreate = gen_mod:get_opt(access_create, Opts, fun(A) -> A end, all), + AccessAdmin = gen_mod:get_opt(access_admin, Opts, fun(A) -> A end, none), + AccessPersistent = gen_mod:get_opt(access_persistent, Opts, fun(A) -> A end, all), + HistorySize = gen_mod:get_opt(history_size, Opts, fun(A) -> A end, 20), + DefRoomOpts = gen_mod:get_opt(default_room_options, Opts, fun(A) -> A end, []), + RoomShaper = gen_mod:get_opt(room_shaper, Opts, fun(A) -> A end, none), + ejabberd_router:register_route(MyHost), + load_permanent_rooms(MyHost, Host, + {Access, AccessCreate, AccessAdmin, AccessPersistent}, + HistorySize, + RoomShaper), + {ok, #state{host = MyHost, + server_host = Host, + access = {Access, AccessCreate, AccessAdmin, AccessPersistent}, + default_room_opts = DefRoomOpts, + history_size = HistorySize, + room_shaper = RoomShaper}}. + +%%-------------------------------------------------------------------- +%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | +%% {reply, Reply, State, Timeout} | +%% {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, Reply, State} | +%% {stop, Reason, State} +%% Description: Handling call messages +%%-------------------------------------------------------------------- +handle_call(stop, _From, State) -> + {stop, normal, ok, State}; +handle_call({create, Room, From, Nick, Opts}, _From, + #state{host = Host, server_host = ServerHost, + access = Access, default_room_opts = DefOpts, + history_size = HistorySize, + room_shaper = RoomShaper} = State) -> + ?DEBUG("MUC: create new room '~s'~n", [Room]), + NewOpts = case Opts of + default -> DefOpts; + _ -> Opts + end, + {ok, Pid} = mod_muc_room:start( + Host, ServerHost, Access, + Room, HistorySize, + RoomShaper, From, + Nick, NewOpts), + register_room(Host, Room, Pid), + {reply, ok, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_cast(Msg, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handling cast messages +%%-------------------------------------------------------------------- +handle_cast(_Msg, State) -> {noreply, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_info(Info, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handling all non call/cast messages +%%-------------------------------------------------------------------- +handle_info({route, From, To, Packet}, + #state{host = Host, server_host = ServerHost, + access = Access, default_room_opts = DefRoomOpts, + history_size = HistorySize, + room_shaper = RoomShaper} = State) -> + case catch do_route(Host, ServerHost, Access, HistorySize, RoomShaper, + From, To, Packet, DefRoomOpts) of + {'EXIT', Reason} -> + ?ERROR_MSG("~p", [Reason]); + _ -> + ok + end, + {noreply, State}; +handle_info({room_destroyed, RoomHost, Pid}, State) -> + F = fun () -> + mnesia:delete_object(#muc_online_room{name_host = + RoomHost, + pid = Pid}) + end, + mnesia:transaction(F), + {noreply, State}; +handle_info({mnesia_system_event, {mnesia_down, Node}}, State) -> + clean_table_from_bad_node(Node), + {noreply, State}; +handle_info(_Info, State) -> {noreply, State}. + +%%-------------------------------------------------------------------- +%% Function: terminate(Reason, State) -> void() +%% Description: This function is called by a gen_server when it is about to +%% terminate. It should be the opposite of Module:init/1 and do any necessary +%% cleaning up. When it returns, the gen_server terminates with Reason. +%% The return value is ignored. +%%-------------------------------------------------------------------- +terminate(_Reason, State) -> + ejabberd_router:unregister_route(State#state.host), + ok. + +%%-------------------------------------------------------------------- +%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} +%% Description: Convert process state when code is changed +%%-------------------------------------------------------------------- +code_change(_OldVsn, State, _Extra) -> {ok, State}. + +%%-------------------------------------------------------------------- +%%% Internal functions +%%-------------------------------------------------------------------- +start_supervisor(Host) -> + Proc = gen_mod:get_module_proc(Host, + ejabberd_mod_muc_sup), + ChildSpec = {Proc, + {ejabberd_tmp_sup, start_link, [Proc, mod_muc_room]}, + permanent, infinity, supervisor, [ejabberd_tmp_sup]}, + supervisor:start_child(ejabberd_sup, ChildSpec). + +stop_supervisor(Host) -> + Proc = gen_mod:get_module_proc(Host, + ejabberd_mod_muc_sup), + supervisor:terminate_child(ejabberd_sup, Proc), + supervisor:delete_child(ejabberd_sup, Proc). + +do_route(Host, ServerHost, Access, HistorySize, RoomShaper, + From, To, Packet, DefRoomOpts) -> + {AccessRoute, _AccessCreate, _AccessAdmin, _AccessPersistent} = Access, + case acl:match_rule(ServerHost, AccessRoute, From) of + allow -> + do_route1(Host, ServerHost, Access, HistorySize, RoomShaper, + From, To, Packet, DefRoomOpts); + _ -> + #xmlel{attrs = Attrs} = Packet, + Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), + ErrText = <<"Access denied by service policy">>, + Err = jlib:make_error_reply(Packet, + ?ERRT_FORBIDDEN(Lang, ErrText)), + ejabberd_router:route_error(To, From, Err, Packet) + end. + + +do_route1(Host, ServerHost, Access, HistorySize, RoomShaper, + From, To, Packet, DefRoomOpts) -> + {_AccessRoute, AccessCreate, AccessAdmin, _AccessPersistent} = Access, + {Room, _, Nick} = jlib:jid_tolower(To), + #xmlel{name = Name, attrs = Attrs} = Packet, + case Room of + <<"">> -> + case Nick of + <<"">> -> + case Name of + <<"iq">> -> + case jlib:iq_query_info(Packet) of + #iq{type = get, xmlns = (?NS_DISCO_INFO) = XMLNS, + sub_el = _SubEl, lang = Lang} = + IQ -> + Info = ejabberd_hooks:run_fold(disco_info, + ServerHost, [], + [ServerHost, ?MODULE, + <<"">>, <<"">>]), + Res = IQ#iq{type = result, + sub_el = + [#xmlel{name = <<"query">>, + attrs = + [{<<"xmlns">>, XMLNS}], + children = + iq_disco_info(Lang) ++ + Info}]}, + ejabberd_router:route(To, From, + jlib:iq_to_xml(Res)); + #iq{type = get, xmlns = ?NS_DISCO_ITEMS} = IQ -> + spawn(?MODULE, process_iq_disco_items, + [Host, From, To, IQ]); + #iq{type = get, xmlns = (?NS_REGISTER) = XMLNS, + lang = Lang, sub_el = _SubEl} = + IQ -> + Res = IQ#iq{type = result, + sub_el = + [#xmlel{name = <<"query">>, + attrs = + [{<<"xmlns">>, XMLNS}], + children = + iq_get_register_info(ServerHost, + Host, + From, + Lang)}]}, + ejabberd_router:route(To, From, + jlib:iq_to_xml(Res)); + #iq{type = set, xmlns = (?NS_REGISTER) = XMLNS, + lang = Lang, sub_el = SubEl} = + IQ -> + case process_iq_register_set(ServerHost, Host, From, + SubEl, Lang) + of + {result, IQRes} -> + Res = IQ#iq{type = result, + sub_el = + [#xmlel{name = <<"query">>, + attrs = + [{<<"xmlns">>, + XMLNS}], + children = IQRes}]}, + ejabberd_router:route(To, From, + jlib:iq_to_xml(Res)); + {error, Error} -> + Err = jlib:make_error_reply(Packet, Error), + ejabberd_router:route(To, From, Err) + end; + #iq{type = get, xmlns = (?NS_VCARD) = XMLNS, + lang = Lang, sub_el = _SubEl} = + IQ -> + Res = IQ#iq{type = result, + sub_el = + [#xmlel{name = <<"vCard">>, + attrs = + [{<<"xmlns">>, XMLNS}], + children = + iq_get_vcard(Lang)}]}, + ejabberd_router:route(To, From, + jlib:iq_to_xml(Res)); + #iq{type = get, xmlns = ?NS_MUC_UNIQUE} = IQ -> + Res = IQ#iq{type = result, + sub_el = + [#xmlel{name = <<"unique">>, + attrs = + [{<<"xmlns">>, + ?NS_MUC_UNIQUE}], + children = + [iq_get_unique(From)]}]}, + ejabberd_router:route(To, From, + jlib:iq_to_xml(Res)); + #iq{} -> + Err = jlib:make_error_reply(Packet, + ?ERR_FEATURE_NOT_IMPLEMENTED), + ejabberd_router:route(To, From, Err); + _ -> ok + end; + <<"message">> -> + case xml:get_attr_s(<<"type">>, Attrs) of + <<"error">> -> ok; + _ -> + case acl:match_rule(ServerHost, AccessAdmin, From) + of + allow -> + Msg = xml:get_path_s(Packet, + [{elem, <<"body">>}, + cdata]), + broadcast_service_message(Host, Msg); + _ -> + Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), + ErrText = + <<"Only service administrators are allowed " + "to send service messages">>, + Err = jlib:make_error_reply(Packet, + ?ERRT_FORBIDDEN(Lang, + ErrText)), + ejabberd_router:route(To, From, Err) + end + end; + <<"presence">> -> ok + end; + _ -> + case xml:get_attr_s(<<"type">>, Attrs) of + <<"error">> -> ok; + <<"result">> -> ok; + _ -> + Err = jlib:make_error_reply( + Packet, ?ERR_ITEM_NOT_FOUND), + ejabberd_router:route(To, From, Err) + end + end; + _ -> + case mnesia:dirty_read(muc_online_room, {Room, Host}) of + [] -> + Type = xml:get_attr_s(<<"type">>, Attrs), + case {Name, Type} of + {<<"presence">>, <<"">>} -> + case check_user_can_create_room(ServerHost, + AccessCreate, From, + Room) of + true -> + {ok, Pid} = start_new_room( + Host, ServerHost, Access, + Room, HistorySize, + RoomShaper, From, + Nick, DefRoomOpts), + register_room(Host, Room, Pid), + mod_muc_room:route(Pid, From, Nick, Packet), + ok; + false -> + Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), + ErrText = <<"Room creation is denied by service policy">>, + Err = jlib:make_error_reply( + Packet, ?ERRT_FORBIDDEN(Lang, ErrText)), + ejabberd_router:route(To, From, Err) + end; + _ -> + Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), + ErrText = <<"Conference room does not exist">>, + Err = jlib:make_error_reply(Packet, + ?ERRT_ITEM_NOT_FOUND(Lang, + ErrText)), + ejabberd_router:route(To, From, Err) + end; + [R] -> + Pid = R#muc_online_room.pid, + ?DEBUG("MUC: send to process ~p~n", [Pid]), + mod_muc_room:route(Pid, From, Nick, Packet), + ok + end + end. + +check_user_can_create_room(ServerHost, AccessCreate, + From, RoomID) -> + case acl:match_rule(ServerHost, AccessCreate, From) of + allow -> + byte_size(RoomID) =< + gen_mod:get_module_opt(ServerHost, ?MODULE, max_room_id, + fun(infinity) -> infinity; + (I) when is_integer(I), I>0 -> I + end, infinity); + _ -> false + end. + +get_rooms(ServerHost, Host) -> + LServer = jlib:nameprep(ServerHost), + get_rooms(LServer, Host, + gen_mod:db_type(LServer, ?MODULE)). + +get_rooms(_LServer, Host, mnesia) -> + case catch mnesia:dirty_select(muc_room, + [{#muc_room{name_host = {'_', Host}, + _ = '_'}, + [], ['$_']}]) + of + {'EXIT', Reason} -> ?ERROR_MSG("~p", [Reason]), []; + Rs -> Rs + end; +get_rooms(LServer, Host, odbc) -> + SHost = ejabberd_odbc:escape(Host), + case catch ejabberd_odbc:sql_query(LServer, + [<<"select name, opts from muc_room ">>, + <<"where host='">>, SHost, <<"';">>]) + of + {'EXIT', Reason} -> ?ERROR_MSG("~p", [Reason]), []; + {selected, [<<"name">>, <<"opts">>], RoomOpts} -> + lists:map(fun ([Room, Opts]) -> + #muc_room{name_host = {Room, Host}, + opts = opts_to_binary( + ejabberd_odbc:decode_term(Opts))} + end, + RoomOpts) + end. + +load_permanent_rooms(Host, ServerHost, Access, HistorySize, RoomShaper) -> + lists:foreach( + fun(R) -> + {Room, Host} = R#muc_room.name_host, + case mnesia:dirty_read(muc_online_room, {Room, Host}) of + [] -> + {ok, Pid} = mod_muc_room:start( + Host, + ServerHost, + Access, + Room, + HistorySize, + RoomShaper, + R#muc_room.opts), + register_room(Host, Room, Pid); + _ -> + ok + end + end, get_rooms(ServerHost, Host)). + +start_new_room(Host, ServerHost, Access, Room, + HistorySize, RoomShaper, From, + Nick, DefRoomOpts) -> + case restore_room(ServerHost, Room, Host) of + error -> + ?DEBUG("MUC: open new room '~s'~n", [Room]), + mod_muc_room:start(Host, ServerHost, Access, + Room, HistorySize, + RoomShaper, From, + Nick, DefRoomOpts); + Opts -> + ?DEBUG("MUC: restore room '~s'~n", [Room]), + mod_muc_room:start(Host, ServerHost, Access, + Room, HistorySize, + RoomShaper, Opts) + end. + +register_room(Host, Room, Pid) -> + F = fun() -> + mnesia:write(#muc_online_room{name_host = {Room, Host}, + pid = Pid}) + end, + mnesia:transaction(F). + + +iq_disco_info(Lang) -> + [#xmlel{name = <<"identity">>, + attrs = + [{<<"category">>, <<"conference">>}, + {<<"type">>, <<"text">>}, + {<<"name">>, + translate:translate(Lang, <<"Chatrooms">>)}], + children = []}, + #xmlel{name = <<"feature">>, + attrs = [{<<"var">>, ?NS_DISCO_INFO}], children = []}, + #xmlel{name = <<"feature">>, + attrs = [{<<"var">>, ?NS_DISCO_ITEMS}], children = []}, + #xmlel{name = <<"feature">>, + attrs = [{<<"var">>, ?NS_MUC}], children = []}, + #xmlel{name = <<"feature">>, + attrs = [{<<"var">>, ?NS_MUC_UNIQUE}], children = []}, + #xmlel{name = <<"feature">>, + attrs = [{<<"var">>, ?NS_REGISTER}], children = []}, + #xmlel{name = <<"feature">>, + attrs = [{<<"var">>, ?NS_RSM}], children = []}, + #xmlel{name = <<"feature">>, + attrs = [{<<"var">>, ?NS_VCARD}], children = []}]. + +iq_disco_items(Host, From, Lang, none) -> + lists:zf(fun (#muc_online_room{name_host = + {Name, _Host}, + pid = Pid}) -> + case catch gen_fsm:sync_send_all_state_event(Pid, + {get_disco_item, + From, Lang}, + 100) + of + {item, Desc} -> + flush(), + {true, + #xmlel{name = <<"item">>, + attrs = + [{<<"jid">>, + jlib:jid_to_string({Name, Host, + <<"">>})}, + {<<"name">>, Desc}], + children = []}}; + _ -> false + end + end, get_vh_rooms(Host)); + +iq_disco_items(Host, From, Lang, Rsm) -> + {Rooms, RsmO} = get_vh_rooms(Host, Rsm), + RsmOut = jlib:rsm_encode(RsmO), + lists:zf(fun (#muc_online_room{name_host = + {Name, _Host}, + pid = Pid}) -> + case catch gen_fsm:sync_send_all_state_event(Pid, + {get_disco_item, + From, Lang}, + 100) + of + {item, Desc} -> + flush(), + {true, + #xmlel{name = <<"item">>, + attrs = + [{<<"jid">>, + jlib:jid_to_string({Name, Host, + <<"">>})}, + {<<"name">>, Desc}], + children = []}}; + _ -> false + end + end, + Rooms) + ++ RsmOut. + +get_vh_rooms(Host, #rsm_in{max=M, direction=Direction, id=I, index=Index})-> + AllRooms = lists:sort(get_vh_rooms(Host)), + Count = erlang:length(AllRooms), + Guard = case Direction of + _ when Index =/= undefined -> [{'==', {element, 2, '$1'}, Host}]; + aft -> [{'==', {element, 2, '$1'}, Host}, {'>=',{element, 1, '$1'} ,I}]; + before when I =/= []-> [{'==', {element, 2, '$1'}, Host}, {'=<',{element, 1, '$1'} ,I}]; + _ -> [{'==', {element, 2, '$1'}, Host}] + end, + L = lists:sort( + mnesia:dirty_select(muc_online_room, + [{#muc_online_room{name_host = '$1', _ = '_'}, + Guard, + ['$_']}])), + L2 = if + Index == undefined andalso Direction == before -> + lists:reverse(lists:sublist(lists:reverse(L), 1, M)); + Index == undefined -> + lists:sublist(L, 1, M); + Index > Count orelse Index < 0 -> + []; + true -> + lists:sublist(L, Index+1, M) + end, + if L2 == [] -> {L2, #rsm_out{count = Count}}; + true -> + H = hd(L2), + NewIndex = get_room_pos(H, AllRooms), + T = lists:last(L2), + {F, _} = H#muc_online_room.name_host, + {Last, _} = T#muc_online_room.name_host, + {L2, + #rsm_out{first = F, last = Last, count = Count, + index = NewIndex}} + end. + +%% @doc Return the position of desired room in the list of rooms. +%% The room must exist in the list. The count starts in 0. +%% @spec (Desired::muc_online_room(), Rooms::[muc_online_room()]) -> integer() +get_room_pos(Desired, Rooms) -> + get_room_pos(Desired, Rooms, 0). + +get_room_pos(Desired, [HeadRoom | _], HeadPosition) + when Desired#muc_online_room.name_host == + HeadRoom#muc_online_room.name_host -> + HeadPosition; +get_room_pos(Desired, [_ | Rooms], HeadPosition) -> + get_room_pos(Desired, Rooms, HeadPosition + 1). + +flush() -> receive _ -> flush() after 0 -> ok end. + +-define(XFIELD(Type, Label, Var, Val), +%% @doc Get a pseudo unique Room Name. The Room Name is generated as a hash of +%% the requester JID, the local time and a random salt. +%% +%% "pseudo" because we don't verify that there is not a room +%% with the returned Name already created, nor mark the generated Name +%% as "already used". But in practice, it is unique enough. See +%% http://xmpp.org/extensions/xep-0045.html#createroom-unique + #xmlel{name = <<"field">>, + attrs = + [{<<"type">>, Type}, + {<<"label">>, translate:translate(Lang, Label)}, + {<<"var">>, Var}], + children = + [#xmlel{name = <<"value">>, attrs = [], + children = [{xmlcdata, Val}]}]}). + +iq_get_unique(From) -> + {xmlcdata, + sha:sha(term_to_binary([From, now(), + randoms:get_string()]))}. + +get_nick(ServerHost, Host, From) -> + LServer = jlib:nameprep(ServerHost), + get_nick(LServer, Host, From, + gen_mod:db_type(LServer, ?MODULE)). + +get_nick(_LServer, Host, From, mnesia) -> + {LUser, LServer, _} = jlib:jid_tolower(From), + LUS = {LUser, LServer}, + case catch mnesia:dirty_read(muc_registered, + {LUS, Host}) + of + {'EXIT', _Reason} -> error; + [] -> error; + [#muc_registered{nick = Nick}] -> Nick + end; +get_nick(LServer, Host, From, odbc) -> + SJID = + ejabberd_odbc:escape(jlib:jid_to_string(jlib:jid_tolower(jlib:jid_remove_resource(From)))), + SHost = ejabberd_odbc:escape(Host), + case catch ejabberd_odbc:sql_query(LServer, + [<<"select nick from muc_registered where " + "jid='">>, + SJID, <<"' and host='">>, SHost, + <<"';">>]) + of + {selected, [<<"nick">>], [[Nick]]} -> Nick; + _ -> error + end. + +iq_get_register_info(ServerHost, Host, From, Lang) -> + {Nick, Registered} = case get_nick(ServerHost, Host, + From) + of + error -> {<<"">>, []}; + N -> + {N, + [#xmlel{name = <<"registered">>, attrs = [], + children = []}]} + end, + Registered ++ + [#xmlel{name = <<"instructions">>, attrs = [], + children = + [{xmlcdata, + translate:translate(Lang, + <<"You need a client that supports x:data " + "to register the nickname">>)}]}, + #xmlel{name = <<"x">>, + attrs = [{<<"xmlns">>, ?NS_XDATA}], + children = + [#xmlel{name = <<"title">>, attrs = [], + children = + [{xmlcdata, + <<(translate:translate(Lang, + <<"Nickname Registration at ">>))/binary, + Host/binary>>}]}, + #xmlel{name = <<"instructions">>, attrs = [], + children = + [{xmlcdata, + translate:translate(Lang, + <<"Enter nickname you want to register">>)}]}, + ?XFIELD(<<"text-single">>, <<"Nickname">>, <<"nick">>, + Nick)]}]. + +set_nick(ServerHost, Host, From, Nick) -> + LServer = jlib:nameprep(ServerHost), + set_nick(LServer, Host, From, Nick, + gen_mod:db_type(LServer, ?MODULE)). + +set_nick(_LServer, Host, From, Nick, mnesia) -> + {LUser, LServer, _} = jlib:jid_tolower(From), + LUS = {LUser, LServer}, + F = fun () -> + case Nick of + <<"">> -> + mnesia:delete({muc_registered, {LUS, Host}}), ok; + _ -> + Allow = case mnesia:select(muc_registered, + [{#muc_registered{us_host = + '$1', + nick = Nick, + _ = '_'}, + [{'==', {element, 2, '$1'}, + Host}], + ['$_']}]) + of + [] -> true; + [#muc_registered{us_host = {U, _Host}}] -> + U == LUS + end, + if Allow -> + mnesia:write(#muc_registered{us_host = {LUS, Host}, + nick = Nick}), + ok; + true -> false + end + end + end, + mnesia:transaction(F); +set_nick(LServer, Host, From, Nick, odbc) -> + JID = + jlib:jid_to_string(jlib:jid_tolower(jlib:jid_remove_resource(From))), + SJID = ejabberd_odbc:escape(JID), + SNick = ejabberd_odbc:escape(Nick), + SHost = ejabberd_odbc:escape(Host), + F = fun () -> + case Nick of + <<"">> -> + ejabberd_odbc:sql_query_t([<<"delete from muc_registered where ">>, + <<"jid='">>, SJID, + <<"' and host='">>, Host, + <<"';">>]), + ok; + _ -> + Allow = case + ejabberd_odbc:sql_query_t([<<"select jid from muc_registered ">>, + <<"where nick='">>, + SNick, + <<"' and host='">>, + SHost, <<"';">>]) + of + {selected, [<<"jid">>], [[J]]} -> J == JID; + _ -> true + end, + if Allow -> + odbc_queries:update_t(<<"muc_registered">>, + [<<"jid">>, <<"host">>, + <<"nick">>], + [SJID, SHost, SNick], + [<<"jid='">>, SJID, + <<"' and host='">>, SHost, + <<"'">>]), + ok; + true -> false + end + end + end, + ejabberd_odbc:sql_transaction(LServer, F). + +iq_set_register_info(ServerHost, Host, From, Nick, + Lang) -> + case set_nick(ServerHost, Host, From, Nick) of + {atomic, ok} -> {result, []}; + {atomic, false} -> + ErrText = <<"That nickname is registered by another " + "person">>, + {error, ?ERRT_CONFLICT(Lang, ErrText)}; + _ -> {error, ?ERR_INTERNAL_SERVER_ERROR} + end. + +process_iq_register_set(ServerHost, Host, From, SubEl, + Lang) -> + #xmlel{children = Els} = SubEl, + case xml:get_subtag(SubEl, <<"remove">>) of + false -> + 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, []}; + {?NS_XDATA, <<"submit">>} -> + XData = jlib:parse_xdata_submit(XEl), + case XData of + invalid -> {error, ?ERR_BAD_REQUEST}; + _ -> + case lists:keysearch(<<"nick">>, 1, XData) of + {value, {_, [Nick]}} when Nick /= <<"">> -> + iq_set_register_info(ServerHost, Host, From, + Nick, Lang); + _ -> + ErrText = + <<"You must fill in field \"Nickname\" " + "in the form">>, + {error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)} + end + end; + _ -> {error, ?ERR_BAD_REQUEST} + end; + _ -> {error, ?ERR_BAD_REQUEST} + end; + _ -> + iq_set_register_info(ServerHost, Host, From, <<"">>, + Lang) + end. + +iq_get_vcard(Lang) -> + [#xmlel{name = <<"FN">>, attrs = [], + children = [{xmlcdata, <<"ejabberd/mod_muc">>}]}, + #xmlel{name = <<"URL">>, attrs = [], + children = [{xmlcdata, ?EJABBERD_URI}]}, + #xmlel{name = <<"DESC">>, attrs = [], + children = + [{xmlcdata, + <<(translate:translate(Lang, + <<"ejabberd MUC module">>))/binary, + "\nCopyright (c) 2003-2013 ProcessOne">>}]}]. + + +broadcast_service_message(Host, Msg) -> + lists:foreach( + fun(#muc_online_room{pid = Pid}) -> + gen_fsm:send_all_state_event( + Pid, {service_message, Msg}) + end, get_vh_rooms(Host)). + + +get_vh_rooms(Host) -> + mnesia:dirty_select(muc_online_room, + [{#muc_online_room{name_host = '$1', _ = '_'}, + [{'==', {element, 2, '$1'}, Host}], + ['$_']}]). + + +clean_table_from_bad_node(Node) -> + F = fun() -> + Es = mnesia:select( + muc_online_room, + [{#muc_online_room{pid = '$1', _ = '_'}, + [{'==', {node, '$1'}, Node}], + ['$_']}]), + lists:foreach(fun(E) -> + mnesia:delete_object(E) + end, Es) + end, + mnesia:async_dirty(F). + +clean_table_from_bad_node(Node, Host) -> + F = fun() -> + Es = mnesia:select( + muc_online_room, + [{#muc_online_room{pid = '$1', + name_host = {'_', Host}, + _ = '_'}, + [{'==', {node, '$1'}, Node}], + ['$_']}]), + lists:foreach(fun(E) -> + mnesia:delete_object(E) + end, Es) + end, + mnesia:async_dirty(F). + +opts_to_binary(Opts) -> + lists:map( + fun({title, Title}) -> + {title, iolist_to_binary(Title)}; + ({description, Desc}) -> + {description, iolist_to_binary(Desc)}; + ({password, Pass}) -> + {password, iolist_to_binary(Pass)}; + ({subject, Subj}) -> + {subject, iolist_to_binary(Subj)}; + ({subject_author, Author}) -> + {subject_author, iolist_to_binary(Author)}; + ({affiliations, Affs}) -> + {affiliations, lists:map( + fun({{U, S, R}, Aff}) -> + NewAff = + case Aff of + {A, Reason} -> + {A, iolist_to_binary(Reason)}; + _ -> + Aff + end, + {{iolist_to_binary(U), + iolist_to_binary(S), + iolist_to_binary(R)}, + NewAff} + end, Affs)}; + ({captcha_whitelist, CWList}) -> + {captcha_whitelist, lists:map( + fun({U, S, R}) -> + {iolist_to_binary(U), + iolist_to_binary(S), + iolist_to_binary(R)} + end, CWList)}; + (Opt) -> + Opt + end, Opts). + +update_tables(Host) -> + update_muc_room_table(Host), + update_muc_registered_table(Host). + +update_muc_room_table(Host) -> + Fields = record_info(fields, muc_room), + case mnesia:table_info(muc_room, attributes) of + Fields -> + ejabberd_config:convert_table_to_binary( + muc_room, Fields, set, + fun(#muc_room{name_host = {N, _}}) -> N end, + fun(#muc_room{name_host = {N, H}, + opts = Opts} = R) -> + R#muc_room{name_host = {iolist_to_binary(N), + iolist_to_binary(H)}, + opts = opts_to_binary(Opts)} + end); + _ -> + ?INFO_MSG("Recreating muc_room table", []), + mnesia:transform_table(muc_room, ignore, Fields) + end. + +update_muc_registered_table(Host) -> + Fields = record_info(fields, muc_registered), + case mnesia:table_info(muc_registered, attributes) of + Fields -> + ejabberd_config:convert_table_to_binary( + muc_registered, Fields, set, + fun(#muc_registered{us_host = {_, H}}) -> H end, + fun(#muc_registered{us_host = {{U, S}, H}, + nick = Nick} = R) -> + R#muc_registered{us_host = {{iolist_to_binary(U), + iolist_to_binary(S)}, + iolist_to_binary(H)}, + nick = iolist_to_binary(Nick)} + end); + _ -> + ?INFO_MSG("Recreating muc_registered table", []), + mnesia:transform_table(muc_registered, ignore, Fields) + end. -- cgit v1.2.3