diff options
Diffstat (limited to 'src')
171 files changed, 19239 insertions, 10305 deletions
diff --git a/src/acl.erl b/src/acl.erl index 8d9692ff..fdf397d8 100644 --- a/src/acl.erl +++ b/src/acl.erl @@ -5,7 +5,7 @@ %%% Created : 18 Jan 2003 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,11 +25,14 @@ -module(acl). +-behaviour(ejabberd_config). + -author('alexey@process-one.net'). -export([start/0, to_record/3, add/3, add_list/3, - add_local/3, add_list_local/3, load_from_config/0, - match_rule/3, match_acl/3, transform_options/1]). + add_local/3, add_list_local/3, load_from_config/0, + match_rule/3, match_acl/3, transform_options/1, + opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -95,7 +98,7 @@ to_record(Host, ACLName, ACLSpec) -> -spec add(binary(), aclname(), aclspec()) -> ok | {error, any()}. add(Host, ACLName, ACLSpec) -> - {ResL, BadNodes} = rpc:multicall(mnesia:system_info(running_db_nodes), + {ResL, BadNodes} = ejabberd_cluster:multicall( ?MODULE, add_local, [Host, ACLName, ACLSpec]), case lists:keyfind(aborted, 1, ResL) of @@ -122,7 +125,7 @@ add_local(Host, ACLName, ACLSpec) -> -spec add_list(binary(), [acl()], boolean()) -> ok | {error, any()}. add_list(Host, ACLs, Clear) -> - {ResL, BadNodes} = rpc:multicall(mnesia:system_info(running_db_nodes), + {ResL, BadNodes} = ejabberd_cluster:multicall( ?MODULE, add_list_local, [Host, ACLs, Clear]), case lists:keyfind(aborted, 1, ResL) of @@ -164,16 +167,12 @@ add_list_local(Host, ACLs, Clear) -> access_name(), [access_rule()]) -> ok | {error, any()}. add_access(Host, Access, Rules) -> - case mnesia:transaction( - fun() -> - mnesia:write( - #access{name = {Access, Host}, - rules = Rules}) - end) of - {atomic, ok} -> - ok; - Err -> - {error, Err} + Obj = #access{name = {Access, Host}, rules = Rules}, + case mnesia:transaction(fun() -> mnesia:write(Obj) end) of + {atomic, ok} -> + ok; + Err -> + {error, Err} end. -spec load_from_config() -> ok. @@ -209,13 +208,13 @@ b(S) -> iolist_to_binary(S). nodeprep(S) -> - jlib:nodeprep(b(S)). + jid:nodeprep(b(S)). nameprep(S) -> - jlib:nameprep(b(S)). + jid:nameprep(b(S)). resourceprep(S) -> - jlib:resourceprep(b(S)). + jid:resourceprep(b(S)). normalize_spec(Spec) -> case Spec of @@ -236,8 +235,7 @@ normalize_spec(Spec) -> {server_regexp, SR} -> {server_regexp, b(SR)}; {server_glob, S} -> {server_glob, b(S)}; {resource_glob, R} -> {resource_glob, b(R)}; - {ip, {Net, Mask}} -> - {ip, {Net, Mask}}; + {ip, {Net, Mask}} -> {ip, {Net, Mask}}; {ip, S} -> case parse_ip_netmask(b(S)) of {ok, Net, Mask} -> @@ -295,11 +293,9 @@ match_acl(ACL, IP, Host) when tuple_size(IP) == 4; is_ip_match(IP, Net, Mask); (_) -> false - end, - ets:lookup(acl, {ACL, Host}) ++ - ets:lookup(acl, {ACL, global})); + end, get_aclspecs(ACL, Host)); match_acl(ACL, JID, Host) -> - {User, Server, Resource} = jlib:jid_tolower(JID), + {User, Server, Resource} = jid:tolower(JID), lists:any( fun(#acl{aclspec = Spec}) -> case Spec of @@ -347,8 +343,10 @@ match_acl(ACL, JID, Host) -> false end end, - ets:lookup(acl, {ACL, Host}) ++ - ets:lookup(acl, {ACL, global})). + get_aclspecs(ACL, Host)). + +get_aclspecs(ACL, Host) -> + ets:lookup(acl, {ACL, Host}) ++ ets:lookup(acl, {ACL, global}). is_regexp_match(String, RegExp) -> case ejabberd_regexp:run(String, RegExp) of @@ -476,3 +474,7 @@ transform_options({access, Name, Rules}, Opts) -> [{access, [{Name, NewRules}]}|Opts]; transform_options(Opt, Opts) -> [Opt|Opts]. + +opt_type(access) -> fun (V) -> V end; +opt_type(acl) -> fun (V) -> V end; +opt_type(_) -> [access, acl]. diff --git a/src/adhoc.erl b/src/adhoc.erl index a68b54d8..788bf65a 100644 --- a/src/adhoc.erl +++ b/src/adhoc.erl @@ -5,7 +5,7 @@ %%% Created : 31 Oct 2005 by Magnus Henoch <henoch@dtek.chalmers.se> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -51,9 +51,9 @@ parse_request(#iq{type = set, lang = Lang, sub_el = SubEl, xmlns = ?NS_COMMANDS}) -> ?DEBUG("entering parse_request...", []), - Node = xml:get_tag_attr_s(<<"node">>, SubEl), - SessionID = xml:get_tag_attr_s(<<"sessionid">>, SubEl), - Action = xml:get_tag_attr_s(<<"action">>, SubEl), + Node = fxml:get_tag_attr_s(<<"node">>, SubEl), + SessionID = fxml:get_tag_attr_s(<<"sessionid">>, SubEl), + Action = fxml:get_tag_attr_s(<<"action">>, SubEl), XData = find_xdata_el(SubEl), #xmlel{children = AllEls} = SubEl, Others = case XData of @@ -76,7 +76,7 @@ find_xdata_el(#xmlel{children = SubEls}) -> find_xdata_el1([]) -> false; find_xdata_el1([El | Els]) when is_record(El, xmlel) -> - case xml:get_tag_attr_s(<<"xmlns">>, El) of + case fxml:get_tag_attr_s(<<"xmlns">>, El) of ?NS_XDATA -> El; _ -> find_xdata_el1(Els) end; @@ -121,7 +121,7 @@ produce_response( }) -> SessionID = if is_binary(ProvidedSessionID), ProvidedSessionID /= <<"">> -> ProvidedSessionID; - true -> jlib:now_to_utc_string(now()) + true -> jlib:now_to_utc_string(p1_time_compat:timestamp()) end, case Actions of [] -> diff --git a/src/cyrsasl.erl b/src/cyrsasl.erl index 09b1a1a6..21fbc966 100644 --- a/src/cyrsasl.erl +++ b/src/cyrsasl.erl @@ -5,7 +5,7 @@ %%% Created : 8 Mar 2003 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,10 +25,13 @@ -module(cyrsasl). +-behaviour(ejabberd_config). + -author('alexey@process-one.net'). -export([start/0, register_mechanism/3, listmech/1, - server_new/7, server_start/3, server_step/2]). + server_new/7, server_start/3, server_step/2, + opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -81,6 +84,7 @@ start() -> cyrsasl_digest:start([]), cyrsasl_scram:start([]), cyrsasl_anonymous:start([]), + cyrsasl_oauth:start([]), ok. %% @@ -107,13 +111,13 @@ register_mechanism(Mechanism, Module, PasswordType) -> %%-include("ejabberd.hrl"). %%-include("jlib.hrl"). %%check_authzid(_State, Props) -> -%% AuthzId = xml:get_attr_s(authzid, Props), -%% case jlib:string_to_jid(AuthzId) of +%% AuthzId = fxml:get_attr_s(authzid, Props), +%% case jid:from_string(AuthzId) of %% error -> %% {error, "invalid-authzid"}; %% JID -> -%% LUser = jlib:nodeprep(xml:get_attr_s(username, Props)), -%% {U, S, R} = jlib:jid_tolower(JID), +%% LUser = jid:nodeprep(fxml:get_attr_s(username, Props)), +%% {U, S, R} = jid:tolower(JID), %% case R of %% "" -> %% {error, "invalid-authzid"}; @@ -129,7 +133,7 @@ register_mechanism(Mechanism, Module, PasswordType) -> check_credentials(_State, Props) -> User = proplists:get_value(authzid, Props, <<>>), - case jlib:nodeprep(User) of + case jid:nodeprep(User) of error -> {error, <<"not-authorized">>}; <<"">> -> {error, <<"not-authorized">>}; _LUser -> ok @@ -237,3 +241,10 @@ is_disabled(Mechanism) -> [str:to_upper(V)] end, []), lists:member(Mechanism, Disabled). + +opt_type(disable_sasl_mechanisms) -> + fun (V) when is_list(V) -> + lists:map(fun (M) -> str:to_upper(M) end, V); + (V) -> [str:to_upper(V)] + end; +opt_type(_) -> [disable_sasl_mechanisms]. diff --git a/src/cyrsasl_anonymous.erl b/src/cyrsasl_anonymous.erl index 51d5db9d..2b2a9f63 100644 --- a/src/cyrsasl_anonymous.erl +++ b/src/cyrsasl_anonymous.erl @@ -6,7 +6,7 @@ %%% Created : 23 Aug 2005 by Magnus Henoch <henoch@dtek.chalmers.se> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -26,6 +26,8 @@ -module(cyrsasl_anonymous). +-protocol({xep, 175, '1.2'}). + -export([start/1, stop/0, mech_new/4, mech_step/2]). -behaviour(cyrsasl). @@ -41,11 +43,11 @@ stop() -> ok. mech_new(Host, _GetPassword, _CheckPassword, _CheckPasswordDigest) -> {ok, #state{server = Host}}. -mech_step(#state{server = Server}, _ClientIn) -> - User = iolist_to_binary([randoms:get_string() - | [jlib:integer_to_binary(X) - || X <- tuple_to_list(now())]]), +mech_step(#state{server = Server} = S, ClientIn) -> + User = iolist_to_binary([randoms:get_string(), + randoms:get_string(), + randoms:get_string()]), case ejabberd_auth:is_user_exists(User, Server) of - true -> {error, <<"not-authorized">>}; + true -> mech_step(S, ClientIn); false -> {ok, [{username, User}, {auth_module, ejabberd_auth_anonymous}]} end. diff --git a/src/cyrsasl_digest.erl b/src/cyrsasl_digest.erl index 12e5555a..e58cb303 100644 --- a/src/cyrsasl_digest.erl +++ b/src/cyrsasl_digest.erl @@ -5,7 +5,7 @@ %%% Created : 11 Mar 2003 by Alexey Shchepin <alexey@sevcom.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,9 +25,12 @@ -module(cyrsasl_digest). +-behaviour(ejabberd_config). + -author('alexey@sevcom.net'). --export([start/1, stop/0, mech_new/4, mech_step/2, parse/1]). +-export([start/1, stop/0, mech_new/4, mech_step/2, + parse/1, opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -256,3 +259,6 @@ response(KeyVals, User, Passwd, Nonce, AuthzId, ":", NC/binary, ":", CNonce/binary, ":", QOP/binary, ":", (hex((erlang:md5(A2))))/binary>>, hex((erlang:md5(T))). + +opt_type(fqdn) -> fun iolist_to_binary/1; +opt_type(_) -> [fqdn]. diff --git a/src/cyrsasl_oauth.erl b/src/cyrsasl_oauth.erl new file mode 100644 index 00000000..16f1e3df --- /dev/null +++ b/src/cyrsasl_oauth.erl @@ -0,0 +1,91 @@ +%%%---------------------------------------------------------------------- +%%% File : cyrsasl_oauth.erl +%%% Author : Alexey Shchepin <alexey@process-one.net> +%%% Purpose : X-OAUTH2 SASL mechanism +%%% Created : 17 Sep 2015 by Alexey Shchepin <alexey@process-one.net> +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2016 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(cyrsasl_oauth). + +-author('alexey@process-one.net'). + +-export([start/1, stop/0, mech_new/4, mech_step/2, parse/1]). + +-behaviour(cyrsasl). + +-record(state, {host}). + +start(_Opts) -> + cyrsasl:register_mechanism(<<"X-OAUTH2">>, ?MODULE, plain), + ok. + +stop() -> ok. + +mech_new(Host, _GetPassword, _CheckPassword, _CheckPasswordDigest) -> + {ok, #state{host = Host}}. + +mech_step(State, ClientIn) -> + case prepare(ClientIn) of + [AuthzId, User, Token] -> + case ejabberd_oauth:check_token( + User, State#state.host, <<"sasl_auth">>, Token) of + true -> + {ok, + [{username, User}, {authzid, AuthzId}, + {auth_module, ejabberd_oauth}]}; + false -> + {error, <<"not-authorized">>, User} + end; + _ -> {error, <<"bad-protocol">>} + end. + +prepare(ClientIn) -> + case parse(ClientIn) of + [<<"">>, UserMaybeDomain, Token] -> + case parse_domain(UserMaybeDomain) of + %% <NUL>login@domain<NUL>pwd + [User, _Domain] -> [UserMaybeDomain, User, Token]; + %% <NUL>login<NUL>pwd + [User] -> [<<"">>, User, Token] + end; + %% login@domain<NUL>login<NUL>pwd + [AuthzId, User, Token] -> [AuthzId, User, Token]; + _ -> error + end. + +parse(S) -> parse1(binary_to_list(S), "", []). + +parse1([0 | Cs], S, T) -> + parse1(Cs, "", [list_to_binary(lists:reverse(S)) | T]); +parse1([C | Cs], S, T) -> parse1(Cs, [C | S], T); +%parse1([], [], T) -> +% lists:reverse(T); +parse1([], S, T) -> + lists:reverse([list_to_binary(lists:reverse(S)) | T]). + +parse_domain(S) -> parse_domain1(binary_to_list(S), "", []). + +parse_domain1([$@ | Cs], S, T) -> + parse_domain1(Cs, "", [list_to_binary(lists:reverse(S)) | T]); +parse_domain1([C | Cs], S, T) -> + parse_domain1(Cs, [C | S], T); +parse_domain1([], S, T) -> + lists:reverse([list_to_binary(lists:reverse(S)) | T]). diff --git a/src/cyrsasl_plain.erl b/src/cyrsasl_plain.erl index ceceacca..82d68f87 100644 --- a/src/cyrsasl_plain.erl +++ b/src/cyrsasl_plain.erl @@ -5,7 +5,7 @@ %%% Created : 8 Mar 2003 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -66,7 +66,7 @@ prepare(ClientIn) -> end; [AuthzId, User, Password] -> case parse_domain(AuthzId) of - %% login@domain<NUL>login<NUL>pwd + %% login@domain<NUL>login<NUL>pwd [AuthzUser, _Domain] -> [AuthzUser, User, Password]; %% login<NUL>login<NUL>pwd [AuthzUser] -> [AuthzUser, User, Password] diff --git a/src/cyrsasl_scram.erl b/src/cyrsasl_scram.erl index deef51c5..059938f5 100644 --- a/src/cyrsasl_scram.erl +++ b/src/cyrsasl_scram.erl @@ -5,7 +5,7 @@ %%% Created : 7 Aug 2011 by Stephen Röttger <stephen.roettger@googlemail.com> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -27,6 +27,8 @@ -author('stephen.roettger@googlemail.com'). +-protocol({rfc, 5802}). + -export([start/1, stop/0, mech_new/4, mech_step/2]). -include("ejabberd.hrl"). @@ -77,7 +79,7 @@ mech_step(#state{step = 2} = State, ClientIn) -> case parse_attribute(ClientNonceAttribute) of {$r, ClientNonce} -> {Ret, _AuthModule} = (State#state.get_password)(UserName), - case {Ret, jlib:resourceprep(Ret)} of + case {Ret, jid:resourceprep(Ret)} of {false, _} -> {error, <<"not-authorized">>, UserName}; {_, error} when is_binary(Ret) -> ?WARNING_MSG("invalid plain password", []), {error, <<"not-authorized">>, UserName}; {Ret, _} -> diff --git a/src/ejabberd.erl b/src/ejabberd.erl index 64ac5a0d..e1b92c26 100644 --- a/src/ejabberd.erl +++ b/src/ejabberd.erl @@ -5,7 +5,7 @@ %%% Created : 16 Nov 2002 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -26,6 +26,16 @@ -module(ejabberd). -author('alexey@process-one.net'). +-protocol({xep, 4, '2.9'}). +-protocol({xep, 86, '1.0'}). +-protocol({xep, 106, '1.1'}). +-protocol({xep, 170, '1.0'}). +-protocol({xep, 205, '1.0'}). +-protocol({xep, 212, '1.0'}). +-protocol({xep, 216, '1.0'}). +-protocol({xep, 243, '1.0'}). +-protocol({xep, 270, '1.0'}). + -export([start/0, stop/0, start_app/1, start_app/2, get_pid_file/0, check_app/1]). @@ -95,7 +105,7 @@ start_app([], _Type, _StartFlag) -> ok. check_app_modules(App, StartFlag) -> - {A, B, C} = now(), + {A, B, C} = p1_time_compat:timestamp(), random:seed(A, B, C), sleep(5000), case application:get_key(App, modules) of diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl index 8b6e27b8..b22a7038 100644 --- a/src/ejabberd_admin.erl +++ b/src/ejabberd_admin.erl @@ -5,7 +5,7 @@ %%% Created : 7 May 2006 by Mickael Remond <mremond@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -28,10 +28,13 @@ -export([start/0, stop/0, %% Server - status/0, reopen_log/0, + status/0, reopen_log/0, rotate_log/0, + set_loglevel/1, stop_kindly/2, send_service_message_all_mucs/2, registered_vhosts/0, reload_config/0, + %% Cluster + join_cluster/1, leave_cluster/1, list_cluster/0, %% Erlang update_list/0, update/1, %% Accounts @@ -42,14 +45,14 @@ %% Purge DB delete_expired_messages/0, delete_old_messages/1, %% Mnesia - export2odbc/2, set_master/1, backup_mnesia/1, restore_mnesia/1, dump_mnesia/1, dump_table/2, load_mnesia/1, install_fallback_mnesia/1, dump_to_textfile/1, dump_to_textfile/2, mnesia_change_nodename/4, - restore/1 % Still used by some modules + restore/1, % Still used by some modules + get_commands_spec/0 ]). -include("ejabberd.hrl"). @@ -57,16 +60,16 @@ -include("ejabberd_commands.hrl"). start() -> - ejabberd_commands:register_commands(commands()). + ejabberd_commands:register_commands(get_commands_spec()). stop() -> - ejabberd_commands:unregister_commands(commands()). + ejabberd_commands:unregister_commands(get_commands_spec()). %%% %%% ejabberd commands %%% -commands() -> +get_commands_spec() -> [ %% The commands status, stop and restart are implemented also in ejabberd_ctl %% They are defined here so that other interfaces can use them too @@ -86,6 +89,10 @@ commands() -> desc = "Reopen the log files", module = ?MODULE, function = reopen_log, args = [], result = {res, rescode}}, + #ejabberd_commands{name = rotate_log, tags = [logs, server], + desc = "Rotate the log files", + module = ?MODULE, function = rotate_log, + args = [], result = {res, rescode}}, #ejabberd_commands{name = stop_kindly, tags = [server], desc = "Inform users and rooms, wait, and stop the server", longdesc = "Provide the delay in seconds, and the " @@ -103,6 +110,11 @@ commands() -> {levelatom, atom}, {leveldesc, string} ]}}}, + #ejabberd_commands{name = set_loglevel, tags = [logs, server], + desc = "Set the loglevel (0 to 5)", + module = ?MODULE, function = set_loglevel, + args = [{loglevel, integer}], + result = {logger, atom}}, #ejabberd_commands{name = update_list, tags = [server], desc = "List modified modules that can be updated", @@ -136,11 +148,27 @@ commands() -> args = [], result = {vhosts, {list, {vhost, string}}}}, #ejabberd_commands{name = reload_config, tags = [server], - desc = "Reload ejabberd configuration file into memory", + desc = "Reload config file in memory (only affects ACL and Access)", module = ?MODULE, function = reload_config, args = [], result = {res, rescode}}, + #ejabberd_commands{name = join_cluster, tags = [cluster], + desc = "Join this node into the cluster handled by Node", + module = ?MODULE, function = join_cluster, + args = [{node, binary}], + result = {res, rescode}}, + #ejabberd_commands{name = leave_cluster, tags = [cluster], + desc = "Remove node handled by Node from the cluster", + module = ?MODULE, function = leave_cluster, + args = [{node, binary}], + result = {res, rescode}}, + #ejabberd_commands{name = list_cluster, tags = [cluster], + desc = "List nodes that are part of the cluster handled by Node", + module = ?MODULE, function = list_cluster, + args = [], + result = {nodes, {list, {node, atom}}}}, + #ejabberd_commands{name = import_file, tags = [mnesia], desc = "Import user data from jabberd14 spool file", module = ?MODULE, function = import_file, @@ -168,6 +196,19 @@ commands() -> desc = "Export all tables as SQL queries to a file", module = ejd2odbc, function = export, args = [{host, string}, {file, string}], result = {res, rescode}}, + #ejabberd_commands{name = delete_mnesia, tags = [mnesia, odbc], + desc = "Export all tables as SQL queries to a file", + module = ejd2odbc, function = delete, + args = [{host, string}], result = {res, rescode}}, + #ejabberd_commands{name = convert_to_scram, tags = [odbc], + desc = "Convert the passwords in 'users' ODBC table to SCRAM", + module = ejabberd_auth_odbc, function = convert_to_scram, + args = [{host, binary}], result = {res, rescode}}, + + #ejabberd_commands{name = import_prosody, tags = [mnesia, odbc, riak], + desc = "Import data from Prosody", + module = prosody2ejabberd, function = from_dir, + args = [{dir, string}], result = {res, rescode}}, #ejabberd_commands{name = convert_to_yaml, tags = [config], desc = "Convert the input file from Erlang to YAML format", @@ -184,14 +225,9 @@ commands() -> module = ?MODULE, function = delete_old_messages, args = [{days, integer}], result = {res, rescode}}, - #ejabberd_commands{name = rename_default_nodeplugin, tags = [mnesia], - desc = "Update PubSub table from old ejabberd trunk SVN to 2.1.0", - module = mod_pubsub, function = rename_default_nodeplugin, - args = [], result = {res, rescode}}, - #ejabberd_commands{name = export2odbc, tags = [mnesia], desc = "Export virtual host information from Mnesia tables to SQL files", - module = ?MODULE, function = export2odbc, + module = ejd2odbc, function = export, args = [{host, string}, {directory, string}], result = {res, rescode}}, #ejabberd_commands{name = set_master, tags = [mnesia], @@ -254,6 +290,15 @@ reopen_log() -> ejabberd_hooks:run(reopen_log_hook, []), ejabberd_logger:reopen_log(). +rotate_log() -> + ejabberd_hooks:run(rotate_log_hook, []), + ejabberd_logger:rotate_log(). + +set_loglevel(LogLevel) -> + {module, Module} = ejabberd_logger:set(LogLevel), + Module. + + %%% %%% Stop Kindly %%% @@ -364,6 +409,19 @@ reload_config() -> shaper:start(). %%% +%%% Cluster management +%%% + +join_cluster(NodeBin) -> + ejabberd_cluster:join(list_to_atom(binary_to_list(NodeBin))). + +leave_cluster(NodeBin) -> + ejabberd_cluster:leave(list_to_atom(binary_to_list(NodeBin))). + +list_cluster() -> + ejabberd_cluster:get_nodes(). + +%%% %%% Migration management %%% @@ -408,23 +466,6 @@ delete_old_messages(Days) -> %%% Mnesia management %%% -export2odbc(Host, Directory) -> - Tables = [{export_last, last}, - {export_offline, offline}, - {export_private_storage, private_storage}, - {export_roster, roster}, - {export_vcard, vcard}, - {export_vcard_search, vcard_search}, - {export_passwd, passwd}], - Export = fun({TableFun, Table}) -> - Filename = filename:join([Directory, atom_to_list(Table)++".txt"]), - io:format("Trying to export Mnesia table '~p' on Host '~s' to file '~s'~n", [Table, Host, Filename]), - Res = (catch ejd2odbc:TableFun(Host, Filename)), - io:format(" Result: ~p~n", [Res]) - end, - lists:foreach(Export, Tables), - ok. - set_master("self") -> set_master(node()); set_master(NodeString) when is_list(NodeString) -> @@ -469,7 +510,7 @@ restore_mnesia(Path) -> %% Mnesia database restore %% This function is called from ejabberd_ctl, ejabberd_web_admin and -%% mod_configure/adhoc +%% mod_configure/adhoc restore(Path) -> mnesia:restore(Path, [{keep_tables,keep_tables()}, {default_op, skip_tables}]). @@ -484,7 +525,7 @@ keep_tables() -> %% Returns the list of modules tables in use, according to the list of actually %% loaded modules -keep_modules_tables() -> +keep_modules_tables() -> lists:map(fun(Module) -> module_tables(Module) end, gen_mod:loaded_modules(?MYNAME)). diff --git a/src/ejabberd_app.erl b/src/ejabberd_app.erl index fabe2d3e..e493eac0 100644 --- a/src/ejabberd_app.erl +++ b/src/ejabberd_app.erl @@ -5,7 +5,7 @@ %%% Created : 31 Jan 2003 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -24,11 +24,14 @@ %%%---------------------------------------------------------------------- -module(ejabberd_app). + +-behaviour(ejabberd_config). -author('alexey@process-one.net'). -behaviour(application). --export([start_modules/0,start/2, prep_stop/1, stop/1, init/0]). +-export([start_modules/0, start/2, prep_stop/1, stop/1, + init/0, opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -40,6 +43,7 @@ start(normal, _Args) -> ejabberd_logger:start(), write_pid_file(), + jid:start(), start_apps(), ejabberd:check_app(ejabberd), randoms:start(), @@ -50,8 +54,9 @@ start(normal, _Args) -> ejabberd_commands:init(), ejabberd_admin:start(), gen_mod:start(), + ext_mod:start(), ejabberd_config:start(), - set_loglevel_from_config(), + set_settings_from_config(), acl:start(), shaper:start(), connect_nodes(), @@ -59,13 +64,13 @@ start(normal, _Args) -> ejabberd_rdbms:start(), ejabberd_riak_sup:start(), ejabberd_sm:start(), - ejabberd_auth:start(), cyrsasl:start(), % Profiling %ejabberd_debug:eprof_start(), %ejabberd_debug:fprof_start(), maybe_add_nameservers(), - ext_mod:start(), + ejabberd_auth:start(), + ejabberd_oauth:start(), start_modules(), ejabberd_listener:start_listeners(), ?INFO_MSG("ejabberd ~s is started in the node ~p", [?VERSION, node()]), @@ -231,20 +236,39 @@ delete_pid_file() -> file:delete(PidFilename) end. -set_loglevel_from_config() -> +set_settings_from_config() -> Level = ejabberd_config:get_option( loglevel, fun(P) when P>=0, P=<5 -> P end, 4), - ejabberd_logger:set(Level). + ejabberd_logger:set(Level), + Ticktime = ejabberd_config:get_option( + net_ticktime, + opt_type(net_ticktime), + 60), + net_kernel:set_net_ticktime(Ticktime). start_apps() -> crypto:start(), ejabberd:start_app(sasl), ejabberd:start_app(ssl), - ejabberd:start_app(p1_yaml), - ejabberd:start_app(p1_tls), - ejabberd:start_app(p1_xml), - ejabberd:start_app(p1_stringprep), - ejabberd:start_app(p1_zlib), - ejabberd:start_app(p1_cache_tab). + ejabberd:start_app(fast_yaml), + ejabberd:start_app(fast_tls), + ejabberd:start_app(fast_xml), + ejabberd:start_app(stringprep), + ejabberd:start_app(cache_tab). + +opt_type(net_ticktime) -> + fun (P) when is_integer(P), P > 0 -> P end; +opt_type(cluster_nodes) -> + fun (Ns) -> true = lists:all(fun is_atom/1, Ns), Ns end; +opt_type(loglevel) -> + fun (P) when P >= 0, P =< 5 -> P end; +opt_type(modules) -> + fun (Mods) -> + lists:map(fun ({M, A}) when is_atom(M), is_list(A) -> + {M, A} + end, + Mods) + end; +opt_type(_) -> [cluster_nodes, loglevel, modules, net_ticktime]. diff --git a/src/ejabberd_auth.erl b/src/ejabberd_auth.erl index bf47af85..343ad943 100644 --- a/src/ejabberd_auth.erl +++ b/src/ejabberd_auth.erl @@ -5,7 +5,7 @@ %%% Created : 23 Nov 2002 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -27,6 +27,8 @@ -module(ejabberd_auth). +-behaviour(ejabberd_config). + -author('alexey@process-one.net'). %% External exports @@ -42,7 +44,7 @@ remove_user/2, remove_user/3, plain_password_required/1, store_type/1, entropy/1]). --export([auth_modules/1]). +-export([auth_modules/1, opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -75,7 +77,7 @@ -callback get_password_s(binary(), binary()) -> binary() | {binary(), binary(), binary(), integer()}. start() -> -%% This is only executed by ejabberd_c2s for non-SASL auth client + %% This is only executed by ejabberd_c2s for non-SASL auth client lists:foreach(fun (Host) -> lists:foreach(fun (M) -> M:start(Host) end, auth_modules(Host)) @@ -116,7 +118,7 @@ check_password(User, AuthzId, Server, Password) -> %% true | false -spec check_password(binary(), binary(), binary(), binary(), binary(), fun((binary()) -> binary())) -> boolean(). - + check_password(User, AuthzId, Server, Password, Digest, DigestGen) -> case check_password_with_authmodule(User, AuthzId, Server, @@ -185,7 +187,8 @@ try_register(User, Server, Password) -> case is_user_exists(User, Server) of true -> {atomic, exists}; false -> - case lists:member(jlib:nameprep(Server), ?MYHOSTS) of + LServer = jid:nameprep(Server), + case lists:member(LServer, ?MYHOSTS) of true -> Res = lists:foldl(fun (_M, {atomic, ok} = Res) -> Res; (M, _) -> @@ -355,7 +358,7 @@ remove_user(User, Server) -> lists:foreach(fun (M) -> M:remove_user(User, Server) end, auth_modules(Server)), - ejabberd_hooks:run(remove_user, jlib:nameprep(Server), + ejabberd_hooks:run(remove_user, jid:nameprep(Server), [User, Server]), ok. @@ -373,7 +376,7 @@ remove_user(User, Server, Password) -> error, auth_modules(Server)), case R of ok -> - ejabberd_hooks:run(remove_user, jlib:nameprep(Server), + ejabberd_hooks:run(remove_user, jid:nameprep(Server), [User, Server]); _ -> none end, @@ -424,7 +427,7 @@ auth_modules() -> %% Return the list of authenticated modules for a given host auth_modules(Server) -> - LServer = jlib:nameprep(Server), + LServer = jid:nameprep(Server), Default = case gen_mod:default_db(LServer) of mnesia -> internal; DBType -> DBType @@ -453,3 +456,10 @@ import(Server, riak, Passwd) -> ejabberd_auth_riak:import(Server, riak, Passwd); import(_, _, _) -> pass. + +opt_type(auth_method) -> + fun (V) when is_list(V) -> + true = lists:all(fun is_atom/1, V), V; + (V) when is_atom(V) -> [V] + end; +opt_type(_) -> [auth_method]. diff --git a/src/ejabberd_auth_anonymous.erl b/src/ejabberd_auth_anonymous.erl index 05f790db..9c4b719c 100644 --- a/src/ejabberd_auth_anonymous.erl +++ b/src/ejabberd_auth_anonymous.erl @@ -5,7 +5,7 @@ %%% Created : 17 Feb 2006 by Mickael Remond <mremond@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -24,6 +24,8 @@ %%%---------------------------------------------------------------------- -module(ejabberd_auth_anonymous). + +-behaviour(ejabberd_config). -author('mickael.remond@process-one.net'). -export([start/1, @@ -36,16 +38,15 @@ unregister_connection/3 ]). - -%% Function used by ejabberd_auth: -export([login/2, set_password/3, check_password/4, check_password/6, try_register/3, dirty_get_registered_users/0, get_vh_registered_users/1, - get_vh_registered_users/2, get_vh_registered_users_number/1, - get_vh_registered_users_number/2, get_password_s/2, + get_vh_registered_users/2, + get_vh_registered_users_number/1, + get_vh_registered_users_number/2, get_password_s/2, get_password/2, get_password/3, is_user_exists/2, remove_user/2, remove_user/3, store_type/0, - plain_password_required/0]). + plain_password_required/0, opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -55,7 +56,7 @@ %% Create the anonymous table if at least one virtual host has anonymous features enabled %% Register to login / logout events -record(anonymous, {us = {<<"">>, <<"">>} :: {binary(), binary()}, - sid = {now(), self()} :: ejabberd_sm:sid()}). + sid = {p1_time_compat:timestamp(), self()} :: ejabberd_sm:sid()}). start(Host) -> %% TODO: Check cluster mode @@ -121,8 +122,8 @@ allow_multiple_connections(Host) -> %% Check if user exist in the anonymus database anonymous_user_exist(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), US = {LUser, LServer}, case catch mnesia:dirty_read({anonymous, US}) of [] -> @@ -141,7 +142,7 @@ remove_connection(SID, LUser, LServer) -> %% Register connection register_connection(SID, #jid{luser = LUser, lserver = LServer}, Info) -> - AuthModule = list_to_atom(binary_to_list(xml:get_attr_s(<<"auth_module">>, Info))), + AuthModule = proplists:get_value(auth_module, Info, undefined), case AuthModule == (?MODULE) of true -> ejabberd_hooks:run(register_user, LServer, @@ -269,3 +270,13 @@ plain_password_required() -> false. store_type() -> plain. + +opt_type(allow_multiple_connections) -> + fun (V) when is_boolean(V) -> V end; +opt_type(anonymous_protocol) -> + fun (sasl_anon) -> sasl_anon; + (login_anon) -> login_anon; + (both) -> both + end; +opt_type(_) -> + [allow_multiple_connections, anonymous_protocol]. diff --git a/src/ejabberd_auth_external.erl b/src/ejabberd_auth_external.erl index 44c931cb..5897fba5 100644 --- a/src/ejabberd_auth_external.erl +++ b/src/ejabberd_auth_external.erl @@ -5,7 +5,7 @@ %%% Created : 12 Dec 2004 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,11 +25,12 @@ -module(ejabberd_auth_external). +-behaviour(ejabberd_config). + -author('alexey@process-one.net'). -behaviour(ejabberd_auth). -%% External exports -export([start/1, set_password/3, check_password/4, check_password/6, try_register/3, dirty_get_registered_users/0, get_vh_registered_users/1, @@ -37,8 +38,8 @@ get_vh_registered_users_number/1, get_vh_registered_users_number/2, get_password/2, get_password_s/2, is_user_exists/2, remove_user/2, - remove_user/3, store_type/0, - plain_password_required/0]). + remove_user/3, store_type/0, plain_password_required/0, + opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -79,9 +80,9 @@ check_password(User, AuthzId, Server, Password) -> if AuthzId /= <<>> andalso AuthzId /= User -> false; true -> - case get_cache_option(Server) of + case get_cache_option(Server) of false -> check_password_extauth(User, AuthzId, Server, Password); - {true, CacheTime} -> + {true, CacheTime} -> check_password_cache(User, AuthzId, Server, Password, CacheTime) end end. @@ -225,7 +226,7 @@ check_password_cache(User, AuthzId, Server, Password, get_password_internal(User, Server) -> ejabberd_auth_internal:get_password(User, Server). -%% @spec (User, Server, CacheTime) -> false | Password::string() +-spec get_password_cache(User::binary(), Server::binary(), CacheTime::integer()) -> Password::string() | false. get_password_cache(User, Server, CacheTime) -> case get_last_access(User, Server) of online -> get_password_internal(User, Server); @@ -273,18 +274,16 @@ set_password_internal(User, Server, Password) -> Password). is_fresh_enough(TimeStampLast, CacheTime) -> - {MegaSecs, Secs, _MicroSecs} = now(), - Now = MegaSecs * 1000000 + Secs, + Now = p1_time_compat:system_time(seconds), TimeStampLast + CacheTime > Now. -%% @spec (User, Server) -> online | never | mod_last_required | TimeStamp::integer() %% Code copied from mod_configure.erl %% Code copied from web/ejabberd_web_admin.erl %% TODO: Update time format to XEP-0202: Entity Time +-spec(get_last_access(User::binary(), Server::binary()) -> (online | never | mod_last_required | integer())). get_last_access(User, Server) -> case ejabberd_sm:get_user_resources(User, Server) of [] -> - _US = {User, Server}, case get_last_info(User, Server) of mod_last_required -> mod_last_required; not_found -> never; @@ -314,4 +313,14 @@ get_mod_last_configured(Server) -> end. is_configured(Host, Module) -> - gen_mod:is_loaded(Host, Module). + Os = ejabberd_config:get_local_option({modules, Host}, + fun(M) when is_list(M) -> M end), + lists:keymember(Module, 1, Os). + +opt_type(extauth_cache) -> + fun (false) -> undefined; + (I) when is_integer(I), I >= 0 -> I + end; +opt_type(extauth_program) -> + fun (V) -> binary_to_list(iolist_to_binary(V)) end; +opt_type(_) -> [extauth_cache, extauth_program]. diff --git a/src/ejabberd_auth_internal.erl b/src/ejabberd_auth_internal.erl index bb4ceab5..3b30b360 100644 --- a/src/ejabberd_auth_internal.erl +++ b/src/ejabberd_auth_internal.erl @@ -5,7 +5,7 @@ %%% Created : 12 Dec 2004 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,11 +25,12 @@ -module(ejabberd_auth_internal). +-behaviour(ejabberd_config). + -author('alexey@process-one.net'). -behaviour(ejabberd_auth). -%% External exports -export([start/1, set_password/3, check_password/4, check_password/6, try_register/3, dirty_get_registered_users/0, get_vh_registered_users/1, @@ -38,7 +39,7 @@ get_vh_registered_users_number/2, get_password/2, get_password_s/2, is_user_exists/2, remove_user/2, remove_user/3, store_type/0, export/1, import/1, - import/3, plain_password_required/0]). + import/3, plain_password_required/0, opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -69,7 +70,7 @@ start(Host) -> update_reg_users_counter_table(Server) -> Set = get_vh_registered_users(Server), Size = length(Set), - LServer = jlib:nameprep(Server), + LServer = jid:nameprep(Server), F = fun () -> mnesia:write(#reg_users_counter{vhost = LServer, count = Size}) @@ -89,17 +90,17 @@ check_password(User, AuthzId, Server, Password) -> if AuthzId /= <<>> andalso AuthzId /= User -> false; true -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - US = {LUser, LServer}, - case catch mnesia:dirty_read({passwd, US}) of - [#passwd{password = Password}] - when is_binary(Password) -> - Password /= <<"">>; - [#passwd{password = Scram}] - when is_record(Scram, scram) -> - is_password_scram_valid(Password, Scram); - _ -> false + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + US = {LUser, LServer}, + case catch mnesia:dirty_read({passwd, US}) of + [#passwd{password = Password}] + when is_binary(Password) -> + Password /= <<"">>; + [#passwd{password = Scram}] + when is_record(Scram, scram) -> + is_password_scram_valid(Password, Scram); + _ -> false end end. @@ -108,37 +109,37 @@ check_password(User, AuthzId, Server, Password, Digest, if AuthzId /= <<>> andalso AuthzId /= User -> false; true -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - US = {LUser, LServer}, - case catch mnesia:dirty_read({passwd, US}) of - [#passwd{password = Passwd}] when is_binary(Passwd) -> - DigRes = if Digest /= <<"">> -> - Digest == DigestGen(Passwd); - true -> false - end, - if DigRes -> true; - true -> (Passwd == Password) and (Password /= <<"">>) - end; - [#passwd{password = Scram}] - when is_record(Scram, scram) -> - Passwd = jlib:decode_base64(Scram#scram.storedkey), - DigRes = if Digest /= <<"">> -> - Digest == DigestGen(Passwd); - true -> false - end, - if DigRes -> true; - true -> (Passwd == Password) and (Password /= <<"">>) - end; - _ -> false + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + US = {LUser, LServer}, + case catch mnesia:dirty_read({passwd, US}) of + [#passwd{password = Passwd}] when is_binary(Passwd) -> + DigRes = if Digest /= <<"">> -> + Digest == DigestGen(Passwd); + true -> false + end, + if DigRes -> true; + true -> (Passwd == Password) and (Password /= <<"">>) + end; + [#passwd{password = Scram}] + when is_record(Scram, scram) -> + Passwd = jlib:decode_base64(Scram#scram.storedkey), + DigRes = if Digest /= <<"">> -> + Digest == DigestGen(Passwd); + true -> false + end, + if DigRes -> true; + true -> (Passwd == Password) and (Password /= <<"">>) + end; + _ -> false end end. %% @spec (User::string(), Server::string(), Password::string()) -> %% ok | {error, invalid_jid} set_password(User, Server, Password) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), US = {LUser, LServer}, if (LUser == error) or (LServer == error) -> {error, invalid_jid}; @@ -157,8 +158,8 @@ set_password(User, Server, Password) -> %% @spec (User, Server, Password) -> {atomic, ok} | {atomic, exists} | {error, invalid_jid} | {error, not_allowed} | {error, Reason} try_register(User, Server, PasswordList) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), Password = if is_list(PasswordList); is_binary(PasswordList) -> iolist_to_binary(PasswordList); true -> PasswordList @@ -192,7 +193,7 @@ dirty_get_registered_users() -> mnesia:dirty_all_keys(passwd). get_vh_registered_users(Server) -> - LServer = jlib:nameprep(Server), + LServer = jid:nameprep(Server), mnesia:dirty_select(passwd, [{#passwd{us = '$1', _ = '_'}, [{'==', {element, 2, '$1'}, LServer}], ['$1']}]). @@ -251,7 +252,7 @@ get_vh_registered_users(Server, _) -> get_vh_registered_users(Server). get_vh_registered_users_number(Server) -> - LServer = jlib:nameprep(Server), + LServer = jid:nameprep(Server), Query = mnesia:dirty_select(reg_users_counter, [{#reg_users_counter{vhost = LServer, count = '$1'}, @@ -272,8 +273,8 @@ get_vh_registered_users_number(Server, _) -> get_vh_registered_users_number(Server). get_password(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), US = {LUser, LServer}, case catch mnesia:dirty_read(passwd, US) of [#passwd{password = Password}] @@ -289,8 +290,8 @@ get_password(User, Server) -> end. get_password_s(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), US = {LUser, LServer}, case catch mnesia:dirty_read(passwd, US) of [#passwd{password = Password}] @@ -304,8 +305,8 @@ get_password_s(User, Server) -> %% @spec (User, Server) -> true | false | {error, Error} is_user_exists(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), US = {LUser, LServer}, case catch mnesia:dirty_read({passwd, US}) of [] -> false; @@ -317,8 +318,8 @@ is_user_exists(User, Server) -> %% @doc Remove user. %% Note: it returns ok even if there was some problem removing the user. remove_user(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), US = {LUser, LServer}, F = fun () -> mnesia:delete({passwd, US}), @@ -331,8 +332,8 @@ remove_user(User, Server) -> %% @spec (User, Server, Password) -> ok | not_exists | not_allowed | bad_request %% @doc Remove user if the provided password is correct. remove_user(User, Server, Password) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), US = {LUser, LServer}, F = fun () -> case mnesia:read({passwd, US}) of @@ -492,3 +493,6 @@ import(_LServer, mnesia, #passwd{} = P) -> mnesia:dirty_write(P); import(_, _, _) -> pass. + +opt_type(auth_password_format) -> fun (V) -> V end; +opt_type(_) -> [auth_password_format]. diff --git a/src/ejabberd_auth_ldap.erl b/src/ejabberd_auth_ldap.erl index 45964d66..51b466ef 100644 --- a/src/ejabberd_auth_ldap.erl +++ b/src/ejabberd_auth_ldap.erl @@ -5,7 +5,7 @@ %%% Created : 12 Dec 2004 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,6 +25,8 @@ -module(ejabberd_auth_ldap). +-behaviour(ejabberd_config). + -author('alexey@process-one.net'). -behaviour(gen_server). @@ -34,16 +36,15 @@ -export([init/1, handle_info/2, handle_call/3, handle_cast/2, terminate/2, code_change/3]). -%% External exports -export([start/1, stop/1, start_link/1, set_password/3, check_password/4, check_password/6, try_register/3, dirty_get_registered_users/0, get_vh_registered_users/1, - get_vh_registered_users/2, - get_vh_registered_users_number/1, + get_vh_registered_users/2, + get_vh_registered_users_number/1, get_vh_registered_users_number/2, get_password/2, get_password_s/2, is_user_exists/2, remove_user/2, - remove_user/3, store_type/0, - plain_password_required/0]). + remove_user/3, store_type/0, plain_password_required/0, + opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -119,13 +120,13 @@ check_password(User, AuthzId, Server, Password) -> if AuthzId /= <<>> andalso AuthzId /= User -> false; true -> - if Password == <<"">> -> false; - true -> - case catch check_password_ldap(User, Server, Password) - of - {'EXIT', _} -> false; - Result -> Result - end + if Password == <<"">> -> false; + true -> + case catch check_password_ldap(User, Server, Password) + of + {'EXIT', _} -> false; + Result -> Result + end end end. @@ -231,11 +232,11 @@ get_vh_registered_users_ldap(Server) -> UIDFormat) of {ok, U} -> - case jlib:nodeprep(U) of + case jid:nodeprep(U) of error -> []; LU -> [{LU, - jlib:nameprep(Server)}] + jid:nameprep(Server)}] end; _ -> [] end @@ -364,7 +365,7 @@ parse_options(Host) -> Eldap_ID = jlib:atom_to_binary(gen_mod:get_module_proc(Host, ?MODULE)), Bind_Eldap_ID = jlib:atom_to_binary( gen_mod:get_module_proc(Host, bind_ejabberd_auth_ldap)), - UIDsTemp = eldap_utils:get_opt( + UIDsTemp = gen_mod:get_opt( {ldap_uids, Host}, [], fun(Us) -> lists:map( @@ -379,7 +380,7 @@ parse_options(Host) -> end, [{<<"uid">>, <<"%u">>}]), UIDs = eldap_utils:uids_domain_subst(Host, UIDsTemp), SubFilter = eldap_utils:generate_subfilter(UIDs), - UserFilter = case eldap_utils:get_opt( + UserFilter = case gen_mod:get_opt( {ldap_filter, Host}, [], fun check_filter/1, <<"">>) of <<"">> -> @@ -390,7 +391,7 @@ parse_options(Host) -> SearchFilter = eldap_filter:do_sub(UserFilter, [{<<"%u">>, <<"*">>}]), {DNFilter, DNFilterAttrs} = - eldap_utils:get_opt({ldap_dn_filter, Host}, [], + gen_mod:get_opt({ldap_dn_filter, Host}, [], fun([{DNF, DNFA}]) -> NewDNFA = case DNFA of undefined -> @@ -402,7 +403,7 @@ parse_options(Host) -> NewDNF = check_filter(DNF), {NewDNF, NewDNFA} end, {undefined, []}), - LocalFilter = eldap_utils:get_opt( + LocalFilter = gen_mod:get_opt( {ldap_local_filter, Host}, [], fun(V) -> V end), #state{host = Host, eldap_id = Eldap_ID, bind_eldap_id = Bind_Eldap_ID, @@ -422,3 +423,27 @@ check_filter(F) -> NewF = iolist_to_binary(F), {ok, _} = eldap_filter:parse(NewF), NewF. + +opt_type(ldap_dn_filter) -> + fun ([{DNF, DNFA}]) -> + NewDNFA = case DNFA of + undefined -> []; + _ -> [iolist_to_binary(A) || A <- DNFA] + end, + NewDNF = check_filter(DNF), + {NewDNF, NewDNFA} + end; +opt_type(ldap_filter) -> fun check_filter/1; +opt_type(ldap_local_filter) -> fun (V) -> V end; +opt_type(ldap_uids) -> + fun (Us) -> + lists:map(fun ({U, P}) -> + {iolist_to_binary(U), iolist_to_binary(P)}; + ({U}) -> {iolist_to_binary(U)}; + (U) -> {iolist_to_binary(U)} + end, + lists:flatten(Us)) + end; +opt_type(_) -> + [ldap_dn_filter, ldap_filter, ldap_local_filter, + ldap_uids]. diff --git a/src/ejabberd_auth_odbc.erl b/src/ejabberd_auth_odbc.erl index b3bcd369..dc3248fe 100644 --- a/src/ejabberd_auth_odbc.erl +++ b/src/ejabberd_auth_odbc.erl @@ -5,7 +5,7 @@ %%% Created : 12 Dec 2004 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,11 +25,12 @@ -module(ejabberd_auth_odbc). +-behaviour(ejabberd_config). + -author('alexey@process-one.net'). -behaviour(ejabberd_auth). -%% External exports -export([start/1, set_password/3, check_password/4, check_password/6, try_register/3, dirty_get_registered_users/0, get_vh_registered_users/1, @@ -37,9 +38,8 @@ get_vh_registered_users_number/1, get_vh_registered_users_number/2, get_password/2, get_password_s/2, is_user_exists/2, remove_user/2, - remove_user/3, store_type/0, - plain_password_required/0, - convert_to_scram/1]). + remove_user/3, store_type/0, plain_password_required/0, + convert_to_scram/1, opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -68,51 +68,47 @@ check_password(User, AuthzId, Server, Password) -> if AuthzId /= <<>> andalso AuthzId /= User -> false; true -> - LServer = jlib:nameprep(Server), - LUser = jlib:nodeprep(User), - if (LUser == error) or (LServer == error) -> - false; - (LUser == <<>>) or (LServer == <<>>) -> - false; - true -> - Username = ejabberd_odbc:escape(LUser), - case is_scrammed() of - true -> - try odbc_queries:get_password_scram(LServer, Username) of - {selected, [<<"password">>, <<"serverkey">>, - <<"salt">>, <<"iterationcount">>], - [[StoredKey, ServerKey, Salt, IterationCount]]} -> - Scram = - #scram{storedkey = StoredKey, - serverkey = ServerKey, - salt = Salt, - iterationcount = jlib:binary_to_integer( - IterationCount)}, - is_password_scram_valid(Password, Scram); - {selected, [<<"password">>, <<"serverkey">>, - <<"salt">>, <<"iterationcount">>], []} -> - false; %% Account does not exist - {error, _Error} -> - false %% Typical error is that table doesn't exist - catch - _:_ -> - false %% Typical error is database not accessible - end; - false -> - try odbc_queries:get_password(LServer, Username) of - {selected, [<<"password">>], [[Password]]} -> - Password /= <<"">>; - {selected, [<<"password">>], [[_Password2]]} -> - false; %% Password is not correct - {selected, [<<"password">>], []} -> - false; %% Account does not exist - {error, _Error} -> - false %% Typical error is that table doesn't exist - catch - _:_ -> - false %% Typical error is database not accessible - end - end + LServer = jid:nameprep(Server), + LUser = jid:nodeprep(User), + if (LUser == error) or (LServer == error) -> + false; + (LUser == <<>>) or (LServer == <<>>) -> + false; + true -> + case is_scrammed() of + true -> + try odbc_queries:get_password_scram(LServer, LUser) of + {selected, + [{StoredKey, ServerKey, Salt, IterationCount}]} -> + Scram = + #scram{storedkey = StoredKey, + serverkey = ServerKey, + salt = Salt, + iterationcount = IterationCount}, + is_password_scram_valid(Password, Scram); + {selected, []} -> + false; %% Account does not exist + {error, _Error} -> + false %% Typical error is that table doesn't exist + catch + _:_ -> + false %% Typical error is database not accessible + end; + false -> + try odbc_queries:get_password(LServer, LUser) of + {selected, [{Password}]} -> + Password /= <<"">>; + {selected, [{_Password2}]} -> + false; %% Password is not correct + {selected, []} -> + false; %% Account does not exist + {error, _Error} -> + false %% Typical error is that table doesn't exist + catch + _:_ -> + false %% Typical error is database not accessible + end + end end end. @@ -122,70 +118,67 @@ check_password(User, AuthzId, Server, Password, Digest, if AuthzId /= <<>> andalso AuthzId /= User -> false; true -> - LServer = jlib:nameprep(Server), - LUser = jlib:nodeprep(User), - if (LUser == error) or (LServer == error) -> - false; - (LUser == <<>>) or (LServer == <<>>) -> - false; - true -> - case is_scrammed() of - false -> - Username = ejabberd_odbc:escape(LUser), - try odbc_queries:get_password(LServer, Username) of - %% Account exists, check if password is valid - {selected, [<<"password">>], [[Passwd]]} -> - DigRes = if Digest /= <<"">> -> - Digest == DigestGen(Passwd); - true -> false - end, - if DigRes -> true; - true -> (Passwd == Password) and (Password /= <<"">>) - end; - {selected, [<<"password">>], []} -> - false; %% Account does not exist - {error, _Error} -> - false %% Typical error is that table doesn't exist - catch - _:_ -> - false %% Typical error is database not accessible - end; - true -> - false - end + LServer = jid:nameprep(Server), + LUser = jid:nodeprep(User), + if (LUser == error) or (LServer == error) -> + false; + (LUser == <<>>) or (LServer == <<>>) -> + false; + true -> + case is_scrammed() of + false -> + try odbc_queries:get_password(LServer, LUser) of + %% Account exists, check if password is valid + {selected, [{Passwd}]} -> + DigRes = if Digest /= <<"">> -> + Digest == DigestGen(Passwd); + true -> false + end, + if DigRes -> true; + true -> (Passwd == Password) and (Password /= <<"">>) + end; + {selected, []} -> + false; %% Account does not exist + {error, _Error} -> + false %% Typical error is that table doesn't exist + catch + _:_ -> + false %% Typical error is database not accessible + end; + true -> + false + end end end. %% @spec (User::string(), Server::string(), Password::string()) -> %% ok | {error, invalid_jid} set_password(User, Server, Password) -> - LServer = jlib:nameprep(Server), - LUser = jlib:nodeprep(User), + LServer = jid:nameprep(Server), + LUser = jid:nodeprep(User), if (LUser == error) or (LServer == error) -> {error, invalid_jid}; (LUser == <<>>) or (LServer == <<>>) -> {error, invalid_jid}; true -> - Username = ejabberd_odbc:escape(LUser), case is_scrammed() of true -> Scram = password_to_scram(Password), case catch odbc_queries:set_password_scram_t( LServer, - Username, - ejabberd_odbc:escape(Scram#scram.storedkey), - ejabberd_odbc:escape(Scram#scram.serverkey), - ejabberd_odbc:escape(Scram#scram.salt), - jlib:integer_to_binary(Scram#scram.iterationcount) + LUser, + Scram#scram.storedkey, + Scram#scram.serverkey, + Scram#scram.salt, + Scram#scram.iterationcount ) of {atomic, ok} -> ok; Other -> {error, Other} end; false -> - Pass = ejabberd_odbc:escape(Password), case catch odbc_queries:set_password_t(LServer, - Username, Pass) + LUser, Password) of {atomic, ok} -> ok; Other -> {error, Other} @@ -195,33 +188,30 @@ set_password(User, Server, Password) -> %% @spec (User, Server, Password) -> {atomic, ok} | {atomic, exists} | {error, invalid_jid} try_register(User, Server, Password) -> - LServer = jlib:nameprep(Server), - LUser = jlib:nodeprep(User), + LServer = jid:nameprep(Server), + LUser = jid:nodeprep(User), if (LUser == error) or (LServer == error) -> {error, invalid_jid}; (LUser == <<>>) or (LServer == <<>>) -> {error, invalid_jid}; true -> - Username = ejabberd_odbc:escape(LUser), case is_scrammed() of true -> Scram = password_to_scram(Password), case catch odbc_queries:add_user_scram( LServer, - Username, - ejabberd_odbc:escape(Scram#scram.storedkey), - ejabberd_odbc:escape(Scram#scram.serverkey), - ejabberd_odbc:escape(Scram#scram.salt), - jlib:integer_to_binary(Scram#scram.iterationcount) + LUser, + Scram#scram.storedkey, + Scram#scram.serverkey, + Scram#scram.salt, + Scram#scram.iterationcount ) of {updated, 1} -> {atomic, ok}; _ -> {atomic, exists} end; false -> - Pass = ejabberd_odbc:escape(Password), - case catch odbc_queries:add_user(LServer, Username, - Pass) - of + case catch odbc_queries:add_user(LServer, LUser, + Password) of {updated, 1} -> {atomic, ok}; _ -> {atomic, exists} end @@ -236,71 +226,85 @@ dirty_get_registered_users() -> Servers). get_vh_registered_users(Server) -> - LServer = jlib:nameprep(Server), - case catch odbc_queries:list_users(LServer) of - {selected, [<<"username">>], Res} -> - [{U, LServer} || [U] <- Res]; - _ -> [] + case jid:nameprep(Server) of + error -> []; + <<>> -> []; + LServer -> + case catch odbc_queries:list_users(LServer) of + {selected, Res} -> + [{U, LServer} || {U} <- Res]; + _ -> [] + end end. get_vh_registered_users(Server, Opts) -> - LServer = jlib:nameprep(Server), - case catch odbc_queries:list_users(LServer, Opts) of - {selected, [<<"username">>], Res} -> - [{U, LServer} || [U] <- Res]; - _ -> [] + case jid:nameprep(Server) of + error -> []; + <<>> -> []; + LServer -> + case catch odbc_queries:list_users(LServer, Opts) of + {selected, Res} -> + [{U, LServer} || {U} <- Res]; + _ -> [] + end end. get_vh_registered_users_number(Server) -> - LServer = jlib:nameprep(Server), - case catch odbc_queries:users_number(LServer) of - {selected, [_], [[Res]]} -> - jlib:binary_to_integer(Res); - _ -> 0 + case jid:nameprep(Server) of + error -> 0; + <<>> -> 0; + LServer -> + case catch odbc_queries:users_number(LServer) of + {selected, [{Res}]} -> + Res; + _ -> 0 + end end. get_vh_registered_users_number(Server, Opts) -> - LServer = jlib:nameprep(Server), - case catch odbc_queries:users_number(LServer, Opts) of - {selected, [_], [[Res]]} -> - jlib:binary_to_integer(Res); - _Other -> 0 + case jid:nameprep(Server) of + error -> 0; + <<>> -> 0; + LServer -> + case catch odbc_queries:users_number(LServer, Opts) of + {selected, [{Res}]} -> + Res; + _Other -> 0 + end end. get_password(User, Server) -> - LServer = jlib:nameprep(Server), - LUser = jlib:nodeprep(User), + LServer = jid:nameprep(Server), + LUser = jid:nodeprep(User), if (LUser == error) or (LServer == error) -> false; (LUser == <<>>) or (LServer == <<>>) -> false; true -> - Username = ejabberd_odbc:escape(LUser), case is_scrammed() of true -> case catch odbc_queries:get_password_scram( - LServer, Username) of - {selected, [<<"password">>, <<"serverkey">>, - <<"salt">>, <<"iterationcount">>], - [[StoredKey, ServerKey, Salt, IterationCount]]} -> + LServer, LUser) of + {selected, + [{StoredKey, ServerKey, Salt, IterationCount}]} -> {jlib:decode_base64(StoredKey), jlib:decode_base64(ServerKey), jlib:decode_base64(Salt), - jlib:binary_to_integer(IterationCount)}; + IterationCount}; _ -> false end; false -> - case catch odbc_queries:get_password(LServer, Username) + case catch odbc_queries:get_password(LServer, LUser) of - {selected, [<<"password">>], [[Password]]} -> Password; + {selected, [{Password}]} -> Password; _ -> false end end end. get_password_s(User, Server) -> - LServer = jlib:nameprep(Server), - LUser = jlib:nodeprep(User), + LServer = jid:nameprep(Server), + LUser = jid:nodeprep(User), if (LUser == error) or (LServer == error) -> <<"">>; (LUser == <<>>) or (LServer == <<>>) -> @@ -308,9 +312,8 @@ get_password_s(User, Server) -> true -> case is_scrammed() of false -> - Username = ejabberd_odbc:escape(LUser), - case catch odbc_queries:get_password(LServer, Username) of - {selected, [<<"password">>], [[Password]]} -> Password; + case catch odbc_queries:get_password(LServer, LUser) of + {selected, [{Password}]} -> Password; _ -> <<"">> end; true -> <<"">> @@ -319,15 +322,17 @@ get_password_s(User, Server) -> %% @spec (User, Server) -> true | false | {error, Error} is_user_exists(User, Server) -> - case jlib:nodeprep(User) of - error -> false; - LUser -> - Username = ejabberd_odbc:escape(LUser), - LServer = jlib:nameprep(Server), - try odbc_queries:get_password(LServer, Username) of - {selected, [<<"password">>], [[_Password]]} -> + LServer = jid:nameprep(Server), + LUser = jid:nodeprep(User), + if (LUser == error) or (LServer == error) -> + false; + (LUser == <<>>) or (LServer == <<>>) -> + false; + true -> + try odbc_queries:get_password(LServer, LUser) of + {selected, [{_Password}]} -> true; %% Account exists - {selected, [<<"password">>], []} -> + {selected, []} -> false; %% Account does not exist {error, Error} -> {error, Error} catch @@ -339,20 +344,22 @@ is_user_exists(User, Server) -> %% @doc Remove user. %% Note: it may return ok even if there was some problem removing the user. remove_user(User, Server) -> - case jlib:nodeprep(User) of - error -> error; - LUser -> - Username = ejabberd_odbc:escape(LUser), - LServer = jlib:nameprep(Server), - catch odbc_queries:del_user(LServer, Username), + LServer = jid:nameprep(Server), + LUser = jid:nodeprep(User), + if (LUser == error) or (LServer == error) -> + error; + (LUser == <<>>) or (LServer == <<>>) -> + error; + true -> + catch odbc_queries:del_user(LServer, LUser), ok end. %% @spec (User, Server, Password) -> ok | error | not_exists | not_allowed %% @doc Remove user if the provided password is correct. remove_user(User, Server, Password) -> - LServer = jlib:nameprep(Server), - LUser = jlib:nodeprep(User), + LServer = jid:nameprep(Server), + LUser = jid:nodeprep(User), if (LUser == error) or (LServer == error) -> error; (LUser == <<>>) or (LServer == <<>>) -> @@ -367,16 +374,12 @@ remove_user(User, Server, Password) -> false -> not_allowed end; false -> - Username = ejabberd_odbc:escape(LUser), - Pass = ejabberd_odbc:escape(Password), F = fun () -> Result = odbc_queries:del_user_return_password( - LServer, Username, Pass), + LServer, LUser, Password), case Result of - {selected, [<<"password">>], - [[Password]]} -> ok; - {selected, [<<"password">>], - []} -> not_exists; + {selected, [{Password}]} -> ok; + {selected, []} -> not_exists; _ -> not_allowed end end, @@ -437,7 +440,7 @@ set_password_scram_t(Username, <<"'">>]). convert_to_scram(Server) -> - LServer = jlib:nameprep(Server), + LServer = jid:nameprep(Server), if LServer == error; LServer == <<>> -> @@ -447,7 +450,7 @@ convert_to_scram(Server) -> case ejabberd_odbc:sql_query_t( [<<"select username, password from users where " "iterationcount=0 limit ">>, - jlib:integer_to_binary(?BATCH_SIZE), + integer_to_binary(?BATCH_SIZE), <<";">>]) of {selected, [<<"username">>, <<"password">>], []} -> ok; @@ -461,7 +464,7 @@ convert_to_scram(Server) -> ejabberd_odbc:escape(Scram#scram.storedkey), ejabberd_odbc:escape(Scram#scram.serverkey), ejabberd_odbc:escape(Scram#scram.salt), - jlib:integer_to_binary(Scram#scram.iterationcount) + integer_to_binary(Scram#scram.iterationcount) ) end, Rs), continue; @@ -475,3 +478,6 @@ convert_to_scram(Server) -> Error -> Error end end. + +opt_type(auth_password_format) -> fun (V) -> V end; +opt_type(_) -> [auth_password_format]. diff --git a/src/ejabberd_auth_pam.erl b/src/ejabberd_auth_pam.erl index da893eb4..fa4b9f07 100644 --- a/src/ejabberd_auth_pam.erl +++ b/src/ejabberd_auth_pam.erl @@ -5,7 +5,7 @@ %%% Created : 5 Jul 2007 by Evgeniy Khramtsov <xram@jabber.ru> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -24,22 +24,21 @@ %%%------------------------------------------------------------------- -module(ejabberd_auth_pam). +-behaviour(ejabberd_config). + -author('xram@jabber.ru'). -behaviour(ejabberd_auth). -%% External exports -%%==================================================================== -%% API -%%==================================================================== -export([start/1, set_password/3, check_password/4, check_password/6, try_register/3, dirty_get_registered_users/0, get_vh_registered_users/1, - get_vh_registered_users/2, get_vh_registered_users_number/1, - get_vh_registered_users_number/2, - get_password/2, get_password_s/2, is_user_exists/2, - remove_user/2, remove_user/3, store_type/0, - plain_password_required/0]). + get_vh_registered_users/2, + get_vh_registered_users_number/1, + get_vh_registered_users_number/2, get_password/2, + get_password_s/2, is_user_exists/2, remove_user/2, + remove_user/3, store_type/0, plain_password_required/0, + opt_type/1]). start(_Host) -> ejabberd:start_app(p1_pam). @@ -55,16 +54,16 @@ check_password(User, AuthzId, Host, Password) -> if AuthzId /= <<>> andalso AuthzId /= User -> false; true -> - Service = get_pam_service(Host), - UserInfo = case get_pam_userinfotype(Host) of - username -> User; - jid -> <<User/binary, "@", Host/binary>> - end, - case catch epam:authenticate(Service, UserInfo, - Password) - of - true -> true; - _ -> false + Service = get_pam_service(Host), + UserInfo = case get_pam_userinfotype(Host) of + username -> User; + jid -> <<User/binary, "@", Host/binary>> + end, + case catch epam:authenticate(Service, UserInfo, + Password) + of + true -> true; + _ -> false end end. @@ -122,3 +121,10 @@ get_pam_userinfotype(Host) -> (jid) -> jid end, username). + +opt_type(pam_service) -> fun iolist_to_binary/1; +opt_type(pam_userinfotype) -> + fun (username) -> username; + (jid) -> jid + end; +opt_type(_) -> [pam_service, pam_userinfotype]. diff --git a/src/ejabberd_auth_riak.erl b/src/ejabberd_auth_riak.erl index 8c926f4b..bc745fea 100644 --- a/src/ejabberd_auth_riak.erl +++ b/src/ejabberd_auth_riak.erl @@ -5,7 +5,7 @@ %%% Created : 12 Nov 2012 by Evgeniy Khramtsov <ekhramtsov@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -70,15 +70,15 @@ check_password(User, AuthzId, Server, Password) -> if AuthzId /= <<>> andalso AuthzId /= User -> false; true -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - case ejabberd_riak:get(passwd, passwd_schema(), {LUser, LServer}) of - {ok, #passwd{password = Password}} when is_binary(Password) -> - Password /= <<"">>; - {ok, #passwd{password = Scram}} when is_record(Scram, scram) -> - is_password_scram_valid(Password, Scram); - _ -> - false + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + case ejabberd_riak:get(passwd, passwd_schema(), {LUser, LServer}) of + {ok, #passwd{password = Password}} when is_binary(Password) -> + Password /= <<"">>; + {ok, #passwd{password = Scram}} when is_record(Scram, scram) -> + is_password_scram_valid(Password, Scram); + _ -> + false end end. @@ -87,34 +87,34 @@ check_password(User, AuthzId, Server, Password, Digest, if AuthzId /= <<>> andalso AuthzId /= User -> false; true -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - case ejabberd_riak:get(passwd, passwd_schema(), {LUser, LServer}) of - {ok, #passwd{password = Passwd}} when is_binary(Passwd) -> - DigRes = if Digest /= <<"">> -> - Digest == DigestGen(Passwd); - true -> false - end, - if DigRes -> true; - true -> (Passwd == Password) and (Password /= <<"">>) - end; - {ok, #passwd{password = Scram}} - when is_record(Scram, scram) -> - Passwd = jlib:decode_base64(Scram#scram.storedkey), - DigRes = if Digest /= <<"">> -> - Digest == DigestGen(Passwd); - true -> false - end, - if DigRes -> true; - true -> (Passwd == Password) and (Password /= <<"">>) - end; - _ -> false + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + case ejabberd_riak:get(passwd, passwd_schema(), {LUser, LServer}) of + {ok, #passwd{password = Passwd}} when is_binary(Passwd) -> + DigRes = if Digest /= <<"">> -> + Digest == DigestGen(Passwd); + true -> false + end, + if DigRes -> true; + true -> (Passwd == Password) and (Password /= <<"">>) + end; + {ok, #passwd{password = Scram}} + when is_record(Scram, scram) -> + Passwd = jlib:decode_base64(Scram#scram.storedkey), + DigRes = if Digest /= <<"">> -> + Digest == DigestGen(Passwd); + true -> false + end, + if DigRes -> true; + true -> (Passwd == Password) and (Password /= <<"">>) + end; + _ -> false end end. set_password(User, Server, Password) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), US = {LUser, LServer}, if (LUser == error) or (LServer == error) -> {error, invalid_jid}; @@ -130,8 +130,8 @@ set_password(User, Server, Password) -> end. try_register(User, Server, PasswordList) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), Password = if is_list(PasswordList); is_binary(PasswordList) -> iolist_to_binary(PasswordList); true -> PasswordList @@ -167,7 +167,7 @@ dirty_get_registered_users() -> end, ejabberd_config:get_vh_by_auth_method(riak)). get_vh_registered_users(Server) -> - LServer = jlib:nameprep(Server), + LServer = jid:nameprep(Server), case ejabberd_riak:get_keys_by_index(passwd, <<"host">>, LServer) of {ok, Users} -> Users; @@ -179,7 +179,7 @@ get_vh_registered_users(Server, _) -> get_vh_registered_users(Server). get_vh_registered_users_number(Server) -> - LServer = jlib:nameprep(Server), + LServer = jid:nameprep(Server), case ejabberd_riak:count_by_index(passwd, <<"host">>, LServer) of {ok, N} -> N; @@ -191,8 +191,8 @@ get_vh_registered_users_number(Server, _) -> get_vh_registered_users_number(Server). get_password(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), case ejabberd_riak:get(passwd, passwd_schema(), {LUser, LServer}) of {ok, #passwd{password = Password}} when is_binary(Password) -> @@ -207,8 +207,8 @@ get_password(User, Server) -> end. get_password_s(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), case ejabberd_riak:get(passwd, passwd_schema(), {LUser, LServer}) of {ok, #passwd{password = Password}} when is_binary(Password) -> @@ -220,8 +220,8 @@ get_password_s(User, Server) -> end. is_user_exists(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), case ejabberd_riak:get(passwd, passwd_schema(), {LUser, LServer}) of {error, notfound} -> false; {ok, _} -> true; @@ -229,14 +229,14 @@ is_user_exists(User, Server) -> end. remove_user(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), ejabberd_riak:delete(passwd, {LUser, LServer}), ok. remove_user(User, Server, Password) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), case ejabberd_riak:get(passwd, passwd_schema(), {LUser, LServer}) of {ok, #passwd{password = Password}} when is_binary(Password) -> diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl index 22e17bee..936abc7a 100644 --- a/src/ejabberd_c2s.erl +++ b/src/ejabberd_c2s.erl @@ -5,7 +5,7 @@ %%% Created : 16 Nov 2002 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,8 +25,14 @@ -module(ejabberd_c2s). +-behaviour(ejabberd_config). + -author('alexey@process-one.net'). +-protocol({xep, 78, '2.5'}). +-protocol({xep, 138, '2.0'}). +-protocol({xep, 198, '1.3'}). + -update_info({update, 0}). -define(GEN_FSM, p1_fsm). @@ -37,6 +43,7 @@ -export([start/2, stop/1, start_link/2, + close/1, send_text/2, send_element/2, socket_type/0, @@ -50,23 +57,12 @@ get_subscribed/1, transform_listen_option/2]). -%% gen_fsm callbacks --export([init/1, - wait_for_stream/2, - wait_for_auth/2, - wait_for_feature_request/2, - wait_for_bind/2, - wait_for_session/2, +-export([init/1, wait_for_stream/2, wait_for_auth/2, + wait_for_feature_request/2, wait_for_bind/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, - print_state/1 - ]). + wait_for_resume/2, session_established/2, + handle_event/3, handle_sync_event/4, code_change/4, + handle_info/3, terminate/3, print_state/1, opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -115,9 +111,11 @@ mgmt_max_queue, mgmt_pending_since, mgmt_timeout, + mgmt_max_timeout, mgmt_resend, mgmt_stanzas_in = 0, mgmt_stanzas_out = 0, + ask_offline = true, lang = <<"">>}). %-define(DBGFSM, true). @@ -132,26 +130,17 @@ -endif. -%% Module start with or without supervisor: --ifdef(NO_TRANSIENT_SUPERVISORS). --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])). --endif. - %% This is the timeout to apply between event when starting a new %% session: -define(C2S_OPEN_TIMEOUT, 60000). --define(C2S_HIBERNATE_TIMEOUT, 90000). +-define(C2S_HIBERNATE_TIMEOUT, ejabberd_config:get_option(c2s_hibernate, fun(X) when is_integer(X); X == hibernate-> X end, 90000)). -define(STREAM_HEADER, <<"<?xml version='1.0'?><stream:stream " "xmlns='jabber:client' xmlns:stream='http://et" - "herx.jabber.org/streams' id='~s' from='~s'~s~" - "s>">>). + "herx.jabber.org/streams' id='~s' from='~s'~s" + "~s>">>). -define(STREAM_TRAILER, <<"</stream:stream>">>). @@ -169,14 +158,14 @@ %% XEP-0198: -define(IS_STREAM_MGMT_TAG(Name), - Name == <<"enable">>; - Name == <<"resume">>; - Name == <<"a">>; - Name == <<"r">>). + (Name == <<"enable">>) or + (Name == <<"resume">>) or + (Name == <<"a">>) or + (Name == <<"r">>)). -define(IS_SUPPORTED_MGMT_XMLNS(Xmlns), - Xmlns == ?NS_STREAM_MGMT_2; - Xmlns == ?NS_STREAM_MGMT_3). + (Xmlns == ?NS_STREAM_MGMT_2) or + (Xmlns == ?NS_STREAM_MGMT_3)). -define(MGMT_FAILED(Condition, Xmlns), #xmlel{name = <<"failed">>, @@ -204,11 +193,14 @@ %%% API %%%---------------------------------------------------------------------- start(SockData, Opts) -> - ?SUPERVISOR_START. + ?GEN_FSM:start(ejabberd_c2s, + [SockData, Opts], + fsm_limit_opts(Opts) ++ ?FSMOPTS). start_link(SockData, Opts) -> - ?GEN_FSM:start_link(ejabberd_c2s, [SockData, Opts], - fsm_limit_opts(Opts) ++ ?FSMOPTS). + (?GEN_FSM):start_link(ejabberd_c2s, + [SockData, Opts], + fsm_limit_opts(Opts) ++ ?FSMOPTS). socket_type() -> xml_stream. @@ -233,7 +225,7 @@ del_aux_field(Key, #state{aux_fields = Opts} = State) -> State#state{aux_fields = Opts1}. get_subscription(From = #jid{}, StateData) -> - get_subscription(jlib:jid_tolower(From), StateData); + get_subscription(jid:tolower(From), StateData); get_subscription(LFrom, StateData) -> LBFrom = setelement(3, LFrom, <<"">>), F = (?SETS):is_element(LFrom, StateData#state.pres_f) @@ -254,19 +246,14 @@ send_filtered(FsmRef, Feature, From, To, Packet) -> broadcast(FsmRef, Type, From, Packet) -> FsmRef ! {broadcast, Type, From, Packet}. -stop(FsmRef) -> (?GEN_FSM):send_event(FsmRef, closed). +stop(FsmRef) -> (?GEN_FSM):send_event(FsmRef, stop). + +close(FsmRef) -> (?GEN_FSM):send_event(FsmRef, closed). %%%---------------------------------------------------------------------- %%% Callback functions from gen_fsm %%%---------------------------------------------------------------------- -%%---------------------------------------------------------------------- -%% Func: init/1 -%% Returns: {ok, StateName, StateData} | -%% {ok, StateName, StateData, Timeout} | -%% ignore | -%% {stop, StopReason} -%%---------------------------------------------------------------------- init([{SockMod, Socket}, Opts]) -> Access = case lists:keysearch(access, 1, Opts) of {value, {_, A}} -> A; @@ -288,6 +275,7 @@ init([{SockMod, Socket}, Opts]) -> StartTLSRequired orelse TLSEnabled, TLSOpts1 = lists:filter(fun ({certfile, _}) -> true; ({ciphers, _}) -> true; + ({dhfile, _}) -> true; (_) -> false end, Opts), @@ -312,12 +300,16 @@ init([{SockMod, Socket}, Opts]) -> MaxAckQueue = case proplists:get_value(max_ack_queue, Opts) of Limit when is_integer(Limit), Limit > 0 -> Limit; infinity -> infinity; - _ -> 500 + _ -> 1000 end, ResumeTimeout = case proplists:get_value(resume_timeout, Opts) of Timeout when is_integer(Timeout), Timeout >= 0 -> Timeout; _ -> 300 end, + MaxResumeTimeout = case proplists:get_value(max_resume_timeout, Opts) of + Max when is_integer(Max), Max >= ResumeTimeout -> Max; + _ -> ResumeTimeout + end, ResendOnTimeout = case proplists:get_value(resend_on_timeout, Opts) of Resend when is_boolean(Resend) -> Resend; if_offline -> if_offline; @@ -335,11 +327,12 @@ init([{SockMod, Socket}, Opts]) -> xml_socket = XMLSocket, zlib = Zlib, tls = TLS, tls_required = StartTLSRequired, tls_enabled = TLSEnabled, tls_options = TLSOpts, - sid = {now(), self()}, streamid = new_id(), + sid = {p1_time_compat:timestamp(), self()}, streamid = new_id(), access = Access, shaper = Shaper, ip = IP, mgmt_state = StreamMgmtState, mgmt_max_queue = MaxAckQueue, mgmt_timeout = ResumeTimeout, + mgmt_max_timeout = MaxResumeTimeout, mgmt_resend = ResendOnTimeout}, {ok, wait_for_stream, StateData, ?C2S_OPEN_TIMEOUT}. @@ -348,41 +341,34 @@ 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} -%%---------------------------------------------------------------------- - wait_for_stream({xmlstreamstart, _Name, Attrs}, StateData) -> DefaultLang = ?MYLANG, - case xml:get_attr_s(<<"xmlns:stream">>, Attrs) of + case fxml:get_attr_s(<<"xmlns:stream">>, Attrs) of ?NS_STREAM -> - 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, + Server = + case StateData#state.server of + <<"">> -> + jid:nameprep(fxml:get_attr_s(<<"to">>, Attrs)); + S -> S + end, + Lang = case fxml: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 when IsBlacklistedIP == false -> - change_shaper(StateData, jlib:make_jid(<<"">>, Server, <<"">>)), - case xml:get_attr_s(<<"version">>, Attrs) of + change_shaper(StateData, jid:make(<<"">>, Server, <<"">>)), + case fxml:get_attr_s(<<"version">>, Attrs) of <<"1.0">> -> send_header(StateData, Server, <<"1.0">>, DefaultLang), case StateData#state.authenticated of @@ -390,168 +376,180 @@ wait_for_stream({xmlstreamstart, _Name, Attrs}, StateData) -> TLS = StateData#state.tls, TLSEnabled = StateData#state.tls_enabled, TLSRequired = StateData#state.tls_required, - SASLState = - cyrsasl:server_new( - <<"jabber">>, Server, <<"">>, [], - fun(U) -> - ejabberd_auth:get_password_with_authmodule( - U, Server) - end, + SASLState = cyrsasl:server_new( + <<"jabber">>, Server, <<"">>, [], + fun (U) -> + ejabberd_auth:get_password_with_authmodule( + U, Server) + end, fun(U, AuthzId, P) -> - ejabberd_auth:check_password_with_authmodule( + ejabberd_auth:check_password_with_authmodule( U, AuthzId, Server, P) - end, + end, fun(U, AuthzId, P, D, DG) -> - ejabberd_auth:check_password_with_authmodule( + ejabberd_auth:check_password_with_authmodule( U, AuthzId, Server, P, D, DG) - end), + end), 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, + 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), + (StateData#state.sockmod):get_sockmod(StateData#state.socket), Zlib = StateData#state.zlib, - CompressFeature = - case Zlib andalso - ((SockMod == gen_tcp) orelse - (SockMod == p1_tls)) of - true -> - [#xmlel{name = <<"compression">>, - attrs = [{<<"xmlns">>, ?NS_FEATURE_COMPRESS}], - children = [#xmlel{name = <<"method">>, - attrs = [], - children = [{xmlcdata, <<"zlib">>}]}]}]; - _ -> - [] - end, + CompressFeature = case Zlib andalso + ((SockMod == gen_tcp) orelse (SockMod == fast_tls)) of + true -> + [#xmlel{name = <<"compression">>, + attrs = [{<<"xmlns">>, ?NS_FEATURE_COMPRESS}], + children = [#xmlel{name = <<"method">>, + attrs = [], + children = [{xmlcdata, <<"zlib">>}]}]}]; + _ -> + [] + end, TLSFeature = case (TLS == true) andalso - (TLSEnabled == false) andalso - (SockMod == gen_tcp) of - true -> - case TLSRequired of - true -> - [#xmlel{name = <<"starttls">>, - attrs = [{<<"xmlns">>, ?NS_TLS}], - children = [#xmlel{name = <<"required">>, - attrs = [], - children = []}]}]; - _ -> - [#xmlel{name = <<"starttls">>, - attrs = [{<<"xmlns">>, ?NS_TLS}], - children = []}] - end; - false -> - [] - end, + (TLSEnabled == false) andalso + (SockMod == gen_tcp) of + true -> + case TLSRequired of + true -> + [#xmlel{name = <<"starttls">>, + attrs = [{<<"xmlns">>, ?NS_TLS}], + children = [#xmlel{name = <<"required">>, + attrs = [], + children = []}]}]; + _ -> + [#xmlel{name = <<"starttls">>, + attrs = [{<<"xmlns">>, ?NS_TLS}], + children = []}] + end; + false -> + [] + end, + StreamFeatures1 = TLSFeature ++ CompressFeature ++ Mechs, + StreamFeatures = ejabberd_hooks:run_fold(c2s_stream_features, + Server, StreamFeatures1, [Server]), send_element(StateData, - #xmlel{name = <<"stream:features">>, - attrs = [], - children = - TLSFeature ++ CompressFeature ++ Mechs - ++ - ejabberd_hooks:run_fold(c2s_stream_features, - Server, [], [Server])}), + #xmlel{name = <<"stream:features">>, + attrs = [], + children = StreamFeatures}), fsm_next_state(wait_for_feature_request, - StateData#state{ - server = Server, - sasl_state = SASLState, - lang = Lang}); + StateData#state{server = Server, + sasl_state = SASLState, + lang = Lang}); _ -> case StateData#state.resource of - <<"">> -> - 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 -> - [] + <<"">> -> + 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}) + SockMod = + (StateData#state.sockmod):get_sockmod( + StateData#state.socket), + Zlib = StateData#state.zlib, + CompressFeature = + case Zlib andalso + ((SockMod == gen_tcp) orelse (SockMod == fast_tls)) of + true -> + [#xmlel{name = <<"compression">>, + attrs = [{<<"xmlns">>, ?NS_FEATURE_COMPRESS}], + children = [#xmlel{name = <<"method">>, + attrs = [], + children = [{xmlcdata, <<"zlib">>}]}]}]; + _ -> + [] + end, + StreamFeatures1 = [#xmlel{name = <<"bind">>, + attrs = [{<<"xmlns">>, ?NS_BIND}], + children = []}, + #xmlel{name = <<"session">>, + attrs = [{<<"xmlns">>, ?NS_SESSION}], + children = + [#xmlel{name = <<"optional">>}]}] + ++ + RosterVersioningFeature ++ + StreamManagementFeature ++ + CompressFeature ++ + ejabberd_hooks:run_fold(c2s_post_auth_features, + Server, [], [Server]), + StreamFeatures = ejabberd_hooks:run_fold(c2s_stream_features, + Server, StreamFeatures1, [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(session_established, + StateData#state{server = Server, lang = Lang}) end end; - _ -> - send_header(StateData, Server, <<"">>, DefaultLang), - if not StateData#state.tls_enabled and - StateData#state.tls_required -> - send_element(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, + <<"Use of STARTTLS required">>)), + send_trailer(StateData), + {stop, normal, StateData}; + true -> + fsm_next_state(wait_for_auth, StateData#state{server = Server, - lang = Lang}) - end + 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}; + _ -> + send_header(StateData, ?MYNAME, <<"">>, DefaultLang), + send_element(StateData, ?HOST_UNKNOWN_ERR), + send_trailer(StateData), + {stop, normal, StateData} 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}; _ -> send_header(StateData, ?MYNAME, <<"">>, DefaultLang), - send_element(StateData, ?HOST_UNKNOWN_ERR), + send_element(StateData, ?INVALID_NS_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}; @@ -569,154 +567,153 @@ wait_for_stream({xmlstreamerror, _}, StateData) -> send_trailer(StateData), {stop, normal, StateData}; wait_for_stream(closed, StateData) -> + {stop, normal, StateData}; +wait_for_stream(stop, StateData) -> {stop, normal, StateData}. wait_for_auth({xmlstreamelement, #xmlel{name = Name} = El}, StateData) - when ?IS_STREAM_MGMT_TAG(Name) -> + 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, _, _, _}} -> - #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, + {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 = jid:make(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, U, - StateData#state.server, - P, D, DGen) + 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}, + {true, AuthModule} -> + ?INFO_MSG("(~w) Accepted legacy authentication for ~s by ~p from ~s", + [StateData#state.socket, + jid:to_string(JID), AuthModule, + ejabberd_config:may_hide_data(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) + 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 = jid:tolower(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, + jid:to_string(JID), + ejabberd_config:may_hide_data(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, + jid:to_string(JID), + ejabberd_config:may_hide_data(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}; @@ -727,6 +724,8 @@ wait_for_auth({xmlstreamerror, _}, StateData) -> send_trailer(StateData), {stop, normal, StateData}; wait_for_auth(closed, StateData) -> + {stop, normal, StateData}; +wait_for_auth(stop, StateData) -> {stop, normal, StateData}. wait_for_feature_request({xmlstreamelement, #xmlel{name = Name} = El}, @@ -743,11 +742,11 @@ wait_for_feature_request({xmlstreamelement, El}, TLSRequired = StateData#state.tls_required, SockMod = (StateData#state.sockmod):get_sockmod(StateData#state.socket), - case {xml:get_attr_s(<<"xmlns">>, Attrs), Name} of + case {fxml: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)), + Mech = fxml:get_attr_s(<<"mechanism">>, Attrs), + ClientIn = jlib:decode_base64(fxml:get_cdata(Els)), case cyrsasl:server_start(StateData#state.sasl_state, Mech, ClientIn) of @@ -758,7 +757,7 @@ wait_for_feature_request({xmlstreamelement, El}, ?INFO_MSG("(~w) Accepted authentication for ~s " "by ~p from ~s", [StateData#state.socket, U, AuthModule, - jlib:ip_to_list(StateData#state.ip)]), + ejabberd_config:may_hide_data(jlib:ip_to_list(StateData#state.ip))]), ejabberd_hooks:run(c2s_auth_result, StateData#state.server, [true, U, StateData#state.server, StateData#state.ip]), @@ -782,10 +781,10 @@ wait_for_feature_request({xmlstreamelement, El}, 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)]), + ?INFO_MSG("(~w) Failed authentication for ~s@~s from ~s", + [StateData#state.socket, + Username, StateData#state.server, + ejabberd_config:may_hide_data(jlib:ip_to_list(StateData#state.ip))]), ejabberd_hooks:run(c2s_auth_result, StateData#state.server, [false, Username, StateData#state.server, StateData#state.ip]), @@ -819,7 +818,7 @@ wait_for_feature_request({xmlstreamelement, El}, StateData#state.tls_options)] end, Socket = StateData#state.socket, - BProceed = xml:element_to_binary(#xmlel{name = <<"proceed">>, + BProceed = fxml:element_to_binary(#xmlel{name = <<"proceed">>, attrs = [{<<"xmlns">>, ?NS_TLS}]}), TLSSocket = (StateData#state.sockmod):starttls(Socket, TLSOpts, @@ -830,39 +829,8 @@ wait_for_feature_request({xmlstreamelement, El}, 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; + (SockMod == gen_tcp) or (SockMod == fast_tls) -> + process_compression_request(El, wait_for_feature_request, StateData); _ -> if TLSRequired and not TLSEnabled -> Lang = StateData#state.lang, @@ -887,6 +855,8 @@ wait_for_feature_request({xmlstreamerror, _}, send_trailer(StateData), {stop, normal, StateData}; wait_for_feature_request(closed, StateData) -> + {stop, normal, StateData}; +wait_for_feature_request(stop, StateData) -> {stop, normal, StateData}. wait_for_sasl_response({xmlstreamelement, #xmlel{name = Name} = El}, StateData) @@ -895,9 +865,9 @@ wait_for_sasl_response({xmlstreamelement, #xmlel{name = Name} = 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 + case {fxml:get_attr_s(<<"xmlns">>, Attrs), Name} of {?NS_SASL, <<"response">>} -> - ClientIn = jlib:decode_base64(xml:get_cdata(Els)), + ClientIn = jlib:decode_base64(fxml:get_cdata(Els)), case cyrsasl:server_step(StateData#state.sasl_state, ClientIn) of @@ -909,7 +879,7 @@ wait_for_sasl_response({xmlstreamelement, El}, ?INFO_MSG("(~w) Accepted authentication for ~s " "by ~p from ~s", [StateData#state.socket, U, AuthModule, - jlib:ip_to_list(StateData#state.ip)]), + ejabberd_config:may_hide_data(jlib:ip_to_list(StateData#state.ip))]), ejabberd_hooks:run(c2s_auth_result, StateData#state.server, [true, U, StateData#state.server, StateData#state.ip]), @@ -930,7 +900,7 @@ wait_for_sasl_response({xmlstreamelement, El}, ?INFO_MSG("(~w) Accepted authentication for ~s " "by ~p from ~s", [StateData#state.socket, U, AuthModule, - jlib:ip_to_list(StateData#state.ip)]), + ejabberd_config:may_hide_data(jlib:ip_to_list(StateData#state.ip))]), ejabberd_hooks:run(c2s_auth_result, StateData#state.server, [true, U, StateData#state.server, StateData#state.ip]), @@ -959,7 +929,7 @@ wait_for_sasl_response({xmlstreamelement, El}, ?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_config:may_hide_data(jlib:ip_to_list(StateData#state.ip))]), ejabberd_hooks:run(c2s_auth_result, StateData#state.server, [false, Username, StateData#state.server, StateData#state.ip]), @@ -994,6 +964,8 @@ wait_for_sasl_response({xmlstreamerror, _}, send_trailer(StateData), {stop, normal, StateData}; wait_for_sasl_response(closed, StateData) -> + {stop, normal, StateData}; +wait_for_sasl_response(stop, StateData) -> {stop, normal, StateData}. resource_conflict_action(U, S, R) -> @@ -1021,9 +993,7 @@ resource_conflict_action(U, S, R) -> acceptnew -> {accept_resource, R}; closenew -> closenew; setresource -> - Rnew = iolist_to_binary([randoms:get_string() - | [jlib:integer_to_binary(X) - || X <- tuple_to_list(now())]]), + Rnew = new_uniq_id(), {accept_resource, Rnew} end. @@ -1046,14 +1016,11 @@ wait_for_bind({xmlstreamelement, El}, StateData) -> #iq{type = set, xmlns = ?NS_BIND, sub_el = SubEl} = IQ -> U = StateData#state.user, - R1 = xml:get_path_s(SubEl, + R1 = fxml:get_path_s(SubEl, [{elem, <<"resource">>}, cdata]), - R = case jlib:resourceprep(R1) of + R = case jid:resourceprep(R1) of error -> error; - <<"">> -> - iolist_to_binary([randoms:get_string() - | [jlib:integer_to_binary(X) - || X <- tuple_to_list(now())]]); + <<"">> -> new_uniq_id(); Resource -> Resource end, case R of @@ -1073,23 +1040,47 @@ wait_for_bind({xmlstreamelement, El}, StateData) -> 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}) + JID = jid:make(U, StateData#state.server, R2), + StateData2 = + StateData#state{resource = R2, jid = JID}, + case open_session(StateData2) of + {ok, StateData3} -> + Res = + IQ#iq{ + type = result, + sub_el = + [#xmlel{name = <<"bind">>, + attrs = [{<<"xmlns">>, ?NS_BIND}], + children = + [#xmlel{name = <<"jid">>, + attrs = [], + children = + [{xmlcdata, + jid:to_string(JID)}]}]}]}, + send_element(StateData3, jlib:iq_to_xml(Res)), + fsm_next_state_pack( + session_established, + StateData3); + {error, Error} -> + Err = jlib:make_error_reply(El, Error), + send_element(StateData, Err), + fsm_next_state(wait_for_bind, StateData) + end end end; - _ -> fsm_next_state(wait_for_bind, StateData) + _ -> + #xmlel{name = Name, attrs = Attrs, children = _Els} = El, + Zlib = StateData#state.zlib, + SockMod = + (StateData#state.sockmod):get_sockmod(StateData#state.socket), + case {fxml:get_attr_s(<<"xmlns">>, Attrs), Name} of + {?NS_COMPRESS, <<"compress">>} + when Zlib == true, + (SockMod == gen_tcp) or (SockMod == fast_tls) -> + process_compression_request(El, wait_for_bind, StateData); + _ -> + fsm_next_state(wait_for_bind, StateData) + end end; wait_for_bind(timeout, StateData) -> {stop, normal, StateData}; @@ -1100,78 +1091,54 @@ wait_for_bind({xmlstreamerror, _}, StateData) -> 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 = 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", - [NewStateData#state.socket, - jlib:jid_to_string(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, - NewState#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, 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}, - fsm_next_state_pack(session_established, - UpdatedStateData); - _ -> - ejabberd_hooks:run(forbidden_session_hook, - NewStateData#state.server, [JID]), - ?INFO_MSG("(~w) Forbidden session for ~s", - [NewStateData#state.socket, - jlib:jid_to_string(JID)]), - Err = jlib:make_error_reply(El, ?ERR_NOT_ALLOWED), - send_element(NewStateData, Err), - fsm_next_state(wait_for_session, NewStateData) - end; - _ -> - fsm_next_state(wait_for_session, NewStateData) - end; - -wait_for_session(timeout, StateData) -> {stop, normal, StateData}; -wait_for_session({xmlstreamend, _Name}, StateData) -> - send_trailer(StateData), {stop, normal, StateData}; -wait_for_session({xmlstreamerror, _}, StateData) -> - send_element(StateData, ?INVALID_XML_ERR), - send_trailer(StateData), - {stop, normal, StateData}; -wait_for_session(closed, StateData) -> +wait_for_bind(stop, StateData) -> {stop, normal, StateData}. +open_session(StateData) -> + U = StateData#state.user, + R = StateData#state.resource, + JID = StateData#state.jid, + case acl:match_rule(StateData#state.server, + StateData#state.access, JID) of + allow -> + ?INFO_MSG("(~w) Opened session for ~s", + [StateData#state.socket, jid:to_string(JID)]), + change_shaper(StateData, JID), + {Fs, Ts} = ejabberd_hooks:run_fold( + roster_get_subscription_lists, + StateData#state.server, + {[], []}, + [U, StateData#state.server]), + LJID = jid:tolower(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]), + Conn = get_conn_type(StateData), + Info = [{ip, StateData#state.ip}, {conn, Conn}, + {auth_module, StateData#state.auth_module}], + ejabberd_sm:open_session( + StateData#state.sid, U, StateData#state.server, R, Info), + UpdatedStateData = + StateData#state{ + conn = Conn, + pres_f = ?SETS:from_list(Fs1), + pres_t = ?SETS:from_list(Ts1), + privacy_list = PrivList}, + {ok, UpdatedStateData}; + _ -> + ejabberd_hooks:run(forbidden_session_hook, + StateData#state.server, [JID]), + ?INFO_MSG("(~w) Forbidden session for ~s", + [StateData#state.socket, jid:to_string(JID)]), + {error, ?ERR_NOT_ALLOWED} + end. + session_established({xmlstreamelement, #xmlel{name = Name} = El}, StateData) when ?IS_STREAM_MGMT_TAG(Name) -> fsm_next_state(session_established, dispatch_stream_mgmt(El, StateData)); @@ -1218,36 +1185,38 @@ session_established({xmlstreamerror, _}, StateData) -> send_trailer(StateData), {stop, normal, StateData}; session_established(closed, #state{mgmt_state = active} = StateData) -> + catch (StateData#state.sockmod):close(StateData#state.socket), fsm_next_state(wait_for_resume, StateData); session_established(closed, StateData) -> + {stop, normal, StateData}; +session_established(stop, StateData) -> {stop, normal, StateData}. -%% Process packets sent by user (coming from user on c2s XMPP -%% connection) +%% 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), + To = fxml:get_attr_s(<<"to">>, Attrs), ToJID = case To of - <<"">> -> jlib:make_jid(User, Server, <<"">>); - _ -> jlib:string_to_jid(To) + <<"">> -> jid:make(User, Server, <<"">>); + _ -> jid:from_string(To) end, NewEl1 = jlib:remove_attr(<<"xmlns">>, El), - NewEl = case xml:get_attr_s(<<"xml:lang">>, Attrs) of + NewEl = case fxml:get_attr_s(<<"xml:lang">>, Attrs) of <<"">> -> case NewStateData#state.lang of <<"">> -> NewEl1; Lang -> - xml:replace_tag_attr(<<"xml:lang">>, Lang, NewEl1) + fxml:replace_tag_attr(<<"xml:lang">>, Lang, NewEl1) end; _ -> NewEl1 end, NewState = case ToJID of error -> - case xml:get_attr_s(<<"type">>, Attrs) of + case fxml:get_attr_s(<<"type">>, Attrs) of <<"error">> -> NewStateData; <<"result">> -> NewStateData; _ -> @@ -1258,12 +1227,14 @@ session_established2(El, StateData) -> _ -> case Name of <<"presence">> -> - PresenceEl = + PresenceEl0 = ejabberd_hooks:run_fold(c2s_update_presence, Server, NewEl, [User, Server]), - ejabberd_hooks:run(user_send_packet, Server, - [FromJID, ToJID, PresenceEl]), + PresenceEl = + ejabberd_hooks:run_fold( + user_send_packet, Server, PresenceEl0, + [NewStateData, FromJID, ToJID]), case ToJID of #jid{user = User, server = Server, resource = <<"">>} -> @@ -1282,17 +1253,23 @@ session_established2(El, StateData) -> Xmlns == (?NS_BLOCKING) -> process_privacy_iq(FromJID, ToJID, IQ, NewStateData); + #iq{xmlns = ?NS_SESSION} -> + Res = jlib:make_result_iq_reply( + NewEl#xmlel{children = []}), + send_stanza(NewStateData, Res); _ -> - ejabberd_hooks:run(user_send_packet, Server, - [FromJID, ToJID, NewEl]), + NewEl0 = ejabberd_hooks:run_fold( + user_send_packet, Server, NewEl, + [NewStateData, FromJID, ToJID]), check_privacy_route(FromJID, NewStateData, - FromJID, ToJID, NewEl) + FromJID, ToJID, NewEl0) end; <<"message">> -> - ejabberd_hooks:run(user_send_packet, Server, - [FromJID, ToJID, NewEl]), + NewEl0 = ejabberd_hooks:run_fold( + user_send_packet, Server, NewEl, + [NewStateData, FromJID, ToJID]), check_privacy_route(FromJID, NewStateData, FromJID, - ToJID, NewEl); + ToJID, NewEl0); _ -> NewStateData end end, @@ -1301,47 +1278,19 @@ session_established2(El, StateData) -> fsm_next_state(session_established, NewState). wait_for_resume({xmlstreamelement, _El} = Event, StateData) -> - session_established(Event, StateData), - fsm_next_state(wait_for_resume, StateData); + Result = session_established(Event, StateData), + fsm_next_state(wait_for_resume, element(3, Result)); wait_for_resume(timeout, StateData) -> ?DEBUG("Timed out waiting for resumption of stream for ~s", - [jlib:jid_to_string(StateData#state.jid)]), + [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 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {reply, Reply, NextStateName, NextStateData} | -%% {reply, Reply, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} | -%% {stop, Reason, Reply, NewStateData} -%%---------------------------------------------------------------------- -%state_name(Event, From, StateData) -> -% Reply = ok, -% {reply, Reply, state_name, StateData}. - -%%---------------------------------------------------------------------- -%% Func: handle_event/3 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} -%%---------------------------------------------------------------------- handle_event(_Event, StateName, StateData) -> fsm_next_state(StateName, StateData). -%%---------------------------------------------------------------------- -%% Func: handle_sync_event/4 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {reply, Reply, NextStateName, NextStateData} | -%% {reply, Reply, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} | -%% {stop, Reason, Reply, NewStateData} -%%---------------------------------------------------------------------- handle_sync_event({get_presence}, _From, StateName, StateData) -> User = StateData#state.user, @@ -1374,12 +1323,6 @@ handle_sync_event(_Event, _From, StateName, code_change(_OldVsn, StateName, StateData, _Extra) -> {ok, StateName, StateData}. -%%---------------------------------------------------------------------- -%% Func: handle_info/3 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} -%%---------------------------------------------------------------------- handle_info({send_text, Text}, StateName, StateData) -> send_text(StateData, Text), ejabberd_hooks:run(c2s_loop_debug, [Text]), @@ -1431,10 +1374,11 @@ handle_info({route, _From, _To, {broadcast, Data}}, PrivListName}], children = []}]}]}, PrivPushEl = jlib:replace_from_to( - jlib:jid_remove_resource(StateData#state.jid), + jid:remove_resource(StateData#state.jid), StateData#state.jid, jlib:iq_to_xml(PrivPushIQ)), - NewState = send_stanza(StateData, PrivPushEl), + NewState = send_stanza( + StateData, PrivPushEl), fsm_next_state(StateName, NewState#state{privacy_list = NewPL}) end; @@ -1456,11 +1400,11 @@ handle_info({route, From, To, StateData, [{From, To, Packet}]), - case xml:get_attr_s(<<"type">>, Attrs) of + case fxml:get_attr_s(<<"type">>, Attrs) of <<"probe">> -> - LFrom = jlib:jid_tolower(From), + LFrom = jid:tolower(From), LBFrom = - jlib:jid_remove_resource(LFrom), + jid:remove_resource(LFrom), NewStateData = case (?SETS):is_element(LFrom, State#state.pres_a) @@ -1503,7 +1447,7 @@ handle_info({route, From, To, {false, Attrs, NewStateData}; <<"error">> -> NewA = - remove_element(jlib:jid_tolower(From), + remove_element(jid:tolower(From), State#state.pres_a), {true, Attrs, State#state{pres_a = NewA}}; @@ -1539,9 +1483,9 @@ handle_info({route, From, To, of allow -> LFrom = - jlib:jid_tolower(From), + jid:tolower(From), LBFrom = - jlib:jid_remove_resource(LFrom), + jid:remove_resource(LFrom), case (?SETS):is_element(LFrom, State#state.pres_a) @@ -1592,9 +1536,9 @@ handle_info({route, From, To, IQ = jlib:iq_query_info(Packet), case IQ of #iq{xmlns = ?NS_LAST} -> - LFrom = jlib:jid_tolower(From), + LFrom = jid:tolower(From), LBFrom = - jlib:jid_remove_resource(LFrom), + jid:remove_resource(LFrom), HasFromSub = ((?SETS):is_element(LFrom, StateData#state.pres_f) @@ -1670,10 +1614,13 @@ handle_info({route, From, To, From, To, Packet, in) of - allow -> {true, Attrs, StateData}; + allow -> + {true, Attrs, StateData}; deny -> - case xml:get_attr_s(<<"type">>, Attrs) of + case fxml:get_attr_s(<<"type">>, Attrs) of <<"error">> -> ok; + <<"groupchat">> -> ok; + <<"headline">> -> ok; <<"result">> -> ok; _ -> Err = @@ -1688,13 +1635,15 @@ handle_info({route, From, To, end, if Pass -> 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}, + jlib:replace_from_to_attrs(jid:to_string(From), + jid:to_string(To), NewAttrs), + FixedPacket0 = #xmlel{name = Name, attrs = Attrs2, children = Els}, + FixedPacket = ejabberd_hooks:run_fold( + user_receive_packet, + NewState#state.server, + FixedPacket0, + [NewState, NewState#state.jid, From, To]), SentStateData = send_packet(NewState, FixedPacket), - ejabberd_hooks:run(user_receive_packet, - SentStateData#state.server, - [SentStateData#state.jid, From, To, FixedPacket]), ejabberd_hooks:run(c2s_loop_debug, [{route, From, To, Packet}]), fsm_next_state(StateName, SentStateData); true -> @@ -1727,8 +1676,8 @@ handle_info({route_xmlstreamelement, El}, _StateName, StateData) -> {next_state, NStateName, NStateData, _Timeout} = session_established({xmlstreamelement, El}, StateData), fsm_next_state(NStateName, NStateData); -handle_info({force_update_presence, LUser}, StateName, - #state{user = LUser, server = LServer} = StateData) -> +handle_info({force_update_presence, LUser, LServer}, StateName, + #state{jid = #jid{luser = LUser, lserver = LServer}} = StateData) -> NewStateData = case StateData#state.pres_last of #xmlel{name = <<"presence">>} -> PresenceEl = @@ -1749,8 +1698,8 @@ handle_info({send_filtered, Feature, From, To, Packet}, StateName, StateData) -> Feature, To, Packet]), NewStateData = if Drop -> ?DEBUG("Dropping packet from ~p to ~p", - [jlib:jid_to_string(From), - jlib:jid_to_string(To)]), + [jid:to_string(From), + jid:to_string(To)]), StateData; true -> FinalPacket = jlib:replace_from_to(From, To, Packet), @@ -1777,35 +1726,25 @@ handle_info({broadcast, Type, From, Packet}, StateName, StateData) -> lists:foreach( fun(USR) -> ejabberd_router:route( - From, jlib:make_jid(USR), Packet) + From, jid:make(USR), Packet) end, lists:usort(Recipients)), fsm_next_state(StateName, StateData); +handle_info(dont_ask_offline, StateName, StateData) -> + fsm_next_state(StateName, StateData#state{ask_offline = false}); 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 -%%---------------------------------------------------------------------- + State#state{pres_t = {pres_t, (?SETS):size(T)}, + pres_f = {pres_f, (?SETS):size(F)}, + pres_a = {pres_a, (?SETS):size(A)}}. + terminate(_Reason, StateName, StateData) -> case StateData#state.mgmt_state of resumed -> ?INFO_MSG("Closing former stream of resumed session for ~s", - [jlib:jid_to_string(StateData#state.jid)]); + [jid:to_string(StateData#state.jid)]); _ -> if StateName == session_established; StateName == wait_for_resume -> @@ -1813,7 +1752,7 @@ terminate(_Reason, StateName, StateData) -> replaced -> ?INFO_MSG("(~w) Replaced session for ~s", [StateData#state.socket, - jlib:jid_to_string(StateData#state.jid)]), + jid:to_string(StateData#state.jid)]), From = StateData#state.jid, Packet = #xmlel{name = <<"presence">>, attrs = [{<<"type">>, <<"unavailable">>}], @@ -1833,7 +1772,7 @@ terminate(_Reason, StateName, StateData) -> _ -> ?INFO_MSG("(~w) Close session for ~s", [StateData#state.socket, - jlib:jid_to_string(StateData#state.jid)]), + jid:to_string(StateData#state.jid)]), EmptySet = (?SETS):new(), case StateData of #state{pres_last = undefined, pres_a = EmptySet} -> @@ -1885,7 +1824,7 @@ send_text(StateData, Text) when StateData#state.mgmt_state == active -> case catch (StateData#state.sockmod):send(StateData#state.socket, Text) of {'EXIT', _} -> (StateData#state.sockmod):close(StateData#state.socket), - error; + {error, closed}; _ -> ok end; @@ -1899,19 +1838,14 @@ 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_binary(El)). + send_text(StateData, fxml: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, + NewStateData = send_stanza_and_ack_req(StateData, Stanza), mgmt_queue_add(NewStateData, Stanza); send_stanza(StateData, Stanza) -> send_element(StateData, Stanza), @@ -1972,19 +1906,23 @@ send_trailer(StateData) -> new_id() -> randoms:get_string(). +new_uniq_id() -> + iolist_to_binary([randoms:get_string(), + jlib:integer_to_binary(p1_time_compat:unique_integer([positive]))]). + is_auth_packet(El) -> case jlib:iq_query_info(El) of #iq{id = ID, type = Type, xmlns = ?NS_AUTH, sub_el = SubEl} -> #xmlel{children = Els} = SubEl, {auth, ID, Type, - get_auth_tags(Els, <<"">>, <<"">>, <<"">>, <<"">>)}; + 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 + case fxml:get_attr(<<"xmlns">>, Attrs) of {value, NS} when NS /= <<"jabber:client">>, NS /= <<"jabber:server">> -> false; @@ -1996,7 +1934,7 @@ is_stanza(_El) -> get_auth_tags([#xmlel{name = Name, children = Els} | L], U, P, D, R) -> - CData = xml:get_cdata(Els), + CData = fxml:get_cdata(Els), case Name of <<"username">> -> get_auth_tags(L, CData, P, D, R); <<"password">> -> get_auth_tags(L, U, CData, D, R); @@ -2015,29 +1953,29 @@ get_auth_tags([], U, P, D, R) -> get_conn_type(StateData) -> case (StateData#state.sockmod):get_sockmod(StateData#state.socket) of gen_tcp -> c2s; - p1_tls -> c2s_tls; + fast_tls -> c2s_tls; ezlib -> case ezlib:get_sockmod((StateData#state.socket)#socket_state.socket) of gen_tcp -> c2s_compressed; - p1_tls -> c2s_compressed_tls + fast_tls -> c2s_compressed_tls end; ejabberd_http_bind -> http_bind; + ejabberd_http_ws -> websocket; _ -> unknown end. process_presence_probe(From, To, StateData) -> - LFrom = jlib:jid_tolower(From), + LFrom = jid:tolower(From), LBFrom = setelement(3, LFrom, <<"">>), case StateData#state.pres_last of undefined -> ok; _ -> - Cond = ?SETS:is_element(LFrom, StateData#state.pres_f) - orelse - ((LFrom /= LBFrom) andalso - ?SETS:is_element(LBFrom, StateData#state.pres_f)), - if - Cond -> + Cond = ((?SETS):is_element(LFrom, StateData#state.pres_f) + orelse + ((LFrom /= LBFrom) andalso + (?SETS):is_element(LBFrom, StateData#state.pres_f))), + if Cond -> %% To is the one sending the presence (the probe target) Packet = jlib:add_delay_info(StateData#state.pres_last, To, StateData#state.pres_timestamp), @@ -2063,11 +2001,11 @@ process_presence_probe(From, To, StateData) -> %% User updates his presence (non-directed presence packet) presence_update(From, Packet, StateData) -> #xmlel{attrs = Attrs} = Packet, - case xml:get_attr_s(<<"type">>, Attrs) of + case fxml:get_attr_s(<<"type">>, Attrs) of <<"unavailable">> -> - Status = case xml:get_subtag(Packet, <<"status">>) of + Status = case fxml:get_subtag(Packet, <<"status">>) of false -> <<"">>; - StatusTag -> xml:get_tag_cdata(StatusTag) + StatusTag -> fxml:get_tag_cdata(StatusTag) end, Info = [{ip, StateData#state.ip}, {conn, StateData#state.conn}, @@ -2096,7 +2034,7 @@ presence_update(From, Packet, StateData) -> FromUnavail = (StateData#state.pres_last == undefined), ?DEBUG("from unavail = ~p~n", [FromUnavail]), NewStateData = StateData#state{pres_last = Packet, - pres_timestamp = now()}, + pres_timestamp = p1_time_compat:timestamp()}, NewState = if FromUnavail -> ejabberd_hooks:run(user_available_hook, NewStateData#state.server, @@ -2125,10 +2063,10 @@ presence_update(From, Packet, StateData) -> %% User sends a directed presence packet presence_track(From, To, Packet, StateData) -> #xmlel{attrs = Attrs} = Packet, - LTo = jlib:jid_tolower(To), + LTo = jid:tolower(To), User = StateData#state.user, Server = StateData#state.server, - case xml:get_attr_s(<<"type">>, Attrs) of + case fxml:get_attr_s(<<"type">>, Attrs) of <<"unavailable">> -> A = remove_element(LTo, StateData#state.pres_a), check_privacy_route(From, StateData#state{pres_a = A}, From, To, Packet); @@ -2138,14 +2076,14 @@ presence_track(From, To, Packet, StateData) -> ejabberd_hooks:run(roster_out_subscription, Server, [User, Server, To, subscribed]), check_privacy_route(From, StateData, - jlib:jid_remove_resource(From), To, Packet); + jid:remove_resource(From), To, Packet); <<"unsubscribe">> -> try_roster_subscribe(unsubscribe, User, Server, From, To, Packet, 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); + jid:remove_resource(From), To, Packet); <<"error">> -> check_privacy_route(From, StateData, From, To, Packet); <<"probe">> -> @@ -2188,7 +2126,7 @@ is_privacy_allow(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, <<"">>), + JID1 = jid:make(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 -> @@ -2198,7 +2136,7 @@ try_roster_subscribe(Type, User, Server, From, To, Packet, StateData) -> ejabberd_hooks:run(roster_out_subscription, Server, [User, Server, To, Type]), - check_privacy_route(From, StateData, jlib:jid_remove_resource(From), + check_privacy_route(From, StateData, jid:remove_resource(From), To, Packet) end. @@ -2240,7 +2178,7 @@ presence_broadcast_first(From, StateData, Packet) -> StateData#state{pres_a = As}. format_and_check_privacy(From, StateData, Packet, JIDs, Dir) -> - FJIDs = [jlib:make_jid(JID) || JID <- JIDs], + FJIDs = [jid:make(JID) || JID <- JIDs], lists:filter( fun(FJID) -> case ejabberd_hooks:run_fold( @@ -2267,17 +2205,17 @@ remove_element(E, Set) -> end. roster_change(IJID, ISubscription, StateData) -> - LIJID = jlib:jid_tolower(IJID), + LIJID = jid:tolower(IJID), IsFrom = (ISubscription == both) or (ISubscription == from), 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); - not IsFrom -> remove_element(LIJID, StateData#state.pres_f) + true -> remove_element(LIJID, StateData#state.pres_f) end, TSet = if IsTo -> (?SETS):add_element(LIJID, StateData#state.pres_t); - not IsTo -> remove_element(LIJID, StateData#state.pres_t) + true -> remove_element(LIJID, StateData#state.pres_t) end, case StateData#state.pres_last of undefined -> @@ -2286,7 +2224,7 @@ roster_change(IJID, ISubscription, StateData) -> ?DEBUG("roster changed for ~p~n", [StateData#state.user]), From = StateData#state.jid, - To = jlib:make_jid(IJID), + To = jid:make(IJID), Cond1 = IsFrom andalso not OldIsFrom, Cond2 = not IsFrom andalso OldIsFrom andalso ((?SETS):is_element(LIJID, StateData#state.pres_a)), @@ -2325,11 +2263,11 @@ update_priority(Priority, Packet, StateData) -> StateData#state.resource, Priority, Packet, Info). get_priority_from_presence(PresencePacket) -> - case xml:get_subtag(PresencePacket, <<"priority">>) of + case fxml:get_subtag(PresencePacket, <<"priority">>) of false -> 0; SubEl -> case catch - jlib:binary_to_integer(xml:get_tag_cdata(SubEl)) + jlib:binary_to_integer(fxml:get_tag_cdata(SubEl)) of P when is_integer(P) -> P; _ -> 0 @@ -2370,7 +2308,7 @@ process_privacy_iq(From, To, ejabberd_router:route(To, From, jlib:iq_to_xml(IQRes)), NewStateData. -resend_offline_messages(StateData) -> +resend_offline_messages(#state{ask_offline = true} = StateData) -> case ejabberd_hooks:run_fold(resend_offline_messages_hook, StateData#state.server, [], [StateData#state.user, StateData#state.server]) @@ -2391,7 +2329,9 @@ resend_offline_messages(StateData) -> end end, Rs) - end. + end; +resend_offline_messages(_StateData) -> + ok. resend_subscription_requests(#state{user = User, server = Server} = StateData) -> @@ -2406,21 +2346,21 @@ resend_subscription_requests(#state{user = User, get_showtag(undefined) -> <<"unavailable">>; get_showtag(Presence) -> - case xml:get_path_s(Presence, [{elem, <<"show">>}, cdata]) of + case fxml:get_path_s(Presence, [{elem, <<"show">>}, cdata]) of <<"">> -> <<"available">>; ShowTag -> ShowTag end. get_statustag(undefined) -> <<"">>; get_statustag(Presence) -> - xml:get_path_s(Presence, [{elem, <<"status">>}, cdata]). + fxml:get_path_s(Presence, [{elem, <<"status">>}, cdata]). process_unauthenticated_stanza(StateData, El) -> - NewEl = case xml:get_tag_attr_s(<<"xml:lang">>, El) of + NewEl = case fxml:get_tag_attr_s(<<"xml:lang">>, El) of <<"">> -> case StateData#state.lang of <<"">> -> El; - Lang -> xml:replace_tag_attr(<<"xml:lang">>, Lang, El) + Lang -> fxml:replace_tag_attr(<<"xml:lang">>, Lang, El) end; _ -> El end, @@ -2434,10 +2374,10 @@ process_unauthenticated_stanza(StateData, El) -> empty -> ResIQ = IQ#iq{type = error, sub_el = [?ERR_SERVICE_UNAVAILABLE]}, - Res1 = jlib:replace_from_to(jlib:make_jid(<<"">>, + Res1 = jlib:replace_from_to(jid:make(<<"">>, StateData#state.server, <<"">>), - jlib:make_jid(<<"">>, <<"">>, + jid:make(<<"">>, <<"">>, <<"">>), jlib:iq_to_xml(ResIQ)), send_element(StateData, @@ -2475,7 +2415,7 @@ fsm_next_state_gc(StateName, PackedStateData) -> 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)]), + [jid:to_string(StateData#state.jid)]), Err = ?SERRT_POLICY_VIOLATION(StateData#state.lang, <<"Too many unacked stanzas">>), send_element(StateData, Err), @@ -2491,7 +2431,7 @@ fsm_next_state(wait_for_resume, #state{mgmt_timeout = 0} = 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)]), + [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}; @@ -2522,23 +2462,23 @@ is_ip_blacklisted({IP, _Port}, Lang) -> %% Check from attributes %% returns invalid-from|NewElement check_from(El, FromJID) -> - case xml:get_tag_attr(<<"from">>, El) of + case fxml:get_tag_attr(<<"from">>, El) of false -> El; {value, SJID} -> - JID = jlib:string_to_jid(SJID), + JID = jid:from_string(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) -> + (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 == <<"">>) -> + (JID#jid.lserver == FromJID#jid.lserver) and + (JID#jid.lresource == <<"">>) -> El; true -> 'invalid-from' @@ -2565,6 +2505,41 @@ bounce_messages() -> after 0 -> ok end. +process_compression_request(El, StateName, StateData) -> + case fxml: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(StateName, StateData); + Method -> + case fxml:get_tag_cdata(Method) of + <<"zlib">> -> + Socket = StateData#state.socket, + BCompressed = fxml: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(StateName, StateData) + end + end. + %%%---------------------------------------------------------------------- %%% XEP-0191 %%%---------------------------------------------------------------------- @@ -2579,7 +2554,7 @@ route_blocking(What, StateData) -> #xmlel{name = <<"item">>, attrs = [{<<"jid">>, - jlib:jid_to_string(JID)}], + jid:to_string(JID)}], children = []} end, JIDs)}; @@ -2591,7 +2566,7 @@ route_blocking(What, StateData) -> #xmlel{name = <<"item">>, attrs = [{<<"jid">>, - jlib:jid_to_string(JID)}], + jid:to_string(JID)}], children = []} end, JIDs)}; @@ -2601,7 +2576,7 @@ route_blocking(What, StateData) -> end, PrivPushIQ = #iq{type = set, id = <<"push">>, sub_el = [SubEl]}, PrivPushEl = - jlib:replace_from_to(jlib:jid_remove_resource(StateData#state.jid), + jlib:replace_from_to(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 @@ -2633,7 +2608,7 @@ negotiate_stream_mgmt(_El, #state{resource = <<"">>} = StateData) -> 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 + case fxml:get_attr_s(<<"xmlns">>, Attrs) of Xmlns when ?IS_SUPPORTED_MGMT_XMLNS(Xmlns) -> case stream_mgmt_enabled(StateData) of true -> @@ -2661,7 +2636,7 @@ negotiate_stream_mgmt(#xmlel{name = Name, attrs = Attrs}, StateData) -> end. perform_stream_mgmt(#xmlel{name = Name, attrs = Attrs}, StateData) -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of + case fxml:get_attr_s(<<"xmlns">>, Attrs) of Xmlns when Xmlns == StateData#state.mgmt_xmlns -> case Name of <<"r">> -> @@ -2684,16 +2659,17 @@ perform_stream_mgmt(#xmlel{name = Name, attrs = Attrs}, StateData) -> StateData end. -handle_enable(#state{mgmt_timeout = ConfigTimeout} = StateData, Attrs) -> - Timeout = case xml:get_attr_s(<<"resume">>, Attrs) of +handle_enable(#state{mgmt_timeout = DefaultTimeout, + mgmt_max_timeout = MaxTimeout} = StateData, Attrs) -> + Timeout = case fxml:get_attr_s(<<"resume">>, Attrs) of ResumeAttr when ResumeAttr == <<"true">>; ResumeAttr == <<"1">> -> - MaxAttr = xml:get_attr_s(<<"max">>, Attrs), + MaxAttr = fxml:get_attr_s(<<"max">>, Attrs), case catch jlib:binary_to_integer(MaxAttr) of - Max when is_integer(Max), Max > 0, Max =< ConfigTimeout -> + Max when is_integer(Max), Max > 0, Max =< MaxTimeout -> Max; _ -> - ConfigTimeout + DefaultTimeout end; _ -> 0 @@ -2701,13 +2677,13 @@ handle_enable(#state{mgmt_timeout = ConfigTimeout} = StateData, Attrs) -> 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)]), + [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)]), + [jid:to_string(StateData#state.jid)]), [] end, Res = #xmlel{name = <<"enabled">>, @@ -2728,22 +2704,22 @@ handle_r(StateData) -> StateData. handle_a(StateData, Attrs) -> - case catch jlib:binary_to_integer(xml:get_attr_s(<<"h">>, Attrs)) of + case catch jlib:binary_to_integer(fxml: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)]), + [jid:to_string(StateData#state.jid)]), StateData end. handle_resume(StateData, Attrs) -> - R = case xml:get_attr_s(<<"xmlns">>, Attrs) of + R = case fxml: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))} + case {fxml:get_attr(<<"previd">>, Attrs), + catch jlib:binary_to_integer(fxml:get_attr_s(<<"h">>, Attrs))} of {{value, PrevID}, H} when is_integer(H), H >= 0 -> case inherit_session_state(StateData, PrevID) of @@ -2788,7 +2764,7 @@ handle_resume(StateData, Attrs) -> FlushedState = csi_queue_flush(NewState), NewStateData = FlushedState#state{csi_state = active}, ?INFO_MSG("Resumed session for ~s", - [jlib:jid_to_string(NewStateData#state.jid)]), + [jid:to_string(NewStateData#state.jid)]), {ok, NewStateData}; {error, El, Msg} -> send_element(StateData, El), @@ -2800,11 +2776,11 @@ handle_resume(StateData, Attrs) -> 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]), + [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]), + [jid:to_string(StateData#state.jid), H, NumStanzasOut]), mgmt_queue_drop(StateData, H). update_num_stanzas_in(#state{mgmt_state = active} = StateData, El) -> @@ -2824,11 +2800,12 @@ send_stanza_and_ack_req(StateData, Stanza) -> AckReq = #xmlel{name = <<"r">>, attrs = [{<<"xmlns">>, StateData#state.mgmt_xmlns}], children = []}, - case send_element(StateData, Stanza) of - ok -> - send_element(StateData, AckReq); - error -> - error + case send_element(StateData, Stanza) == ok andalso + send_element(StateData, AckReq) == ok of + true -> + StateData; + false -> + StateData#state{mgmt_state = pending} end. mgmt_queue_add(StateData, El) -> @@ -2838,7 +2815,7 @@ mgmt_queue_add(StateData, El) -> Num -> Num + 1 end, - NewQueue = queue:in({NewNum, now(), El}, StateData#state.mgmt_queue), + NewQueue = queue:in({NewNum, p1_time_compat:timestamp(), El}, StateData#state.mgmt_queue), NewState = StateData#state{mgmt_queue = NewQueue, mgmt_stanzas_out = NewNum}, check_queue_length(NewState). @@ -2870,13 +2847,13 @@ handle_unacked_stanzas(StateData, F) ok; N -> ?INFO_MSG("~B stanzas were not acknowledged by ~s", - [N, jlib:jid_to_string(StateData#state.jid)]), + [N, jid:to_string(StateData#state.jid)]), lists:foreach( fun({_, Time, #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), + From_s = fxml:get_attr_s(<<"from">>, Attrs), + From = jid:from_string(From_s), + To_s = fxml:get_attr_s(<<"to">>, Attrs), + To = jid:from_string(To_s), F(From, To, El, Time) end, queue:to_list(Queue)) end; @@ -2891,8 +2868,16 @@ handle_unacked_stanzas(StateData) Resend when is_boolean(Resend) -> Resend; if_offline -> - ejabberd_sm:get_user_resources(StateData#state.user, - StateData#state.server) == [] + Resource = StateData#state.resource, + case ejabberd_sm:get_user_resources(StateData#state.user, + StateData#state.server) of + [Resource] -> % Same resource opened new session + true; + [] -> + true; + _ -> + false + end end, ReRoute = case ResendOnTimeout of true -> @@ -2908,7 +2893,13 @@ handle_unacked_stanzas(StateData) ejabberd_router:route(To, From, Err) end end, - F = fun(From, To, El, Time) -> + F = fun(From, _To, #xmlel{name = <<"presence">>}, _Time) -> + ?DEBUG("Dropping presence stanza from ~s", + [jid:to_string(From)]); + (From, To, #xmlel{name = <<"iq">>} = El, _Time) -> + Err = jlib:make_error_reply(El, ?ERR_SERVICE_UNAVAILABLE), + ejabberd_router:route(To, From, Err); + (From, To, El, Time) -> %% We'll drop the stanza if it was <forwarded/> by some %% encapsulating protocol as per XEP-0297. One such protocol is %% XEP-0280, which says: "When a receiving server attempts to @@ -2918,10 +2909,19 @@ handle_unacked_stanzas(StateData) %% 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)]); + ?DEBUG("Dropping forwarded message stanza from ~s", + [fxml:get_attr_s(<<"from">>, El#xmlel.attrs)]); false -> - ReRoute(From, To, El, Time) + case ejabberd_hooks:run_fold(message_is_archived, + StateData#state.server, + false, + [StateData, From, + StateData#state.jid, El]) of + true -> + ok; + false -> + ReRoute(From, To, El, Time) + end end end, handle_unacked_stanzas(StateData, F); @@ -2929,9 +2929,9 @@ 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 + SubTag = case {fxml:get_subtag(El, <<"sent">>), + fxml:get_subtag(El, <<"received">>), + fxml:get_subtag(El, <<"result">>)} of {false, false, false} -> false; {Tag, false, false} -> @@ -2944,7 +2944,7 @@ is_encapsulated_forward(#xmlel{name = <<"message">>} = El) -> if SubTag == false -> false; true -> - case xml:get_subtag(SubTag, <<"forwarded">>) of + case fxml:get_subtag(SubTag, <<"forwarded">>) of false -> false; _ -> @@ -3006,12 +3006,14 @@ inherit_session_state(#state{user = U, server = S} = StateData, ResumeID) -> end. resume_session({Time, PID}) -> - (?GEN_FSM):sync_send_all_state_event(PID, {resume_session, Time}, 3000). + (?GEN_FSM):sync_send_all_state_event(PID, {resume_session, Time}, 5000). make_resume_id(StateData) -> {Time, _} = StateData#state.sid, jlib:term_to_base64({StateData#state.resource, Time}). +add_resent_delay_info(_State, #xmlel{name = <<"iq">>} = El, _Time) -> + El; add_resent_delay_info(#state{server = From}, El, Time) -> jlib:add_delay_info(El, From, Time, <<"Resent">>). @@ -3025,12 +3027,12 @@ csi_filter_stanza(#state{csi_state = CsiState, jid = JID} = StateData, StateData#state.server, send, [Stanza]), ?DEBUG("Going to ~p stanza for inactive client ~p", - [Action, jlib:jid_to_string(JID)]), + [Action, jid:to_string(JID)]), case Action of queue -> csi_queue_add(StateData, Stanza); drop -> StateData; send -> - From = xml:get_tag_attr_s(<<"from">>, Stanza), + From = fxml:get_tag_attr_s(<<"from">>, Stanza), StateData1 = csi_queue_send(StateData, From), StateData2 = send_stanza(StateData1#state{csi_state = active}, Stanza), @@ -3041,8 +3043,8 @@ csi_queue_add(#state{csi_queue = Queue} = StateData, Stanza) -> case length(StateData#state.csi_queue) >= csi_max_queue(StateData) of true -> csi_queue_add(csi_queue_flush(StateData), Stanza); false -> - From = xml:get_tag_attr_s(<<"from">>, Stanza), - NewQueue = lists:keystore(From, 1, Queue, {From, now(), Stanza}), + From = fxml:get_tag_attr_s(<<"from">>, Stanza), + NewQueue = lists:keystore(From, 1, Queue, {From, p1_time_compat:timestamp(), Stanza}), StateData#state{csi_queue = NewQueue} end. @@ -3060,7 +3062,7 @@ csi_queue_send(#state{csi_queue = Queue, csi_state = CsiState, server = Host} = csi_queue_flush(#state{csi_queue = Queue, csi_state = CsiState, jid = JID, server = Host} = StateData) -> - ?DEBUG("Flushing CSI queue for ~s", [jlib:jid_to_string(JID)]), + ?DEBUG("Flushing CSI queue for ~s", [jid:to_string(JID)]), NewStateData = lists:foldl(fun({_From, Time, Stanza}, AccState) -> NewStanza = @@ -3122,7 +3124,19 @@ transform_listen_option(Opt, Opts) -> [Opt|Opts]. identity(Props) -> - case proplists:get_value(authzid, Props, <<>>) of - <<>> -> proplists:get_value(username, Props, <<>>); - AuthzId -> AuthzId - end. + case proplists:get_value(authzid, Props, <<>>) of + <<>> -> proplists:get_value(username, Props, <<>>); + AuthzId -> AuthzId + end. + +opt_type(domain_certfile) -> fun iolist_to_binary/1; +opt_type(max_fsm_queue) -> + fun (I) when is_integer(I), I > 0 -> I end; +opt_type(resource_conflict) -> + fun (setresource) -> setresource; + (closeold) -> closeold; + (closenew) -> closenew; + (acceptnew) -> acceptnew + end; +opt_type(_) -> + [domain_certfile, max_fsm_queue, resource_conflict]. diff --git a/src/ejabberd_c2s_config.erl b/src/ejabberd_c2s_config.erl index a971f0af..3384e338 100644 --- a/src/ejabberd_c2s_config.erl +++ b/src/ejabberd_c2s_config.erl @@ -6,7 +6,7 @@ %%% Created : 2 Nov 2007 by Mickael Remond <mremond@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -26,9 +26,11 @@ -module(ejabberd_c2s_config). +-behaviour(ejabberd_config). + -author('mremond@process-one.net'). --export([get_c2s_limits/0]). +-export([get_c2s_limits/0, opt_type/1]). %% Get first c2s configuration limitations to apply it to other c2s %% connectors. @@ -63,3 +65,6 @@ select_opts_values([{max_stanza_size, Value} | Opts], [{max_stanza_size, Value} | SelectedValues]); select_opts_values([_Opt | Opts], SelectedValues) -> select_opts_values(Opts, SelectedValues). + +opt_type(listen) -> fun (V) -> V end; +opt_type(_) -> [listen]. diff --git a/src/ejabberd_captcha.erl b/src/ejabberd_captcha.erl index 110da1f6..157700c4 100644 --- a/src/ejabberd_captcha.erl +++ b/src/ejabberd_captcha.erl @@ -5,7 +5,7 @@ %%% Created : 26 Apr 2008 by Evgeniy Khramtsov <xramtsov@gmail.com> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,6 +25,10 @@ -module(ejabberd_captcha). +-behaviour(ejabberd_config). + +-protocol({xep, 158, '1.0'}). + -behaviour(gen_server). %% API @@ -37,7 +41,7 @@ -export([create_captcha/6, build_captcha_html/2, check_captcha/2, process_reply/1, process/2, is_feature_available/0, create_captcha_x/5, - create_captcha_x/6]). + create_captcha_x/6, opt_type/1]). -include("jlib.hrl"). @@ -71,13 +75,6 @@ tref :: reference(), args :: any()}). -%%==================================================================== -%% API -%%==================================================================== -%%-------------------------------------------------------------------- -%% Function: start_link() -> {ok,Pid} | ignore | {error,Error} -%% Description: Starts the server -%%-------------------------------------------------------------------- start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). @@ -91,7 +88,7 @@ create_captcha(SID, From, To, Lang, Limiter, Args) -> {ok, Type, Key, Image} -> Id = <<(randoms:get_string())/binary>>, B64Image = jlib:encode_base64((Image)), - JID = jlib:jid_to_string(From), + JID = jid:to_string(From), CID = <<"sha1+", (p1_sha:sha(Image))/binary, "@bob.xmpp.org">>, Data = #xmlel{name = <<"data">>, @@ -112,7 +109,7 @@ create_captcha(SID, From, To, Lang, Limiter, Args) -> {xmlcdata, ?NS_CAPTCHA}), ?VFIELD(<<"hidden">>, <<"from">>, {xmlcdata, - jlib:jid_to_string(To)}), + jid:to_string(To)}), ?VFIELD(<<"hidden">>, <<"challenge">>, {xmlcdata, Id}), @@ -236,7 +233,7 @@ create_captcha_x(SID, To, Lang, Limiter, HeadEls, [{xmlcdata, Imageurl}]}]}, ?VFIELD(<<"hidden">>, <<"from">>, - {xmlcdata, jlib:jid_to_string(To)}), + {xmlcdata, jid:to_string(To)}), ?VFIELD(<<"hidden">>, <<"challenge">>, {xmlcdata, Id}), ?VFIELD(<<"hidden">>, <<"sid">>, @@ -272,12 +269,6 @@ create_captcha_x(SID, To, Lang, Limiter, HeadEls, Err -> Err end. -%% @spec (Id::string(), Lang::string()) -> {FormEl, {ImgEl, TextEl, IdEl, KeyEl}} | captcha_not_found -%% where FormEl = xmlelement() -%% ImgEl = xmlelement() -%% TextEl = xmlelement() -%% IdEl = xmlelement() -%% KeyEl = xmlelement() -spec build_captcha_html(binary(), binary()) -> captcha_not_found | {xmlel(), {xmlel(), xmlel(), @@ -326,16 +317,10 @@ build_captcha_html(Id, Lang) -> _ -> captcha_not_found end. -%% @spec (Id::string(), ProvidedKey::string()) -> captcha_valid | captcha_non_valid | captcha_not_found --spec check_captcha(binary(), binary()) -> captcha_not_found | - captcha_valid | - captcha_non_valid. - - -spec process_reply(xmlel()) -> ok | {error, bad_match | not_found | malformed}. process_reply(#xmlel{} = El) -> - case xml:get_subtag(El, <<"x">>) of + case fxml:get_subtag(El, <<"x">>) of false -> {error, malformed}; Xdata -> Fields = jlib:parse_xdata_submit(Xdata), @@ -401,9 +386,6 @@ process(_Handlers, process(_Handlers, _Request) -> ejabberd_web:error(not_found). -%%==================================================================== -%% gen_server callbacks -%%==================================================================== init([]) -> mnesia:delete_table(captcha), ets:new(captcha, @@ -450,16 +432,6 @@ terminate(_Reason, _State) -> ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. -%%-------------------------------------------------------------------- -%%% Internal functions -%%-------------------------------------------------------------------- -%%-------------------------------------------------------------------- -%% Function: create_image() -> {ok, Type, Key, Image} | {error, Reason} -%% Type = "image/png" | "image/jpeg" | "image/gif" -%% Key = string() -%% Image = binary() -%% Reason = atom() -%%-------------------------------------------------------------------- create_image() -> create_image(undefined). create_image(Limiter) -> @@ -592,12 +564,6 @@ is_limited(Limiter) -> end end. -%%-------------------------------------------------------------------- -%% Function: cmd(Cmd) -> Data | {error, Reason} -%% Cmd = string() -%% Data = binary() -%% Description: os:cmd/1 replacement -%%-------------------------------------------------------------------- -define(CMD_TIMEOUT, 5000). -define(MAX_FILE_SIZE, 64 * 1024). @@ -659,6 +625,10 @@ lookup_captcha(Id) -> _ -> {error, enoent} end. +-spec check_captcha(binary(), binary()) -> captcha_not_found | + captcha_valid | + captcha_non_valid. + check_captcha(Id, ProvidedKey) -> case ets:lookup(captcha, Id) of [#captcha{pid = Pid, args = Args, key = ValidKey, @@ -691,5 +661,15 @@ clean_treap(Treap, CleanPriority) -> end. now_priority() -> - {MSec, Sec, USec} = now(), - -((MSec * 1000000 + Sec) * 1000000 + USec). + -p1_time_compat:system_time(micro_seconds). + +opt_type(captcha_cmd) -> + fun (FileName) -> + F = iolist_to_binary(FileName), if F /= <<"">> -> F end + end; +opt_type(captcha_host) -> fun iolist_to_binary/1; +opt_type(captcha_limit) -> + fun (I) when is_integer(I), I > 0 -> I end; +opt_type(listen) -> fun (V) -> V end; +opt_type(_) -> + [captcha_cmd, captcha_host, captcha_limit, listen]. diff --git a/src/ejabberd_cluster.erl b/src/ejabberd_cluster.erl new file mode 100644 index 00000000..1e3f02a9 --- /dev/null +++ b/src/ejabberd_cluster.erl @@ -0,0 +1,104 @@ +%%%---------------------------------------------------------------------- +%%% File : ejabberd_cluster.erl +%%% Author : Christophe Romain <christophe.romain@process-one.net> +%%% Purpose : Ejabberd clustering management +%%% Created : 7 Oct 2015 by Christophe Romain <christophe.romain@process-one.net> +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2016 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_cluster). + +%% API +-export([get_nodes/0, call/4, multicall/3, multicall/4]). +-export([join/1, leave/1]). + +-include("ejabberd.hrl"). +-include("logger.hrl"). + +-spec get_nodes() -> [node()]. + +get_nodes() -> + mnesia:system_info(running_db_nodes). + +-spec call(node(), module(), atom(), [any()]) -> any(). + +call(Node, Module, Function, Args) -> + rpc:call(Node, Module, Function, Args, 5000). + +-spec multicall(module(), atom(), [any()]) -> {list(), [node()]}. + +multicall(Module, Function, Args) -> + multicall(get_nodes(), Module, Function, Args). + +-spec multicall([node()], module(), atom(), list()) -> {list(), [node()]}. + +multicall(Nodes, Module, Function, Args) -> + rpc:multicall(Nodes, Module, Function, Args, 5000). + +-spec join(node()) -> ok | {error, any()}. + +join(Node) -> + case {node(), net_adm:ping(Node)} of + {Node, _} -> + {error, {not_master, Node}}; + {_, pong} -> + application:stop(ejabberd), + application:stop(mnesia), + mnesia:delete_schema([node()]), + application:start(mnesia), + mnesia:change_config(extra_db_nodes, [Node]), + mnesia:change_table_copy_type(schema, node(), disc_copies), + spawn(fun() -> + lists:foreach(fun(Table) -> + Type = call(Node, mnesia, table_info, [Table, storage_type]), + mnesia:add_table_copy(Table, node(), Type) + end, mnesia:system_info(tables)--[schema]) + end), + application:start(ejabberd); + _ -> + {error, {no_ping, Node}} + end. + +-spec leave(node()) -> ok | {error, any()}. + +leave(Node) -> + case {node(), net_adm:ping(Node)} of + {Node, _} -> + Cluster = get_nodes()--[Node], + leave(Cluster, Node); + {_, pong} -> + rpc:call(Node, ?MODULE, leave, [Node], 10000); + {_, pang} -> + case mnesia:del_table_copy(schema, Node) of + {atomic, ok} -> ok; + {aborted, Reason} -> {error, Reason} + end + end. +leave([], Node) -> + {error, {no_cluster, Node}}; +leave([Master|_], Node) -> + application:stop(ejabberd), + application:stop(mnesia), + call(Master, mnesia, del_table_copy, [schema, Node]), + spawn(fun() -> + mnesia:delete_schema([node()]), + erlang:halt(0) + end), + ok. diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl index ca40d5dc..3c98316d 100644 --- a/src/ejabberd_commands.erl +++ b/src/ejabberd_commands.erl @@ -5,7 +5,7 @@ %%% Created : 20 May 2008 by Badlop <badlop@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -211,22 +211,58 @@ -export([init/0, list_commands/0, get_command_format/1, + get_command_format/2, get_command_definition/1, get_tags_commands/0, + get_commands/0, register_commands/1, unregister_commands/1, execute_command/2, - execute_command/4 + execute_command/4, + opt_type/1, + get_commands_spec/0 ]). -include("ejabberd_commands.hrl"). -include("ejabberd.hrl"). -include("logger.hrl"). - +-define(POLICY_ACCESS, '$policy'). + +get_commands_spec() -> + [ + #ejabberd_commands{name = gen_html_doc_for_commands, tags = [documentation], + desc = "Generates html documentation for ejabberd_commands", + module = ejabberd_commands_doc, function = generate_html_output, + args = [{file, binary}, {regexp, binary}, {examples, binary}], + result = {res, rescode}, + args_desc = ["Path to file where generated " + "documentation should be stored", + "Regexp matching names of commands or modules " + "that will be included inside generated document", + "Comma separated list of languages (choosen from java, perl, xmlrpc, json)" + "that will have example invocation include in markdown document"], + result_desc = "0 if command failed, 1 when succedded", + args_example = ["/home/me/docs/api.html", "mod_admin", "java,json"], + result_example = ok}, + #ejabberd_commands{name = gen_markdown_doc_for_commands, tags = [documentation], + desc = "Generates markdown documentation for ejabberd_commands", + module = ejabberd_commands_doc, function = generate_md_output, + args = [{file, binary}, {regexp, binary}, {examples, binary}], + result = {res, rescode}, + args_desc = ["Path to file where generated " + "documentation should be stored", + "Regexp matching names of commands or modules " + "that will be included inside generated document", + "Comma separated list of languages (choosen from java, perl, xmlrpc, json)" + "that will have example invocation include in markdown document"], + result_desc = "0 if command failed, 1 when succedded", + args_example = ["/home/me/docs/api.html", "mod_admin", "java,json"], + result_example = ok}]. init() -> ets:new(ejabberd_commands, [named_table, set, public, - {keypos, #ejabberd_commands.name}]). + {keypos, #ejabberd_commands.name}]), + register_commands(get_commands_spec()). -spec register_commands([ejabberd_commands()]) -> ok. @@ -265,19 +301,40 @@ list_commands() -> _ = '_'}), [{A, B, C} || [A, B, C] <- Commands]. +-spec list_commands_policy() -> [{atom(), [aterm()], string(), atom()}]. + +%% @doc Get a list of all the available commands, arguments, description, and +%% policy. +list_commands_policy() -> + Commands = ets:match(ejabberd_commands, + #ejabberd_commands{name = '$1', + args = '$2', + desc = '$3', + policy = '$4', + _ = '_'}), + [{A, B, C, D} || [A, B, C, D] <- Commands]. + -spec get_command_format(atom()) -> {[aterm()], rterm()} | {error, command_unknown}. %% @doc Get the format of arguments and result of a command. get_command_format(Name) -> + get_command_format(Name, noauth). + +get_command_format(Name, Auth) -> + Admin = is_admin(Name, Auth), Matched = ets:match(ejabberd_commands, #ejabberd_commands{name = Name, args = '$1', result = '$2', + policy = '$3', _ = '_'}), case Matched of [] -> {error, command_unknown}; - [[Args, Result]] -> + [[Args, Result, user]] when Admin; + Auth == noauth -> + {[{user, binary}, {server, binary} | Args], Result}; + [[Args, Result, _]] -> {Args, Result} end. @@ -295,24 +352,57 @@ get_command_definition(Name) -> execute_command(Name, Arguments) -> execute_command([], noauth, Name, Arguments). +-spec execute_command([{atom(), [atom()], [any()]}], + {binary(), binary(), binary(), boolean()} | + noauth | admin, + atom(), + [any()] + ) -> any(). + %% @spec (AccessCommands, Auth, Name::atom(), Arguments) -> ResultTerm | {error, Error} -%% where +%% where %% AccessCommands = [{Access, CommandNames, Arguments}] -%% Auth = {User::string(), Server::string(), Password::string()} | noauth +%% Auth = {User::string(), Server::string(), Password::string(), Admin::boolean()} +%% | noauth +%% | admin %% Method = atom() %% Arguments = [any()] %% Error = command_unknown | account_unprivileged | invalid_account_data | no_auth_provided -execute_command(AccessCommands, Auth, Name, Arguments) -> +execute_command(AccessCommands1, Auth1, Name, Arguments) -> + Auth = case is_admin(Name, Auth1) of + true -> admin; + false -> Auth1 + end, case ets:lookup(ejabberd_commands, Name) of [Command] -> + AccessCommands = get_access_commands(AccessCommands1), try check_access_commands(AccessCommands, Auth, Name, Command, Arguments) of - ok -> execute_command2(Command, Arguments) + ok -> execute_command2(Auth, Command, Arguments) catch {error, Error} -> {error, Error} end; [] -> {error, command_unknown} end. +execute_command2( + _Auth, #ejabberd_commands{policy = open} = Command, Arguments) -> + execute_command2(Command, Arguments); +execute_command2( + _Auth, #ejabberd_commands{policy = restricted} = Command, Arguments) -> + execute_command2(Command, Arguments); +execute_command2( + _Auth, #ejabberd_commands{policy = admin} = Command, Arguments) -> + execute_command2(Command, Arguments); +execute_command2( + admin, #ejabberd_commands{policy = user} = Command, Arguments) -> + execute_command2(Command, Arguments); +execute_command2( + noauth, #ejabberd_commands{policy = user} = Command, Arguments) -> + execute_command2(Command, Arguments); +execute_command2( + {User, Server, _, _}, #ejabberd_commands{policy = user} = Command, Arguments) -> + execute_command2(Command, [User, Server | Arguments]). + execute_command2(Command, Arguments) -> Module = Command#ejabberd_commands.module, Function = Command#ejabberd_commands.function, @@ -361,7 +451,7 @@ get_tags_commands() -> %% ----------------------------- %% @spec (AccessCommands, Auth, Method, Command, Arguments) -> ok -%% where +%% where %% AccessCommands = [ {Access, CommandNames, Arguments} ] %% Auth = {User::string(), Server::string(), Password::string()} | noauth %% Method = atom() @@ -372,11 +462,31 @@ get_tags_commands() -> %% Error = account_unprivileged | invalid_account_data check_access_commands([], _Auth, _Method, _Command, _Arguments) -> ok; -check_access_commands(AccessCommands, Auth, Method, Command, Arguments) -> +check_access_commands(AccessCommands, Auth, Method, Command1, Arguments) -> + Command = + case {Command1#ejabberd_commands.policy, Auth} of + {user, {_, _, _}} -> + Command1; + {user, _} -> + Command1#ejabberd_commands{ + args = [{user, binary}, {server, binary} | + Command1#ejabberd_commands.args]}; + _ -> + Command1 + end, AccessCommandsAllowed = lists:filter( fun({Access, Commands, ArgumentRestrictions}) -> - case check_access(Access, Auth) of + case check_access(Command, Access, Auth) of + true -> + check_access_command(Commands, Command, ArgumentRestrictions, + Method, Arguments); + false -> + false + end; + ({Access, Commands}) -> + ArgumentRestrictions = [], + case check_access(Command, Access, Auth) of true -> check_access_command(Commands, Command, ArgumentRestrictions, Method, Arguments); @@ -390,31 +500,53 @@ check_access_commands(AccessCommands, Auth, Method, Command, Arguments) -> L when is_list(L) -> ok end. --spec check_auth(noauth) -> noauth_provided; - ({binary(), binary(), binary()}) -> {ok, binary(), binary()}. +-spec check_auth(ejabberd_commands(), noauth) -> noauth_provided; + (ejabberd_commands(), + {binary(), binary(), binary(), boolean()}) -> + {ok, binary(), binary()}. -check_auth(noauth) -> +check_auth(_Command, noauth) -> no_auth_provided; -check_auth({User, Server, Password}) -> +check_auth(Command, {User, Server, {oauth, Token}, _}) -> + Scope = erlang:atom_to_binary(Command#ejabberd_commands.name, utf8), + case ejabberd_oauth:check_token(User, Server, Scope, Token) of + true -> + {ok, User, Server}; + false -> + throw({error, invalid_account_data}) + end; +check_auth(_Command, {User, Server, Password, _}) when is_binary(Password) -> %% Check the account exists and password is valid case ejabberd_auth:check_password(User, <<"">>, Server, Password) of - true -> {ok, User, Server}; - _ -> throw({error, invalid_account_data}) + true -> {ok, User, Server}; + _ -> throw({error, invalid_account_data}) end. -check_access(all, _) -> +check_access(Command, ?POLICY_ACCESS, _) + when Command#ejabberd_commands.policy == open -> true; -check_access(Access, Auth) -> - case check_auth(Auth) of +check_access(_Command, _Access, admin) -> + true; +check_access(_Command, _Access, {_User, _Server, _, true}) -> + false; +check_access(Command, Access, Auth) + when Access =/= ?POLICY_ACCESS; + Command#ejabberd_commands.policy == open; + Command#ejabberd_commands.policy == user -> + case check_auth(Command, Auth) of {ok, User, Server} -> - check_access(Access, User, Server); + check_access2(Access, User, Server); _ -> false - end. + end; +check_access(_Command, _Access, _Auth) -> + false. -check_access(Access, User, Server) -> +check_access2(?POLICY_ACCESS, _User, _Server) -> + true; +check_access2(Access, User, Server) -> %% Check this user has access permission - case acl:match_rule(Server, Access, jlib:make_jid(User, Server, <<"">>)) of + case acl:match_rule(Server, Access, jid:make(User, Server, <<"">>)) of allow -> true; deny -> false end. @@ -438,8 +570,76 @@ check_access_arguments(Command, ArgumentRestrictions, Arguments) -> tag_arguments(ArgsDefs, Args) -> lists:zipwith( - fun({ArgName, _ArgType}, ArgValue) -> + fun({ArgName, _ArgType}, ArgValue) -> {ArgName, ArgValue} - end, + end, ArgsDefs, Args). + + +get_access_commands(undefined) -> + Cmds = get_commands(), + [{?POLICY_ACCESS, Cmds, []}]; +get_access_commands(AccessCommands) -> + AccessCommands. + +get_commands() -> + Opts = ejabberd_config:get_option( + commands, + fun(V) when is_list(V) -> V end, + []), + CommandsList = list_commands_policy(), + OpenCmds = [N || {N, _, _, open} <- CommandsList], + RestrictedCmds = [N || {N, _, _, restricted} <- CommandsList], + AdminCmds = [N || {N, _, _, admin} <- CommandsList], + UserCmds = [N || {N, _, _, user} <- CommandsList], + Cmds = + lists:foldl( + fun({add_commands, L}, Acc) -> + Cmds = case L of + open -> OpenCmds; + restricted -> RestrictedCmds; + admin -> AdminCmds; + user -> UserCmds; + _ when is_list(L) -> L + end, + lists:usort(Cmds ++ Acc); + ({remove_commands, L}, Acc) -> + Cmds = case L of + open -> OpenCmds; + restricted -> RestrictedCmds; + admin -> AdminCmds; + user -> UserCmds; + _ when is_list(L) -> L + end, + Acc -- Cmds; + (_, Acc) -> Acc + end, AdminCmds ++ UserCmds, Opts), + Cmds. + +is_admin(_Name, noauth) -> + false; +is_admin(_Name, admin) -> + true; +is_admin(_Name, {_User, _Server, _, false}) -> + false; +is_admin(Name, {User, Server, _, true} = Auth) -> + AdminAccess = ejabberd_config:get_option( + commands_admin_access, + fun(A) when is_atom(A) -> A end, + none), + case acl:match_rule(Server, AdminAccess, + jid:make(User, Server, <<"">>)) of + allow -> + case catch check_auth(get_command_definition(Name), Auth) of + {ok, _, _} -> true; + _ -> false + end; + deny -> false + end. + +opt_type(commands_admin_access) -> + fun(A) when is_atom(A) -> A end; +opt_type(commands) -> + fun(V) when is_list(V) -> V end; +opt_type(_) -> [commands, commands_admin_access]. diff --git a/src/ejabberd_commands_doc.erl b/src/ejabberd_commands_doc.erl new file mode 100644 index 00000000..dc00c5d2 --- /dev/null +++ b/src/ejabberd_commands_doc.erl @@ -0,0 +1,565 @@ +%%%---------------------------------------------------------------------- +%%% File : ejabberd_commands_doc.erl +%%% Author : Badlop <badlop@process-one.net> +%%% Purpose : Management of ejabberd commands +%%% Created : 20 May 2008 by Badlop <badlop@process-one.net> +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2015 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_commands_doc). +-author('pawel@process-one.net'). + +-export([generate_html_output/3]). +-export([generate_md_output/3]). + +-include("ejabberd_commands.hrl"). +-include("ejabberd.hrl"). + +-define(RAW(V), if HTMLOutput -> fxml:crypt(iolist_to_binary(V)); true -> iolist_to_binary(V) end). +-define(TAG(N), if HTMLOutput -> [<<"<", ??N, "/>">>]; true -> md_tag(N, <<"">>) end). +-define(TAG(N, V), if HTMLOutput -> [<<"<", ??N, ">">>, V, <<"</", ??N, ">">>]; true -> md_tag(N, V) end). +-define(TAG(N, C, V), if HTMLOutput -> [<<"<", ??N, " class='", C, "'>">>, V, <<"</", ??N, ">">>]; true -> md_tag(N, V) end). +-define(TAG_R(N, V), ?TAG(N, ?RAW(V))). +-define(TAG_R(N, C, V), ?TAG(N, C, ?RAW(V))). +-define(SPAN(N, V), ?TAG_R(span, ??N, V)). + +-define(STR(A), ?SPAN(str,[<<"\"">>, A, <<"\"">>])). +-define(NUM(A), ?SPAN(num,jlib:integer_to_binary(A))). +-define(FIELD(A), ?SPAN(field,A)). +-define(ID(A), ?SPAN(id,A)). +-define(OP(A), ?SPAN(op,A)). +-define(ARG(A), ?FIELD(atom_to_list(A))). +-define(KW(A), ?SPAN(kw,A)). +-define(BR, <<"\n">>). + +-define(ARG_S(A), ?STR(atom_to_list(A))). + +-define(RAW_L(A), ?RAW(<<A>>)). +-define(STR_L(A), ?STR(<<A>>)). +-define(FIELD_L(A), ?FIELD(<<A>>)). +-define(ID_L(A), ?ID(<<A>>)). +-define(OP_L(A), ?OP(<<A>>)). +-define(KW_L(A), ?KW(<<A>>)). + +-define(STR_A(A), ?STR(atom_to_list(A))). +-define(ID_A(A), ?ID(atom_to_list(A))). + +list_join_with([], _M) -> + []; +list_join_with([El|Tail], M) -> + lists:reverse(lists:foldl(fun(E, Acc) -> + [E, M | Acc] + end, [El], Tail)). + +md_tag(dt, V) -> + [<<"\n">>, V, <<"\n">>]; +md_tag(dd, V) -> + [<<"\n: ">>, V, <<"\n">>]; +md_tag(li, V) -> + [<<"- ">>, V, <<"\n">>]; +md_tag(pre, V) -> + [V, <<"\n">>]; +md_tag(p, V) -> + [<<"\n\n">>, V, <<"\n">>]; +md_tag(h1, V) -> + [<<"\n\n## ">>, V, <<"\n">>]; +md_tag(h2, V) -> + [<<"\n\n### ">>, V, <<"\n">>]; +md_tag(strong, V) -> + [<<"*">>, V, <<"*">>]; +md_tag(_, V) -> + V. + + +%% rescode_to_int(ok) -> +%% 0; +%% rescode_to_int(true) -> +%% 0; +%% rescode_to_int(_) -> +%% 1. + +perl_gen({Name, integer}, Int, _Indent, HTMLOutput) -> + [?ARG(Name), ?OP_L(" => "), ?NUM(Int)]; +perl_gen({Name, string}, Str, _Indent, HTMLOutput) -> + [?ARG(Name), ?OP_L(" => "), ?STR(Str)]; +perl_gen({Name, binary}, Str, _Indent, HTMLOutput) -> + [?ARG(Name), ?OP_L(" => "), ?STR(Str)]; +perl_gen({Name, atom}, Atom, _Indent, HTMLOutput) -> + [?ARG(Name), ?OP_L(" => "), ?STR_A(Atom)]; +perl_gen({Name, {tuple, Fields}}, Tuple, Indent, HTMLOutput) -> + Res = lists:map(fun({A,B})->perl_gen(A, B, Indent, HTMLOutput) end, lists:zip(Fields, tuple_to_list(Tuple))), + [?ARG(Name), ?OP_L(" => {"), list_join_with(Res, [?OP_L(", ")]), ?OP_L("}")]; +perl_gen({Name, {list, ElDesc}}, List, Indent, HTMLOutput) -> + Res = lists:map(fun(E) -> [?OP_L("{"), perl_gen(ElDesc, E, Indent, HTMLOutput), ?OP_L("}")] end, List), + [?ARG(Name), ?OP_L(" => ["), list_join_with(Res, [?OP_L(", ")]), ?OP_L("]")]. + +perl_call(Name, ArgsDesc, Values, HTMLOutput) -> + {Indent, Preamble} = if HTMLOutput -> {<<"">>, []}; true -> {<<" ">>, <<"~~~ perl\n">>} end, + [Preamble, + Indent, ?ID_L("XMLRPC::Lite"), ?OP_L("->"), ?ID_L("proxy"), ?OP_L("("), ?ID_L("$url"), ?OP_L(")->"), + ?ID_L("call"), ?OP_L("("), ?STR_A(Name), ?OP_L(", {"), ?BR, Indent, <<" ">>, + list_join_with(lists:map(fun({A,B})->perl_gen(A, B, <<Indent/binary, " ">>, HTMLOutput) end, lists:zip(ArgsDesc, Values)), [?OP_L(","), ?BR, Indent, <<" ">>]), + ?BR, Indent, ?OP_L("})->"), ?ID_L("results"), ?OP_L("()")]. + +java_gen_map(Vals, Indent, HTMLOutput) -> + {Split, NL} = case Indent of + none -> {<<" ">>, <<" ">>}; + _ -> {[?BR, <<" ", Indent/binary>>], [?BR, Indent]} + end, + [?KW_L("new "), ?ID_L("HashMap"), ?OP_L("<"), ?ID_L("String"), ?OP_L(", "), ?ID_L("Object"), + ?OP_L(">() {{"), Split, list_join_with(Vals, Split), NL, ?OP_L("}}")]. + +java_gen({Name, integer}, Int, _Indent, HTMLOutput) -> + [?ID_L("put"), ?OP_L("("), ?STR_A(Name), ?OP_L(", "), ?KW_L("new "), ?ID_L("Integer"), ?OP_L("("), ?NUM(Int), ?OP_L("));")]; +java_gen({Name, string}, Str, _Indent, HTMLOutput) -> + [?ID_L("put"), ?OP_L("("), ?STR_A(Name), ?OP_L(", "), ?STR(Str), ?OP_L(");")]; +java_gen({Name, binary}, Str, _Indent, HTMLOutput) -> + [?ID_L("put"), ?OP_L("("), ?STR_A(Name), ?OP_L(", "), ?STR(Str), ?OP_L(");")]; +java_gen({Name, atom}, Atom, _Indent, HTMLOutput) -> + [?ID_L("put"), ?OP_L("("), ?STR_A(Name), ?OP_L(", "), ?STR_A(Atom), ?OP_L(");")]; +java_gen({Name, {tuple, Fields}}, Tuple, Indent, HTMLOutput) -> + NewIndent = <<" ", Indent/binary>>, + Res = lists:map(fun({A, B}) -> [java_gen(A, B, NewIndent, HTMLOutput)] end, lists:zip(Fields, tuple_to_list(Tuple))), + [?ID_L("put"), ?OP_L("("), ?STR_A(Name), ?OP_L(", "), java_gen_map(Res, Indent, HTMLOutput), ?OP_L(")")]; +java_gen({Name, {list, ElDesc}}, List, Indent, HTMLOutput) -> + {NI, NI2, I} = case List of + [_] -> {" ", " ", Indent}; + _ -> {[?BR, <<" ", Indent/binary>>], + [?BR, <<" ", Indent/binary>>], + <<" ", Indent/binary>>} + end, + Res = lists:map(fun(E) -> java_gen_map([java_gen(ElDesc, E, I, HTMLOutput)], none, HTMLOutput) end, List), + [?ID_L("put"), ?OP_L("("), ?STR_A(Name), ?OP_L(", "), ?KW_L("new "), ?ID_L("Object"), ?OP_L("[] {"), NI, + list_join_with(Res, [?OP_L(","), NI]), NI2, ?OP_L("});")]. + +java_call(Name, ArgsDesc, Values, HTMLOutput) -> + {Indent, Preamble} = if HTMLOutput -> {<<"">>, []}; true -> {<<" ">>, <<"~~~ java\n">>} end, + [Preamble, + Indent, ?ID_L("XmlRpcClientConfigImpl config"), ?OP_L(" = "), ?KW_L("new "), ?ID_L("XmlRpcClientConfigImpl"), ?OP_L("();"), ?BR, + Indent, ?ID_L("config"), ?OP_L("."), ?ID_L("setServerURL"), ?OP_L("("), ?ID_L("url"), ?OP_L(");"), ?BR, Indent, ?BR, + Indent, ?ID_L("XmlRpcClient client"), ?OP_L(" = "), ?KW_L("new "), ?ID_L("XmlRpcClient"), ?OP_L("();"), ?BR, + Indent, ?ID_L("client"), ?OP_L("."), ?ID_L("setConfig"), ?OP_L("("), ?ID_L("config"), ?OP_L(");"), ?BR, Indent, ?BR, + Indent, ?ID_L("client"), ?OP_L("."), ?ID_L("execute"), ?OP_L("("), ?STR_A(Name), ?OP_L(", "), + java_gen_map(lists:map(fun({A,B})->java_gen(A, B, Indent, HTMLOutput) end, lists:zip(ArgsDesc, Values)), Indent, HTMLOutput), + ?OP_L(");")]. + +-define(XML_S(N, V), ?OP_L("<"), ?FIELD_L(??N), ?OP_L(">"), V). +-define(XML_E(N), ?OP_L("</"), ?FIELD_L(??N), ?OP_L(">")). +-define(XML(N, Indent, V), ?BR, Indent, ?XML_S(N, V), ?BR, Indent, ?XML_E(N)). +-define(XML(N, Indent, D, V), ?XML(N, [Indent, lists:duplicate(D, <<" ">>)], V)). +-define(XML_L(N, Indent, V), ?BR, Indent, ?XML_S(N, V), ?XML_E(N)). +-define(XML_L(N, Indent, D, V), ?XML_L(N, [Indent, lists:duplicate(D, <<" ">>)], V)). + +xml_gen({Name, integer}, Int, Indent, HTMLOutput) -> + [?XML(member, Indent, + [?XML_L(name, Indent, 1, ?ID_A(Name)), + ?XML(value, Indent, 1, + [?XML_L(integer, Indent, 2, ?ID(jlib:integer_to_binary(Int)))])])]; +xml_gen({Name, string}, Str, Indent, HTMLOutput) -> + [?XML(member, Indent, + [?XML_L(name, Indent, 1, ?ID_A(Name)), + ?XML(value, Indent, 1, + [?XML_L(string, Indent, 2, ?ID(Str))])])]; +xml_gen({Name, binary}, Str, Indent, HTMLOutput) -> + [?XML(member, Indent, + [?XML_L(name, Indent, 1, ?ID_A(Name)), + ?XML(value, Indent, 1, + [?XML_L(string, Indent, 2, ?ID(Str))])])]; +xml_gen({Name, atom}, Atom, Indent, HTMLOutput) -> + [?XML(member, Indent, + [?XML_L(name, Indent, 1, ?ID_A(Name)), + ?XML(value, Indent, 1, + [?XML_L(string, Indent, 2, ?ID(atom_to_list(Atom)))])])]; +xml_gen({Name, {tuple, Fields}}, Tuple, Indent, HTMLOutput) -> + NewIndent = <<" ", Indent/binary>>, + Res = lists:map(fun({A, B}) -> xml_gen(A, B, NewIndent, HTMLOutput) end, lists:zip(Fields, tuple_to_list(Tuple))), + [?XML(member, Indent, + [?XML_L(name, Indent, 1, ?ID_A(Name)), + ?XML(value, Indent, 1, [?XML(struct, NewIndent, Res)])])]; +xml_gen({Name, {list, ElDesc}}, List, Indent, HTMLOutput) -> + Ind1 = <<" ", Indent/binary>>, + Ind2 = <<" ", Ind1/binary>>, + Res = lists:map(fun(E) -> [?XML(value, Ind1, [?XML(struct, Ind1, 1, xml_gen(ElDesc, E, Ind2, HTMLOutput))])] end, List), + [?XML(member, Indent, + [?XML_L(name, Indent, 1, ?ID_A(Name)), + ?XML(value, Indent, 1, [?XML(array, Indent, 2, [?XML(data, Indent, 3, Res)])])])]. + +xml_call(Name, ArgsDesc, Values, HTMLOutput) -> + {Indent, Preamble} = if HTMLOutput -> {<<"">>, []}; true -> {<<" ">>, <<"~~~ xml">>} end, + Res = lists:map(fun({A, B}) -> xml_gen(A, B, <<Indent/binary, " ">>, HTMLOutput) end, lists:zip(ArgsDesc, Values)), + [Preamble, + ?XML(methodCall, Indent, + [?XML_L(methodName, Indent, 1, ?ID_A(Name)), + ?XML(params, Indent, 1, + [?XML(param, Indent, 2, + [?XML(value, Indent, 3, + [?XML(struct, Indent, 4, Res)])])])])]. + +% [?ARG_S(Name), ?OP_L(": "), ?STR(Str)]; +json_gen({_Name, integer}, Int, _Indent, HTMLOutput) -> + [?NUM(Int)]; +json_gen({_Name, string}, Str, _Indent, HTMLOutput) -> + [?STR(Str)]; +json_gen({_Name, binary}, Str, _Indent, HTMLOutput) -> + [?STR(Str)]; +json_gen({_Name, atom}, Atom, _Indent, HTMLOutput) -> + [?STR_A(Atom)]; +json_gen({_Name, rescode}, Val, _Indent, HTMLOutput) -> + [?ID_A(Val == ok orelse Val == true)]; +json_gen({_Name, restuple}, {Val, Str}, _Indent, HTMLOutput) -> + [?OP_L("{"), ?STR_L("res"), ?OP_L(": "), ?ID_A(Val == ok orelse Val == true), ?OP_L(", "), + ?STR_L("text"), ?OP_L(": "), ?STR(Str), ?OP_L("}")]; +json_gen({_Name, {list, {_, {tuple, [{_, atom}, ValFmt]}}}}, List, Indent, HTMLOutput) -> + Indent2 = <<" ", Indent/binary>>, + Res = lists:map(fun({N, V})->[?STR_A(N), ?OP_L(": "), json_gen(ValFmt, V, Indent2, HTMLOutput)] end, List), + [?OP_L("{"), ?BR, Indent2, list_join_with(Res, [?OP_L(","), ?BR, Indent2]), ?BR, Indent, ?OP_L("}")]; +json_gen({_Name, {tuple, Fields}}, Tuple, Indent, HTMLOutput) -> + Indent2 = <<" ", Indent/binary>>, + Res = lists:map(fun({{N, _} = A, B})->[?STR_A(N), ?OP_L(": "), json_gen(A, B, Indent2, HTMLOutput)] end, + lists:zip(Fields, tuple_to_list(Tuple))), + [?OP_L("{"), ?BR, Indent2, list_join_with(Res, [?OP_L(","), ?BR, Indent2]), ?BR, Indent, ?OP_L("}")]; +json_gen({_Name, {list, ElDesc}}, List, Indent, HTMLOutput) -> + Indent2 = <<" ", Indent/binary>>, + Res = lists:map(fun(E) -> json_gen(ElDesc, E, Indent2, HTMLOutput) end, List), + [?OP_L("["), ?BR, Indent2, list_join_with(Res, [?OP_L(","), ?BR, Indent2]), ?BR, Indent, ?OP_L("]")]. + +json_call(Name, ArgsDesc, Values, ResultDesc, Result, HTMLOutput) -> + {Indent, Preamble} = if HTMLOutput -> {<<"">>, []}; true -> {<<" ">>, <<"~~~ json\n">>} end, + {Code, ResultStr} = case {ResultDesc, Result} of + {{_, rescode}, V} when V == true; V == ok -> + {200, [?STR_L("")]}; + {{_, rescode}, _} -> + {500, [?STR_L("")]}; + {{_, restuple}, {V1, Text1}} when V1 == true; V1 == ok -> + {200, [?STR(Text1)]}; + {{_, restuple}, {_, Text2}} -> + {500, [?STR(Text2)]}; + {{_, {list, _}}, _} -> + {200, json_gen(ResultDesc, Result, Indent, HTMLOutput)}; + {{_, {tuple, _}}, _} -> + {200, json_gen(ResultDesc, Result, Indent, HTMLOutput)}; + {{Name0, _}, _} -> + {200, [Indent, ?OP_L("{"), ?STR_A(Name0), ?OP_L(": "), + json_gen(ResultDesc, Result, Indent, HTMLOutput), Indent, ?OP_L("}")]} + end, + CodeStr = case Code of + 200 -> <<" 200 OK">>; + 500 -> <<" 500 Internal Server Error">> + end, + [Preamble, + Indent, ?ID_L("POST /api/"), ?ID_A(Name), ?BR, + Indent, ?OP_L("{"), ?BR, Indent, <<" ">>, + list_join_with(lists:map(fun({{N,_}=A,B})->[?STR_A(N), ?OP_L(": "), json_gen(A, B, <<Indent/binary, " ">>, HTMLOutput)] end, + lists:zip(ArgsDesc, Values)), [?OP_L(","), ?BR, Indent, <<" ">>]), + ?BR, Indent, ?OP_L("}"), ?BR, Indent, ?BR, Indent, + ?ID_L("HTTP/1.1"), ?ID(CodeStr), ?BR, Indent, + ResultStr + ]. + +generate_example_input({_Name, integer}, {LastStr, LastNum}) -> + {LastNum+1, {LastStr, LastNum+1}}; +generate_example_input({_Name, string}, {LastStr, LastNum}) -> + {string:chars(LastStr+1, 5), {LastStr+1, LastNum}}; +generate_example_input({_Name, binary}, {LastStr, LastNum}) -> + {iolist_to_binary(string:chars(LastStr+1, 5)), {LastStr+1, LastNum}}; +generate_example_input({_Name, atom}, {LastStr, LastNum}) -> + {list_to_atom(string:chars(LastStr+1, 5)), {LastStr+1, LastNum}}; +generate_example_input({_Name, rescode}, {LastStr, LastNum}) -> + {ok, {LastStr, LastNum}}; +generate_example_input({_Name, restuple}, {LastStr, LastNum}) -> + {{ok, <<"Success">>}, {LastStr, LastNum}}; +generate_example_input({_Name, {tuple, Fields}}, Data) -> + {R, D} = lists:foldl(fun(Field, {Res2, Data2}) -> + {Res3, Data3} = generate_example_input(Field, Data2), + {[Res3 | Res2], Data3} + end, {[], Data}, Fields), + {list_to_tuple(lists:reverse(R)), D}; +generate_example_input({_Name, {list, Desc}}, Data) -> + {R1, D1} = generate_example_input(Desc, Data), + {R2, D2} = generate_example_input(Desc, D1), + {[R1, R2], D2}. + +gen_calls(#ejabberd_commands{args_example=none, args=ArgsDesc} = C, HTMLOutput, Langs) -> + {R, _} = lists:foldl(fun(Arg, {Res, Data}) -> + {Res3, Data3} = generate_example_input(Arg, Data), + {[Res3 | Res], Data3} + end, {[], {$a-1, 0}}, ArgsDesc), + gen_calls(C#ejabberd_commands{args_example=lists:reverse(R)}, HTMLOutput, Langs); +gen_calls(#ejabberd_commands{result_example=none, result=ResultDesc} = C, HTMLOutput, Langs) -> + {R, _} = generate_example_input(ResultDesc, {$a-1, 0}), + gen_calls(C#ejabberd_commands{result_example=R}, HTMLOutput, Langs); +gen_calls(#ejabberd_commands{args_example=Values, args=ArgsDesc, + result_example=Result, result=ResultDesc, + name=Name}, HTMLOutput, Langs) -> + Perl = perl_call(Name, ArgsDesc, Values, HTMLOutput), + Java = java_call(Name, ArgsDesc, Values, HTMLOutput), + XML = xml_call(Name, ArgsDesc, Values, HTMLOutput), + JSON = json_call(Name, ArgsDesc, Values, ResultDesc, Result, HTMLOutput), + if HTMLOutput -> + [?TAG(ul, "code-samples-names", + [case lists:member(<<"java">>, Langs) of true -> ?TAG(li, <<"Java">>); _ -> [] end, + case lists:member(<<"perl">>, Langs) of true -> ?TAG(li, <<"Perl">>); _ -> [] end, + case lists:member(<<"xmlrpc">>, Langs) of true -> ?TAG(li, <<"XML">>); _ -> [] end, + case lists:member(<<"json">>, Langs) of true -> ?TAG(li, <<"JSON">>); _ -> [] end]), + ?TAG(ul, "code-samples", + [case lists:member(<<"java">>, Langs) of true -> ?TAG(li, ?TAG(pre, Java)); _ -> [] end, + case lists:member(<<"perl">>, Langs) of true -> ?TAG(li, ?TAG(pre, Perl)); _ -> [] end, + case lists:member(<<"xmlrpc">>, Langs) of true -> ?TAG(li, ?TAG(pre, XML)); _ -> [] end, + case lists:member(<<"json">>, Langs) of true -> ?TAG(li, ?TAG(pre, JSON)); _ -> [] end])]; + true -> + [<<"\n">>, case lists:member(<<"java">>, Langs) of true -> <<"* Java\n">>; _ -> [] end, + case lists:member(<<"perl">>, Langs) of true -> <<"* Perl\n">>; _ -> [] end, + case lists:member(<<"xmlrpc">>, Langs) of true -> <<"* XmlRPC\n">>; _ -> [] end, + case lists:member(<<"json">>, Langs) of true -> <<"* JSON\n">>; _ -> [] end, + <<"{: .code-samples-labels}\n">>, + case lists:member(<<"java">>, Langs) of true -> [<<"\n* ">>, ?TAG(pre, Java), <<"~~~\n">>]; _ -> [] end, + case lists:member(<<"perl">>, Langs) of true -> [<<"\n* ">>, ?TAG(pre, Perl), <<"~~~\n">>]; _ -> [] end, + case lists:member(<<"xmlrpc">>, Langs) of true -> [<<"\n* ">>, ?TAG(pre, XML), <<"~~~\n">>]; _ -> [] end, + case lists:member(<<"json">>, Langs) of true -> [<<"\n* ">>, ?TAG(pre, JSON), <<"~~~\n">>]; _ -> [] end, + <<"{: .code-samples-tabs}\n\n">>] + end. + +gen_doc(#ejabberd_commands{name=Name, tags=_Tags, desc=Desc, longdesc=LongDesc, + args=Args, args_desc=ArgsDesc, + result=Result, result_desc=ResultDesc}=Cmd, HTMLOutput, Langs) -> + LDesc = case LongDesc of + "" -> Desc; + _ -> LongDesc + end, + ArgsText = case ArgsDesc of + none -> + [?TAG(ul, "args-list", lists:map(fun({AName, Type}) -> + [?TAG(li, [?TAG_R(strong, atom_to_list(AName)), <<" :: ">>, + ?RAW(io_lib:format("~p", [Type]))])] + end, Args))]; + _ -> + [?TAG(dl, "args-list", lists:map(fun({{AName, Type}, ADesc}) -> + [?TAG(dt, [?TAG_R(strong, atom_to_list(AName)), <<" :: ">>, + ?RAW(io_lib:format("~p", [Type]))]), + ?TAG(dd, ?RAW(ADesc))] + end, lists:zip(Args, ArgsDesc)))] + end, + ResultText = case ResultDesc of + none -> + [?RAW(io_lib:format("~p", [Result]))]; + _ -> + [?TAG(dl, [ + ?TAG(dt, io_lib:format("~p", [Result])), + ?TAG_R(dd, ResultDesc)])] + end, + + [?TAG(h1, [?TAG(strong, atom_to_list(Name)), <<" - ">>, ?RAW(Desc)]), + ?TAG(p, ?RAW(LDesc)), + ?TAG(h2, <<"Arguments:">>), + ArgsText, + ?TAG(h2, <<"Result:">>), + ResultText, + ?TAG(h2, <<"Examples:">>), + gen_calls(Cmd, HTMLOutput, Langs)]. + +find_commands_definitions() -> + case code:lib_dir(ejabberd, ebin) of + {error, _} -> + lists:map(fun({N, _, _}) -> + ejabberd_commands:get_command_definition(N) + end, ejabberd_commands:list_commands()); + Path -> + lists:flatmap(fun(P) -> + Mod = list_to_atom(filename:rootname(P)), + code:ensure_loaded(Mod), + case erlang:function_exported(Mod, get_commands_spec, 0) of + true -> + apply(Mod, get_commands_spec, []); + _ -> + [] + end + end, filelib:wildcard("*.beam", Path)) + end. + +generate_html_output(File, RegExp, Languages) -> + Cmds = find_commands_definitions(), + {ok, RE} = re:compile(RegExp), + Cmds2 = lists:filter(fun(#ejabberd_commands{name=Name, module=Module}) -> + re:run(atom_to_list(Name), RE, [{capture, none}]) == match orelse + re:run(atom_to_list(Module), RE, [{capture, none}]) == match + end, Cmds), + Cmds3 = lists:sort(fun(#ejabberd_commands{name=N1}, #ejabberd_commands{name=N2}) -> + N1 =< N2 + end, Cmds2), + Langs = binary:split(Languages, <<",">>, [global]), + Out = lists:map(fun(C) -> gen_doc(C, true, Langs) end, Cmds3), + {ok, Fh} = file:open(File, [write]), + io:format(Fh, "~s", [[html_pre(), Out, html_post()]]), + file:close(Fh), + ok. + +generate_md_output(File, RegExp, Languages) -> + Cmds = find_commands_definitions(), + {ok, RE} = re:compile(RegExp), + Cmds2 = lists:filter(fun(#ejabberd_commands{name=Name, module=Module}) -> + re:run(atom_to_list(Name), RE, [{capture, none}]) == match orelse + re:run(atom_to_list(Module), RE, [{capture, none}]) == match + end, Cmds), + Cmds3 = lists:sort(fun(#ejabberd_commands{name=N1}, #ejabberd_commands{name=N2}) -> + N1 =< N2 + end, Cmds2), + Langs = binary:split(Languages, <<",">>, [global]), + Header = <<"---\ntitle: Administration API reference\nbodyclass: nocomment\n---">>, + Out = lists:map(fun(C) -> gen_doc(C, false, Langs) end, Cmds3), + {ok, Fh} = file:open(File, [write]), + io:format(Fh, "~s~s", [Header, Out]), + file:close(Fh), + ok. + +html_pre() -> + "<!DOCTYPE> +<html> + <head> + <meta http-equiv='content-type' content='text/html; charset=utf-8' /> + <style> + body { + margin: 0 auto; + font-family: Georgia, Palatino, serif; + color: #000; + line-height: 1; + max-width: 80%; + padding: 10px; + } + h1, h2, h3, h4 { + color: #111111; + font-weight: 400; + } + h1, h2, h3, h4, h5, p { + margin-bottom: 24px; + padding: 0; + } + h1 { + margin-top: 80px; + font-size: 36px; + } + h2 { + font-size: 24px; + margin: 24px 0 6px; + } + ul, ol { + padding: 0; + margin: 0; + } + li { + line-height: 24px; + } + li ul, li ul { + margin-left: 24px; + } + p, ul, ol { + font-size: 16px; + line-height: 24px; + max-width: 80%; + } + .id {color: #bbb} + .lit {color: #aaa} + .op {color: #9f9} + .str {color: #f00} + .num {color: white} + .field {color: #faa} + .kw {font-weight: bold; color: #ff6} + .code-samples li { + font-family: Consolas, Monaco, Andale Mono, monospace; + line-height: 1.5; + font-size: 13px; + background: #333; + overflow: auto; + margin: 0; + padding: 0; + } + .code-samples pre { + margin: 0; + padding: 0.5em 0.5em; + } + .code-samples { + position: relative; + } + .code-samples-names li { + display: block; + } + .code-samples-names li { + color: white; + background: #9c1; + float: left; + margin: 0 1px -4px 0; + position: relative; + z-index: 1; + border: 4px solid #9c1; + border-bottom: 0; + border-radius: 9px 9px 0 0; + padding: 0.2em 1em 4px 1em; + cursor: pointer; + } + .code-samples-names li.selected { + background: #333; + } + .code-samples { + clear: both; + } + .code-samples li { + display: block; + border: 4px solid #9c1; + border-radius: 9px; + border-top-left-radius: 0; + width: 100%; + } + .args-list li { + display: block; + } + </style> +</head> + <body> + <script> + function changeTab2(tab, addClickHandlers) { + var els = tab.parentNode.childNodes; + var els2 = tab.parentNode.nextSibling.childNodes; + for (var i = 0; i < els.length; i++) { + if (addClickHandlers) + els[i].addEventListener('click', changeTab, false); + + if (els[i] == tab) { + els[i].setAttribute('class', 'selected'); + els2[i].style.display = 'block'; + } else { + els[i].removeAttribute('class'); + els2[i].style.display = 'none'; + } + } + } + function changeTab(event) { + changeTab2(event.target); + } + </script>". + +html_post() -> +"<script> + var ul = document.getElementsByTagName('ul'); + for (var i = 0; i < ul.length; i++) { + if (ul[i].className == 'code-samples-names') + changeTab2(ul[i].firstChild, true); + } + </script> + </body> +</html>". diff --git a/src/ejabberd_config.erl b/src/ejabberd_config.erl index 5d1df505..6f7368b7 100644 --- a/src/ejabberd_config.erl +++ b/src/ejabberd_config.erl @@ -5,7 +5,7 @@ %%% Created : 14 Dec 2002 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -36,13 +36,16 @@ prepare_opt_val/4, convert_table_to_binary/5, transform_options/1, collect_options/1, convert_to_yaml/1, convert_to_yaml/2, - env_binary_to_list/2]). + env_binary_to_list/2, opt_type/1, may_hide_data/1]). + +-export([start/2]). -include("ejabberd.hrl"). -include("logger.hrl"). -include("ejabberd_config.hrl"). -include_lib("kernel/include/file.hrl"). +-callback opt_type(atom()) -> function() | [atom()]. %% @type macro() = {macro_key(), macro_value()} @@ -51,8 +54,48 @@ %% @type macro_value() = term(). - start() -> + mnesia_init(), + Config = get_ejabberd_config_path(), + State0 = read_file(Config), + State1 = hosts_to_start(State0), + State2 = validate_opts(State1), + %% This start time is used by mod_last: + UnixTime = p1_time_compat:system_time(seconds), + SharedKey = case erlang:get_cookie() of + nocookie -> + p1_sha:sha(randoms:get_string()); + Cookie -> + p1_sha:sha(jlib:atom_to_binary(Cookie)) + end, + State3 = set_option({node_start, global}, UnixTime, State2), + State4 = set_option({shared_key, global}, SharedKey, State3), + set_opts(State4). + +%% When starting ejabberd for testing, we sometimes want to start a +%% subset of hosts from the one define in the config file. +%% This function override the host list read from config file by the +%% one we provide. +%% Hosts to start are defined in an ejabberd application environment +%% variable 'hosts' to make it easy to ignore some host in config +%% file. +hosts_to_start(State) -> + case application:get_env(ejabberd, hosts) of + undefined -> + %% Start all hosts as defined in config file + State; + {ok, Hosts} -> + set_hosts_in_options(Hosts, State) + end. + +%% @private +%% At the moment, these functions are mainly used to setup unit tests. +-spec(start/2 :: (Hosts :: [binary()], Opts :: [acl:acl() | local_config()]) -> ok). +start(Hosts, Opts) -> + mnesia_init(), + set_opts(#state{hosts = Hosts, opts = Opts}). + +mnesia_init() -> case catch mnesia:table_info(local_config, storage_type) of disc_copies -> mnesia:delete_table(local_config); @@ -63,21 +106,7 @@ start() -> [{ram_copies, [node()]}, {local_content, true}, {attributes, record_info(fields, local_config)}]), - mnesia:add_table_copy(local_config, node(), ram_copies), - Config = get_ejabberd_config_path(), - State = read_file(Config), - %% This start time is used by mod_last: - {MegaSecs, Secs, _} = now(), - UnixTime = MegaSecs*1000000 + Secs, - SharedKey = case erlang:get_cookie() of - nocookie -> - p1_sha:sha(randoms:get_string()); - Cookie -> - p1_sha:sha(jlib:atom_to_binary(Cookie)) - end, - State1 = set_option({node_start, global}, UnixTime, State), - State2 = set_option({shared_key, global}, SharedKey, State1), - set_opts(State2). + mnesia:add_table_copy(local_config, node(), ram_copies). %% @doc Get the filename of the ejabberd configuration file. %% The filename can be specified with: erl -config "/path/to/ejabberd.yml". @@ -111,10 +140,11 @@ get_env_config() -> %% @doc Read the ejabberd configuration file. %% It also includes additional configuration files and replaces macros. %% This function will crash if finds some error in the configuration file. -%% @spec (File::string()) -> #state{}. +%% @spec (File::string()) -> #state{} read_file(File) -> read_file(File, [{replace_macros, true}, - {include_files, true}]). + {include_files, true}, + {include_modules_configs, true}]). read_file(File, Opts) -> Terms1 = get_plain_terms_file(File, Opts), @@ -160,7 +190,7 @@ convert_to_yaml(File, Output) -> fun({Host, Opts1}) -> {host_config, [{Host, Opts1}]} end, HOpts), - Data = p1_yaml:encode(lists:reverse(NewOpts)), + Data = fast_yaml:encode(lists:reverse(NewOpts)), case Output of stdout -> io:format("~s~n", [Data]); @@ -189,7 +219,6 @@ env_binary_to_list(Application, Parameter) -> %% Returns a list of plain terms, %% in which the options 'include_config_file' were parsed %% and the terms in those files were included. -%% @spec(string()) -> [term()] %% @spec(iolist()) -> [term()] get_plain_terms_file(File) -> get_plain_terms_file(File, [{include_files, true}]). @@ -200,7 +229,17 @@ get_plain_terms_file(File1, Opts) -> File = get_absolute_path(File1), case consult(File) of {ok, Terms} -> - BinTerms = strings_to_binary(Terms), + BinTerms1 = strings_to_binary(Terms), + ModInc = case proplists:get_bool(include_modules_configs, Opts) of + true -> + Files = [{filename:rootname(filename:basename(F)), F} + || F <- filelib:wildcard(ext_mod:config_dir() ++ "/*.{yml,yaml}") + ++ filelib:wildcard(ext_mod:modules_dir() ++ "/*/conf/*.{yml,yaml}")], + [proplists:get_value(F,Files) || F <- proplists:get_keys(Files)]; + _ -> + [] + end, + BinTerms = BinTerms1 ++ [{include_config_file, list_to_binary(V)} || V <- ModInc], case proplists:get_bool(include_files, Opts) of true -> include_config_files(BinTerms); @@ -215,14 +254,14 @@ get_plain_terms_file(File1, Opts) -> consult(File) -> case filename:extension(File) of Ex when (Ex == ".yml") or (Ex == ".yaml") -> - case p1_yaml:decode_from_file(File, [plain_as_atom]) of + case fast_yaml:decode_from_file(File, [plain_as_atom]) of {ok, []} -> {ok, []}; {ok, [Document|_]} -> {ok, parserl(Document)}; {error, Err} -> Msg1 = "Cannot load " ++ File ++ ": ", - Msg2 = p1_yaml:format_error(Err), + Msg2 = fast_yaml:format_error(Err), {error, Msg1 ++ Msg2} end; _ -> @@ -266,7 +305,7 @@ search_hosts(Term, State) -> {host, Host} -> if State#state.hosts == [] -> - add_hosts_to_option([Host], State); + set_hosts_in_options([Host], State); true -> ?ERROR_MSG("Can't load config file: " "too many hosts definitions", []), @@ -275,7 +314,7 @@ search_hosts(Term, State) -> {hosts, Hosts} -> if State#state.hosts == [] -> - add_hosts_to_option(Hosts, State); + set_hosts_in_options(Hosts, State); true -> ?ERROR_MSG("Can't load config file: " "too many hosts definitions", []), @@ -285,16 +324,19 @@ search_hosts(Term, State) -> State end. -add_hosts_to_option(Hosts, State) -> +set_hosts_in_options(Hosts, State) -> PrepHosts = normalize_hosts(Hosts), - set_option({hosts, global}, PrepHosts, State#state{hosts = PrepHosts}). + NewOpts = lists:filter(fun({local_config,{hosts,global},_}) -> false; + (_) -> true + end, State#state.opts), + set_option({hosts, global}, PrepHosts, State#state{hosts = PrepHosts, opts = NewOpts}). normalize_hosts(Hosts) -> normalize_hosts(Hosts,[]). normalize_hosts([], PrepHosts) -> lists:reverse(PrepHosts); normalize_hosts([Host|Hosts], PrepHosts) -> - case jlib:nodeprep(iolist_to_binary(Host)) of + case jid:nodeprep(iolist_to_binary(Host)) of error -> ?ERROR_MSG("Can't load config file: " "invalid host name [~p]", [Host]), @@ -358,6 +400,62 @@ exit_or_halt(ExitText) -> %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% Support for 'include_config_file' +get_config_option_key(Name, Val) -> + if Name == listen -> + case Val of + {{Port, IP, Trans}, _Mod, _Opts} -> + {Port, IP, Trans}; + {{Port, Trans}, _Mod, _Opts} when Trans == tcp; Trans == udp -> + {Port, {0,0,0,0}, Trans}; + {{Port, IP}, _Mod, _Opts} -> + {Port, IP, tcp}; + {Port, _Mod, _Opts} -> + {Port, {0,0,0,0}, tcp}; + V when is_list(V) -> + lists:foldl( + fun({port, Port}, {_, IP, T}) -> + {Port, IP, T}; + ({ip, IP}, {Port, _, T}) -> + {Port, IP, T}; + ({transport, T}, {Port, IP, _}) -> + {Port, IP, T}; + (_, Res) -> + Res + end, {5222, {0,0,0,0}, tcp}, Val) + end; + is_tuple(Val) -> + element(1, Val); + true -> + Val + end. + +maps_to_lists(IMap) -> + maps:fold(fun(Name, Map, Res) when Name == host_config orelse Name == append_host_config -> + [{Name, [{Host, maps_to_lists(SMap)} || {Host,SMap} <- maps:values(Map)]} | Res]; + (Name, Map, Res) when is_map(Map) -> + [{Name, maps:values(Map)} | Res]; + (Name, Val, Res) -> + [{Name, Val} | Res] + end, [], IMap). + +merge_configs(Terms, ResMap) -> + lists:foldl(fun({Name, Val}, Map) when is_list(Val), Name =/= auth_method -> + Old = maps:get(Name, Map, #{}), + New = lists:foldl(fun(SVal, OMap) -> + NVal = if Name == host_config orelse Name == append_host_config -> + {Host, Opts} = SVal, + {_, SubMap} = maps:get(Host, OMap, {Host, #{}}), + {Host, merge_configs(Opts, SubMap)}; + true -> + SVal + end, + maps:put(get_config_option_key(Name, SVal), NVal, OMap) + end, Old, Val), + maps:put(Name, New, Map); + ({Name, Val}, Map) -> + maps:put(Name, Val, Map) + end, ResMap, Terms). + %% @doc Include additional configuration files in the list of terms. %% @spec ([term()]) -> [term()] include_config_files(Terms) -> @@ -374,7 +472,10 @@ include_config_files(Terms) -> fun({File, Opts}) -> include_config_file(File, Opts) end, lists:flatten(FileOpts)), - Terms1 ++ Terms2. + + M1 = merge_configs(transform_terms(Terms1), #{}), + M2 = merge_configs(transform_terms(Terms2), M1), + maps_to_lists(M2). transform_include_option({include_config_file, File}) when is_list(File) -> case is_string(File) of @@ -452,11 +553,11 @@ split_terms_macros(Terms) -> lists:foldl( fun(Term, {TOs, Ms}) -> case Term of - {define_macro, Key, Value} -> + {define_macro, Key, Value} -> case is_correct_macro({Key, Value}) of - true -> + true -> {TOs, Ms++[{Key, Value}]}; - false -> + false -> exit({macro_not_properly_defined, Term}) end; {define_macro, KeyVals} -> @@ -686,6 +787,46 @@ get_option(Opt, F, Default) -> end end. +get_modules_with_options() -> + {ok, Mods} = application:get_key(ejabberd, modules), + ExtMods = [Name || {Name, _Details} <- ext_mod:installed()], + lists:foldl( + fun(Mod, D) -> + case catch Mod:opt_type('') of + Opts when is_list(Opts) -> + lists:foldl( + fun(Opt, Acc) -> + dict:append(Opt, Mod, Acc) + end, D, Opts); + {'EXIT', {undef, _}} -> + D + end + end, dict:new(), [?MODULE|ExtMods++Mods]). + +validate_opts(#state{opts = Opts} = State) -> + ModOpts = get_modules_with_options(), + NewOpts = lists:filter( + fun(#local_config{key = {Opt, _Host}, value = Val}) -> + case dict:find(Opt, ModOpts) of + {ok, [Mod|_]} -> + VFun = Mod:opt_type(Opt), + case catch VFun(Val) of + {'EXIT', _} -> + ?ERROR_MSG("ignoring option '~s' with " + "invalid value: ~p", + [Opt, Val]), + false; + _ -> + true + end; + _ -> + ?ERROR_MSG("unknown option '~s' will be likely" + " ignored", [Opt]), + true + end + end, Opts), + State#state{opts = NewOpts}. + -spec get_vh_by_auth_method(atom()) -> [binary()]. %% Return the list of hosts handled by a given module @@ -739,20 +880,31 @@ replace_module(mod_roster_odbc) -> {mod_roster, odbc}; replace_module(mod_shared_roster_odbc) -> {mod_shared_roster, odbc}; replace_module(mod_vcard_odbc) -> {mod_vcard, odbc}; replace_module(mod_vcard_xupdate_odbc) -> {mod_vcard_xupdate, odbc}; +replace_module(mod_pubsub_odbc) -> {mod_pubsub, odbc}; replace_module(Module) -> case is_elixir_module(Module) of true -> expand_elixir_module(Module); false -> Module end. -replace_modules(Modules) -> lists:map( fun({Module, Opts}) -> case - replace_module(Module) of {NewModule, DBType} -> - emit_deprecation_warning(Module, NewModule, DBType), NewOpts = - [{db_type, DBType} | lists:keydelete(db_type, 1, Opts)], - {NewModule, transform_module_options(Module, NewOpts)}; NewModule - -> if Module /= NewModule -> emit_deprecation_warning(Module, - NewModule); true -> ok end, {NewModule, - transform_module_options(Module, Opts)} end end, Modules). +replace_modules(Modules) -> + lists:map( + fun({Module, Opts}) -> + case replace_module(Module) of + {NewModule, DBType} -> + emit_deprecation_warning(Module, NewModule, DBType), + NewOpts = [{db_type, DBType} | + lists:keydelete(db_type, 1, Opts)], + {NewModule, transform_module_options(Module, NewOpts)}; + NewModule -> + if Module /= NewModule -> + emit_deprecation_warning(Module, NewModule); + true -> + ok + end, + {NewModule, transform_module_options(Module, Opts)} + end + end, Modules). %% Elixir module naming %% ==================== @@ -1062,3 +1214,33 @@ emit_deprecation_warning(Module, NewModule) -> ?WARNING_MSG("Module ~s is deprecated, use ~s instead", [Module, NewModule]) end. + +opt_type(hide_sensitive_log_data) -> + fun (H) when is_boolean(H) -> H end; +opt_type(hosts) -> + fun(L) when is_list(L) -> + lists:map( + fun(H) -> + iolist_to_binary(H) + end, L) + end; +opt_type(language) -> + fun iolist_to_binary/1; +opt_type(_) -> + [hide_sensitive_log_data, hosts, language]. + +-spec may_hide_data(string()) -> string(); + (binary()) -> binary(). + +may_hide_data(Data) -> + case ejabberd_config:get_option( + hide_sensitive_log_data, + fun(false) -> false; + (true) -> true + end, + false) of + false -> + Data; + true -> + "hidden_by_ejabberd" + end. diff --git a/src/ejabberd_ctl.erl b/src/ejabberd_ctl.erl index 6ab383b1..7a26644f 100644 --- a/src/ejabberd_ctl.erl +++ b/src/ejabberd_ctl.erl @@ -5,7 +5,7 @@ %%% Created : 11 Jan 2004 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -44,14 +44,13 @@ -module(ejabberd_ctl). + +-behaviour(ejabberd_config). -author('alexey@process-one.net'). --export([start/0, - init/0, - process/1, - process2/2, - register_commands/3, - unregister_commands/3]). +-export([start/0, init/0, process/1, process2/2, + register_commands/3, unregister_commands/3, + opt_type/1]). -include("ejabberd_ctl.hrl"). -include("ejabberd_commands.hrl"). @@ -64,37 +63,40 @@ %%----------------------------- start() -> - case init:get_plain_arguments() of - [SNode | Args] -> - SNode1 = case string:tokens(SNode, "@") of - [_Node, _Server] -> - SNode; - _ -> - case net_kernel:longnames() of - true -> - lists:flatten([SNode, "@", inet_db:gethostname(), - ".", inet_db:res_option(domain)]); - false -> - lists:flatten([SNode, "@", inet_db:gethostname()]); - _ -> - SNode - end - end, - Node = list_to_atom(SNode1), - Status = case rpc:call(Node, ?MODULE, process, [Args]) of - {badrpc, Reason} -> - print("Failed RPC connection to the node ~p: ~p~n", - [Node, Reason]), - %% TODO: show minimal start help - ?STATUS_BADRPC; - S -> - S - end, - halt(Status); - _ -> - print_usage(), - halt(?STATUS_USAGE) - end. + [SNode, Timeout, Args] = case init:get_plain_arguments() of + [SNode2, "--no-timeout" | Args2] -> + [SNode2, infinity, Args2]; + [SNode3 | Args3] -> + [SNode3, 60000, Args3]; + _ -> + print_usage(), + halt(?STATUS_USAGE) + end, + SNode1 = case string:tokens(SNode, "@") of + [_Node, _Server] -> + SNode; + _ -> + case net_kernel:longnames() of + true -> + lists:flatten([SNode, "@", inet_db:gethostname(), + ".", inet_db:res_option(domain)]); + false -> + lists:flatten([SNode, "@", inet_db:gethostname()]); + _ -> + SNode + end + end, + Node = list_to_atom(SNode1), + Status = case rpc:call(Node, ?MODULE, process, [Args], Timeout) of + {badrpc, Reason} -> + print("Failed RPC connection to the node ~p: ~p~n", + [Node, Reason]), + %% TODO: show minimal start help + ?STATUS_BADRPC; + S -> + S + end, + halt(Status). init() -> ets:new(ejabberd_ctl_cmds, [named_table, set, public]), @@ -210,9 +212,9 @@ process(Args) -> %% @spec (Args::[string()], AccessCommands) -> {String::string(), Code::integer()} process2(["--auth", User, Server, Pass | Args], AccessCommands) -> - process2(Args, {list_to_binary(User), list_to_binary(Server), list_to_binary(Pass)}, AccessCommands); + process2(Args, {list_to_binary(User), list_to_binary(Server), list_to_binary(Pass), true}, AccessCommands); process2(Args, AccessCommands) -> - process2(Args, noauth, AccessCommands). + process2(Args, admin, AccessCommands). process2(Args, Auth, AccessCommands) -> case try_run_ctp(Args, Auth, AccessCommands) of @@ -284,7 +286,7 @@ call_command([CmdString | Args], Auth, AccessCommands) -> CmdStringU = ejabberd_regexp:greplace( list_to_binary(CmdString), <<"-">>, <<"_">>), Command = list_to_atom(binary_to_list(CmdStringU)), - case ejabberd_commands:get_command_format(Command) of + case ejabberd_commands:get_command_format(Command, Auth) of {error, command_unknown} -> {error, command_unknown}; {ArgsFormat, ResultFormat} -> @@ -322,7 +324,7 @@ format_args(Args, ArgsFormat) -> format_arg(Arg, integer) -> format_arg2(Arg, "~d"); format_arg(Arg, binary) -> - list_to_binary(format_arg(Arg, string)); + unicode:characters_to_binary(Arg, utf8); format_arg("", string) -> ""; format_arg(Arg, string) -> @@ -347,12 +349,21 @@ format_result(Atom, {_Name, atom}) -> format_result(Int, {_Name, integer}) -> io_lib:format("~p", [Int]); -format_result(String, {_Name, string}) when is_list(String) -> +format_result([A|_]=String, {_Name, string}) when is_list(String) and is_integer(A) -> io_lib:format("~s", [String]); format_result(Binary, {_Name, string}) when is_binary(Binary) -> io_lib:format("~s", [binary_to_list(Binary)]); +format_result(Atom, {_Name, string}) when is_atom(Atom) -> + io_lib:format("~s", [atom_to_list(Atom)]); + +format_result(Integer, {_Name, string}) when is_integer(Integer) -> + io_lib:format("~s", [integer_to_list(Integer)]); + +format_result(Other, {_Name, string}) -> + io_lib:format("~p", [Other]); + format_result(Code, {_Name, rescode}) -> make_status(Code); @@ -384,7 +395,10 @@ format_result(ElementsTuple, {_Name, {tuple, ElementsDef}}) -> fun({Element, ElementDef}) -> ["\t" | format_result(Element, ElementDef)] end, - ElementsAndDef)]. + ElementsAndDef)]; + +format_result(404, {_Name, _}) -> + make_status(not_found). make_status(ok) -> ?STATUS_SUCCESS; make_status(true) -> ?STATUS_SUCCESS; @@ -404,7 +418,8 @@ get_list_commands() -> end. %% Return: {string(), [string()], string()} -tuple_command_help({Name, Args, Desc}) -> +tuple_command_help({Name, _Args, Desc}) -> + {Args, _} = ejabberd_commands:get_command_format(Name, admin), Arguments = [atom_to_list(ArgN) || {ArgN, _ArgF} <- Args], Prepend = case is_supported_args(Args) of true -> ""; @@ -458,7 +473,7 @@ print_usage(HelpMode, MaxC, ShCode) -> get_list_ctls(), print( - ["Usage: ", ?B("ejabberdctl"), " [--node ", ?U("nodename"), "] [--auth ", + ["Usage: ", ?B("ejabberdctl"), " [--no-timeout] [--node ", ?U("nodename"), "] [--auth ", ?U("user"), " ", ?U("host"), " ", ?U("password"), "] ", ?U("command"), " [", ?U("options"), "]\n" "\n" @@ -715,12 +730,13 @@ print_usage_command(Cmd, C, MaxC, ShCode) -> tags = TagsAtoms, desc = Desc, longdesc = LongDesc, - args = ArgsDef, result = ResultDef} = C, NameFmt = [" ", ?B("Command Name"), ": ", Cmd, "\n"], %% Initial indentation of result is 13 = length(" Arguments: ") + {ArgsDef, _} = ejabberd_commands:get_command_format( + C#ejabberd_commands.name, admin), Args = [format_usage_ctype(ArgDef, 13) || ArgDef <- ArgsDef], ArgsMargin = lists:duplicate(13, $\s), ArgsListFmt = case Args of @@ -792,3 +808,7 @@ print(Format, Args) -> %%format_usage_xmlrpc(ArgsDef, ResultDef) -> %% ["aaaa bbb ccc"]. + +opt_type(ejabberdctl_access_commands) -> + fun (V) when is_list(V) -> V end; +opt_type(_) -> [ejabberdctl_access_commands]. diff --git a/src/ejabberd_frontend_socket.erl b/src/ejabberd_frontend_socket.erl index b169572b..b8e706f2 100644 --- a/src/ejabberd_frontend_socket.erl +++ b/src/ejabberd_frontend_socket.erl @@ -5,7 +5,7 @@ %%% Created : 23 Aug 2006 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -58,10 +58,6 @@ %%==================================================================== %% API %%==================================================================== -%%-------------------------------------------------------------------- -%% Function: start_link() -> {ok,Pid} | ignore | {error,Error} -%% Description: Starts the server -%%-------------------------------------------------------------------- start_link(Module, SockMod, Socket, Opts, Receiver) -> gen_server:start_link(?MODULE, [Module, SockMod, Socket, Opts, Receiver], []). @@ -137,18 +133,10 @@ peername(_FsmRef) -> %%gen_server:call(FsmRef, peername). {ok, {{0, 0, 0, 0}, 0}}. - %%==================================================================== %% gen_server callbacks %%==================================================================== -%%-------------------------------------------------------------------- -%% Function: init(Args) -> {ok, State} | -%% {ok, State, Timeout} | -%% ignore | -%% {stop, Reason} -%% Description: Initiates the server -%%-------------------------------------------------------------------- init([Module, SockMod, Socket, Opts, Receiver]) -> Node = ejabberd_node_groups:get_closest_node(backend), {SockMod2, Socket2} = check_starttls(SockMod, Socket, Receiver, Opts), @@ -159,32 +147,22 @@ init([Module, SockMod, Socket, Opts, Receiver]) -> socket = Socket2, receiver = Receiver}}. -%%-------------------------------------------------------------------- -%% 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({starttls, TLSOpts}, _From, State) -> - {ok, TLSSocket} = p1_tls:tcp_to_tls(State#state.socket, TLSOpts), + {ok, TLSSocket} = fast_tls:tcp_to_tls(State#state.socket, TLSOpts), ejabberd_receiver:starttls(State#state.receiver, TLSSocket), Reply = ok, - {reply, Reply, State#state{socket = TLSSocket, sockmod = p1_tls}, + {reply, Reply, State#state{socket = TLSSocket, sockmod = fast_tls}, ?HIBERNATE_TIMEOUT}; handle_call({starttls, TLSOpts, Data}, _From, State) -> - {ok, TLSSocket} = p1_tls:tcp_to_tls(State#state.socket, TLSOpts), + {ok, TLSSocket} = fast_tls:tcp_to_tls(State#state.socket, TLSOpts), ejabberd_receiver:starttls(State#state.receiver, TLSSocket), catch (State#state.sockmod):send( State#state.socket, Data), Reply = ok, {reply, Reply, - State#state{socket = TLSSocket, sockmod = p1_tls}, + State#state{socket = TLSSocket, sockmod = fast_tls}, ?HIBERNATE_TIMEOUT}; - handle_call({compress, Data}, _From, State) -> {ok, ZlibSocket} = ejabberd_receiver:compress(State#state.receiver, Data), @@ -209,10 +187,10 @@ handle_call(get_sockmod, _From, State) -> Reply = State#state.sockmod, {reply, Reply, State, ?HIBERNATE_TIMEOUT}; handle_call(get_peer_certificate, _From, State) -> - Reply = p1_tls:get_peer_certificate(State#state.socket), + Reply = fast_tls:get_peer_certificate(State#state.socket), {reply, Reply, State, ?HIBERNATE_TIMEOUT}; handle_call(get_verify_result, _From, State) -> - Reply = p1_tls:get_verify_result(State#state.socket), + Reply = fast_tls:get_verify_result(State#state.socket), {reply, Reply, State, ?HIBERNATE_TIMEOUT}; handle_call(close, _From, State) -> ejabberd_receiver:close(State#state.receiver), @@ -238,21 +216,9 @@ handle_call(peername, _From, State) -> handle_call(_Request, _From, State) -> Reply = ok, {reply, Reply, State, ?HIBERNATE_TIMEOUT}. -%%-------------------------------------------------------------------- -%% Function: handle_cast(Msg, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling cast messages -%%-------------------------------------------------------------------- handle_cast(_Msg, State) -> {noreply, State, ?HIBERNATE_TIMEOUT}. -%%-------------------------------------------------------------------- -%% Function: handle_info(Info, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling all non call/cast messages -%%-------------------------------------------------------------------- handle_info(timeout, State) -> proc_lib:hibernate(gen_server, enter_loop, [?MODULE, [], State]), @@ -260,34 +226,19 @@ handle_info(timeout, State) -> handle_info(_Info, State) -> {noreply, State, ?HIBERNATE_TIMEOUT}. -%%-------------------------------------------------------------------- -%% 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) -> 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 -%%-------------------------------------------------------------------- check_starttls(SockMod, Socket, Receiver, Opts) -> TLSEnabled = proplists:get_bool(tls, Opts), TLSOpts = lists:filter(fun({certfile, _}) -> true; (_) -> false end, Opts), - if - TLSEnabled -> - {ok, TLSSocket} = p1_tls:tcp_to_tls(Socket, TLSOpts), + if TLSEnabled -> + {ok, TLSSocket} = fast_tls:tcp_to_tls(Socket, TLSOpts), ejabberd_receiver:starttls(Receiver, TLSSocket), - {p1_tls, TLSSocket}; + {fast_tls, TLSSocket}; true -> {SockMod, Socket} end. diff --git a/src/ejabberd_hooks.erl b/src/ejabberd_hooks.erl index d136ba70..c1daa4c0 100644 --- a/src/ejabberd_hooks.erl +++ b/src/ejabberd_hooks.erl @@ -5,7 +5,7 @@ %%% Created : 8 Aug 2004 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -58,9 +58,6 @@ -include("logger.hrl"). -%% Timeout of 5 seconds in calls to distributed hooks --define(TIMEOUT_DISTRIBUTED_HOOK, 5000). - -record(state, {}). -type local_hook() :: { Seq :: integer(), Module :: atom(), Function :: atom()}. -type distributed_hook() :: { Seq :: integer(), Node :: atom(), Module :: atom(), Function :: atom()}. @@ -310,7 +307,7 @@ run1([], _Hook, _Args) -> %% It is not attempted again in case of failure. Next hook will be executed run1([{_Seq, Node, Module, Function} | Ls], Hook, Args) -> %% MR: Should we have a safe rpc, like we have a safe apply or is bad_rpc enough ? - case rpc:call(Node, Module, Function, Args, ?TIMEOUT_DISTRIBUTED_HOOK) of + case ejabberd_cluster:call(Node, Module, Function, Args) of timeout -> ?ERROR_MSG("Timeout on RPC to ~p~nrunning hook: ~p", [Node, {Hook, Args}]), @@ -344,7 +341,7 @@ run1([{_Seq, Module, Function} | Ls], Hook, Args) -> run_fold1([], _Hook, Val, _Args) -> Val; run_fold1([{_Seq, Node, Module, Function} | Ls], Hook, Val, Args) -> - case rpc:call(Node, Module, Function, [Val | Args], ?TIMEOUT_DISTRIBUTED_HOOK) of + case ejabberd_cluster:call(Node, Module, Function, [Val | Args]) of {badrpc, Reason} -> ?ERROR_MSG("Bad RPC error to ~p: ~p~nrunning hook: ~p", [Node, Reason, {Hook, Args}]), diff --git a/src/ejabberd_http.erl b/src/ejabberd_http.erl index 4e7f4b55..d8d1ddd4 100644 --- a/src/ejabberd_http.erl +++ b/src/ejabberd_http.erl @@ -5,7 +5,7 @@ %%% Created : 27 Feb 2004 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,6 +25,8 @@ -module(ejabberd_http). +-behaviour(ejabberd_config). + -author('alexey@process-one.net'). %% External exports @@ -32,8 +34,7 @@ socket_type/0, receive_headers/1, url_encode/1, transform_listen_option/2]). -%% Callbacks --export([init/2]). +-export([init/2, opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -83,8 +84,9 @@ "">>). start(SockData, Opts) -> - supervisor:start_child(ejabberd_http_sup, - [SockData, Opts]). + {ok, + proc_lib:spawn(ejabberd_http, init, + [SockData, Opts])}. start_link(SockData, Opts) -> {ok, @@ -95,6 +97,7 @@ init({SockMod, Socket}, Opts) -> TLSEnabled = proplists:get_bool(tls, Opts), TLSOpts1 = lists:filter(fun ({certfile, _}) -> true; ({ciphers, _}) -> true; + ({dhfile, _}) -> true; (_) -> false end, Opts), @@ -114,9 +117,9 @@ init({SockMod, Socket}, Opts) -> TLSOpts = [verify_none | TLSOpts3], {SockMod1, Socket1} = if TLSEnabled -> inet:setopts(Socket, [{recbuf, 8192}]), - {ok, TLSSocket} = p1_tls:tcp_to_tls(Socket, + {ok, TLSSocket} = fast_tls:tcp_to_tls(Socket, TLSOpts), - {p1_tls, TLSSocket}; + {fast_tls, TLSSocket}; true -> {SockMod, Socket} end, Captcha = case proplists:get_bool(captcha, Opts) of @@ -158,9 +161,14 @@ init({SockMod, Socket}, Opts) -> default_host = DefaultHost, options = Opts, request_handlers = RequestHandlers}, - receive_headers(State). + try receive_headers(State) of + V -> V + catch + {error, _} -> State + end. -become_controller(_Pid) -> ok. +become_controller(_Pid) -> + ok. socket_type() -> raw. @@ -195,22 +203,20 @@ parse_headers(#state{request_method = Method, trail = Data} = State) -> PktType = case Method of - undefined -> http_bin; - _ -> httph_bin - end, + undefined -> http_bin; + _ -> httph_bin + end, case erlang:decode_packet(PktType, Data, []) of - {ok, Pkt, Rest} -> - NewState = process_header(State#state{trail = Rest}, {ok, Pkt}), + {ok, Pkt, Rest} -> + NewState = process_header(State#state{trail = Rest}, {ok, Pkt}), case NewState#state.end_of_request of - true -> - ok; - _ -> - parse_headers(NewState) + true -> ok; + _ -> parse_headers(NewState) end; - {more, _} -> - receive_headers(State#state{trail = Data}); - _ -> - ok + {more, _} -> + receive_headers(State#state{trail = Data}); + _ -> + ok end. process_header(State, Data) -> @@ -260,10 +266,8 @@ process_header(State, Data) -> State#state{request_host = Host, request_headers = add_header(Name, Host, State)}; {ok, {http_header, _, Name, _, Value}} when is_binary(Name) -> - State#state{request_headers = - add_header(normalize_header_name(Name), - Value, - State)}; + State#state{request_headers = + add_header(normalize_header_name(Name), Value, State)}; {ok, {http_header, _, Name, _, Value}} -> State#state{request_headers = add_header(Name, Value, State)}; @@ -284,16 +288,18 @@ process_header(State, Data) -> HostProvided), State2 = State#state{request_host = Host, request_port = Port, request_tp = TP}, - Out = process_request(State2), - send_text(State2, Out), - case State2#state.request_keepalive of + {State3, Out} = process_request(State2), + send_text(State3, Out), + case State3#state.request_keepalive of true -> #state{sockmod = SockMod, socket = Socket, + trail = State3#state.trail, options = State#state.options, default_host = State#state.default_host, request_handlers = State#state.request_handlers}; _ -> #state{end_of_request = true, + trail = State3#state.trail, options = State#state.options, default_host = State#state.default_host, request_handlers = State#state.request_handlers} @@ -316,22 +322,14 @@ get_host_really_served(Default, Provided) -> false -> Default end. -%% @spec (SockMod, HostPort) -> {Host::string(), Port::integer(), TP} -%% where -%% SockMod = gen_tcp | tls -%% HostPort = string() -%% TP = http | https -%% @doc Given a socket and hostport header, return data of transfer protocol. -%% Note that HostPort can be a string of a host like "example.org", -%% or a string of a host and port like "example.org:5280". get_transfer_protocol(SockMod, HostPort) -> [Host | PortList] = str:tokens(HostPort, <<":">>), case {SockMod, PortList} of {gen_tcp, []} -> {Host, 80, http}; {gen_tcp, [Port]} -> {Host, jlib:binary_to_integer(Port), http}; - {p1_tls, []} -> {Host, 443, https}; - {p1_tls, [Port]} -> + {fast_tls, []} -> {Host, 443, https}; + {fast_tls, [Port]} -> {Host, jlib:binary_to_integer(Port), https} end. @@ -371,20 +369,20 @@ process(Handlers, Request, Socket, SockMod, Trail) -> end. extract_path_query(#state{request_method = Method, - request_path = {abs_path, Path}}) + request_path = {abs_path, Path}} = State) when Method =:= 'GET' orelse Method =:= 'HEAD' orelse Method =:= 'DELETE' orelse Method =:= 'OPTIONS' -> case catch url_decode_q_split(Path) of - {'EXIT', _} -> false; - {NPath, Query} -> - LPath = normalize_path([NPE - || NPE <- str:tokens(path_decode(NPath), <<"/">>)]), - LQuery = case catch parse_urlencoded(Query) of - {'EXIT', _Reason} -> []; - LQ -> LQ - end, - {LPath, LQuery, <<"">>} + {'EXIT', _} -> {State, false}; + {NPath, Query} -> + LPath = normalize_path([NPE + || NPE <- str:tokens(path_decode(NPath), <<"/">>)]), + LQuery = case catch parse_urlencoded(Query) of + {'EXIT', _Reason} -> []; + LQ -> LQ + end, + {State, {LPath, LQuery, <<"">>}} end; extract_path_query(#state{request_method = Method, request_path = {abs_path, Path}, @@ -393,45 +391,49 @@ extract_path_query(#state{request_method = Method, socket = _Socket} = State) when (Method =:= 'POST' orelse Method =:= 'PUT') andalso is_integer(Len) -> - Data = recv_data(State, Len), + {NewState, Data} = recv_data(State, Len), ?DEBUG("client data: ~p~n", [Data]), case catch url_decode_q_split(Path) of - {'EXIT', _} -> false; - {NPath, _Query} -> - LPath = normalize_path([NPE - || NPE <- str:tokens(path_decode(NPath), <<"/">>)]), - LQuery = case catch parse_urlencoded(Data) of - {'EXIT', _Reason} -> []; - LQ -> LQ - end, - {LPath, LQuery, Data} + {'EXIT', _} -> {NewState, false}; + {NPath, _Query} -> + LPath = normalize_path([NPE + || NPE <- str:tokens(path_decode(NPath), <<"/">>)]), + LQuery = case catch parse_urlencoded(Data) of + {'EXIT', _Reason} -> []; + LQ -> LQ + end, + {NewState, {LPath, LQuery, Data}} end; -extract_path_query(_State) -> - false. +extract_path_query(State) -> + {State, false}. process_request(#state{request_method = Method, - request_auth = Auth, - request_lang = Lang, - sockmod = SockMod, - socket = Socket, - options = Options, - request_host = Host, - request_port = Port, - request_tp = TP, - request_headers = RequestHeaders, - request_handlers = RequestHandlers, - trail = Trail} = State) -> + request_auth = Auth, + request_lang = Lang, + sockmod = SockMod, + socket = Socket, + options = Options, + request_host = Host, + request_port = Port, + request_tp = TP, + request_headers = RequestHeaders, + request_handlers = RequestHandlers, + trail = Trail} = State) -> case extract_path_query(State) of - false -> - make_bad_request(State); - {LPath, LQuery, Data} -> - {ok, IPHere} = + {State2, false} -> + {State2, make_bad_request(State)}; + {State2, {LPath, LQuery, Data}} -> + PeerName = case SockMod of gen_tcp -> inet:peername(Socket); _ -> SockMod:peername(Socket) end, + IPHere = case PeerName of + {ok, V} -> V; + {error, _} = E -> throw(E) + end, XFF = proplists:get_value('X-Forwarded-For', RequestHeaders, []), IP = analyze_ip_xff(IPHere, XFF, Host), Request = #request{method = Method, @@ -446,27 +448,27 @@ process_request(#state{request_method = Method, opts = Options, headers = RequestHeaders, ip = IP}, - case process(RequestHandlers, Request, Socket, SockMod, Trail) of - El when is_record(El, xmlel) -> - make_xhtml_output(State, 200, [], El); - {Status, Headers, El} - when is_record(El, xmlel) -> - make_xhtml_output(State, Status, Headers, El); - Output when is_binary(Output) or is_list(Output) -> - make_text_output(State, 200, [], Output); - {Status, Headers, Output} - when is_binary(Output) or is_list(Output) -> - make_text_output(State, Status, Headers, Output); - {Status, Reason, Headers, Output} - when is_binary(Output) or is_list(Output) -> - make_text_output(State, Status, Reason, Headers, Output); - _ -> - none - end + Res = case process(RequestHandlers, Request, Socket, SockMod, Trail) of + El when is_record(El, xmlel) -> + make_xhtml_output(State, 200, [], El); + {Status, Headers, El} + when is_record(El, xmlel) -> + make_xhtml_output(State, Status, Headers, El); + Output when is_binary(Output) or is_list(Output) -> + make_text_output(State, 200, [], Output); + {Status, Headers, Output} + when is_binary(Output) or is_list(Output) -> + make_text_output(State, Status, Headers, Output); + {Status, Reason, Headers, Output} + when is_binary(Output) or is_list(Output) -> + make_text_output(State, Status, Reason, Headers, Output); + _ -> + none + end, + {State2, Res} end. make_bad_request(State) -> -%% Support for X-Forwarded-From make_xhtml_output(State, 400, [], ejabberd_web:make_xhtml([#xmlel{name = <<"h1">>, attrs = [], @@ -480,7 +482,8 @@ analyze_ip_xff({IPLast, Port}, XFF, Host) -> [jlib:ip_to_list(IPLast)], TrustedProxies = ejabberd_config:get_option( {trusted_proxies, Host}, - fun(TPs) -> + fun(all) -> all; + (TPs) -> [iolist_to_binary(TP) || TP <- TPs] end, []), IPClient = case is_ipchain_trusted(ProxiesIPs, @@ -503,32 +506,37 @@ is_ipchain_trusted(UserIPs, TrustedIPs) -> recv_data(State, Len) -> recv_data(State, Len, <<>>). -recv_data(_State, 0, Acc) -> (iolist_to_binary(Acc)); +recv_data(State, 0, Acc) -> {State, Acc}; +recv_data(#state{trail = Trail} = State, Len, <<>>) when byte_size(Trail) > Len -> + <<Data:Len/binary, Rest/binary>> = Trail, + {State#state{trail = Rest}, Data}; recv_data(State, Len, Acc) -> case State#state.trail of - <<>> -> - case (State#state.sockmod):recv(State#state.socket, Len, - 300000) - of - {ok, Data} -> - recv_data(State, Len - byte_size(Data), <<Acc/binary, Data/binary>>); - _ -> <<"">> - end; - _ -> - Trail = (State#state.trail), - recv_data(State#state{trail = <<>>}, - Len - byte_size(Trail), <<Acc/binary, Trail/binary>>) + <<>> -> + case (State#state.sockmod):recv(State#state.socket, + min(Len, 16#4000000), 300000) + of + {ok, Data} -> + recv_data(State, Len - byte_size(Data), <<Acc/binary, Data/binary>>); + Err -> + ?DEBUG("Cannot receive HTTP data: ~p", [Err]), + <<"">> + end; + _ -> + Trail = (State#state.trail), + recv_data(State#state{trail = <<>>}, + Len - byte_size(Trail), <<Acc/binary, Trail/binary>>) end. make_xhtml_output(State, Status, Headers, XHTML) -> Data = case lists:member(html, Headers) of - true -> - iolist_to_binary([?HTML_DOCTYPE, - xml:element_to_binary(XHTML)]); - _ -> - iolist_to_binary([?XHTML_DOCTYPE, - xml:element_to_binary(XHTML)]) - end, + true -> + iolist_to_binary([?HTML_DOCTYPE, + fxml:element_to_binary(XHTML)]); + _ -> + iolist_to_binary([?XHTML_DOCTYPE, + fxml:element_to_binary(XHTML)]) + end, Headers1 = case lists:keysearch(<<"Content-Type">>, 1, Headers) of @@ -756,6 +764,9 @@ parse_auth(<<"Basic ", Auth64/binary>>) -> {User, <<$:, Pass/binary>>} = erlang:split_binary(Auth, Pos-1), {User, Pass} end; +parse_auth(<<"Bearer ", SToken/binary>>) -> + Token = str:strip(SToken), + {oauth, Token, []}; parse_auth(<<_/binary>>) -> undefined. parse_urlencoded(S) -> @@ -870,3 +881,8 @@ transform_listen_option({request_handlers, Hs}, Opts) -> [{request_handlers, Hs1} | Opts]; transform_listen_option(Opt, Opts) -> [Opt|Opts]. + +opt_type(trusted_proxies) -> + fun (all) -> all; + (TPs) -> [iolist_to_binary(TP) || TP <- TPs] end; +opt_type(_) -> [trusted_proxies]. diff --git a/src/ejabberd_http_bind.erl b/src/ejabberd_http_bind.erl index 234ccf35..ea8cd792 100644 --- a/src/ejabberd_http_bind.erl +++ b/src/ejabberd_http_bind.erl @@ -1,19 +1,38 @@ %%%---------------------------------------------------------------------- %%% File : ejabberd_http_bind.erl %%% Author : Stefan Strigler <steve@zeank.in-berlin.de> -%%% Purpose : Implements XMPP over BOSH (XEP-0206) (formerly known as -%%% HTTP Binding) +%%% Purpose : Implements XMPP over BOSH (XEP-0206) %%% Created : 21 Sep 2005 by Stefan Strigler <steve@zeank.in-berlin.de> %%% Modified: may 2009 by Mickael Remond, Alexey Schepin -%%% Id : $Id: ejabberd_http_bind.erl 953 2009-05-07 10:40:40Z alexey $ +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2016 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_http_bind). +-protocol({xep, 124, '1.11'}). +-protocol({xep, 206, '1.4'}). + -behaviour(gen_fsm). %% External exports --export([start_link/3, +-export([start_link/4, init/1, handle_event/3, handle_sync_event/4, @@ -32,13 +51,13 @@ change_shaper/2, monitor/1, close/1, - start/4, + start/5, handle_session_start/8, handle_http_put/7, http_put/7, http_get/2, prepare_response/4, - process_request/2]). + process_request/3]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -75,7 +94,7 @@ wait_timer, ctime = 0, timer, - pause=0, + pause = 0, unprocessed_req_list = [], % list of request that have been delayed for proper reordering: {Request, PID} req_list = [], % list of requests (cache) max_inactivity, @@ -129,25 +148,21 @@ -define(PROCESS_DELAY_MAX, 1000). -%% Line copied from mod_http_bind.erl -define(PROCNAME_MHB, ejabberd_mod_http_bind). -%%%---------------------------------------------------------------------- -%%% API -%%%---------------------------------------------------------------------- -%% TODO: If compile with no supervisor option, start the session without -%% supervisor -start(XMPPDomain, Sid, Key, IP) -> +start(XMPPDomain, Sid, Key, IP, HOpts) -> ?DEBUG("Starting session", []), - SupervisorProc = gen_mod:get_module_proc(XMPPDomain, ?PROCNAME_MHB), - case catch supervisor:start_child(SupervisorProc, [Sid, Key, IP]) of - {ok, Pid} -> {ok, Pid}; + case catch gen_fsm:start(?MODULE, + [Sid, Key, IP, HOpts], + ?FSMOPTS) + of + {ok, Pid} -> {ok, Pid}; _ -> check_bind_module(XMPPDomain), {error, "Cannot start HTTP bind session"} end. -start_link(Sid, Key, IP) -> - gen_fsm:start_link(?MODULE, [Sid, Key, IP], ?FSMOPTS). +start_link(Sid, Key, IP, HOpts) -> + gen_fsm:start_link(?MODULE, [Sid, Key, IP, HOpts], ?FSMOPTS). send({http_bind, FsmRef, _IP}, Packet) -> gen_fsm:sync_send_all_state_event(FsmRef, @@ -194,7 +209,7 @@ peername({http_bind, _FsmRef, IP}) -> {ok, IP}. %% Entry point for data coming from client through ejabberd HTTP server: -process_request(Data, IP) -> +process_request(Data, IP, HOpts) -> Opts1 = ejabberd_c2s_config:get_c2s_limits(), Opts = [{xml_socket, true} | Opts1], MaxStanzaSize = case lists:keysearch(max_stanza_size, 1, @@ -209,7 +224,7 @@ process_request(Data, IP) -> of %% No existing session: {ok, {<<"">>, Rid, Attrs, Payload}} -> - case xml:get_attr_s(<<"to">>, Attrs) of + case fxml:get_attr_s(<<"to">>, Attrs) of <<"">> -> ?DEBUG("Session not created (Improper addressing)", []), {200, ?HEADER, @@ -217,8 +232,9 @@ process_request(Data, IP) -> "dressing' xmlns='", (?NS_HTTP_BIND)/binary, "'/>">>}; XmppDomain -> - Sid = p1_sha:sha(term_to_binary({now(), make_ref()})), - case start(XmppDomain, Sid, <<"">>, IP) of + NXmppDomain = jid:nameprep(XmppDomain), + Sid = p1_sha:sha(term_to_binary({p1_time_compat:monotonic_time(), make_ref()})), + case start(NXmppDomain, Sid, <<"">>, IP, HOpts) of {error, _} -> {500, ?HEADER, <<"<body type='terminate' condition='internal-se" @@ -226,19 +242,19 @@ process_request(Data, IP) -> (?NS_HTTP_BIND)/binary, "'>Internal Server Error</body>">>}; {ok, Pid} -> - handle_session_start(Pid, XmppDomain, Sid, Rid, Attrs, + handle_session_start(Pid, NXmppDomain, Sid, Rid, Attrs, Payload, PayloadSize, IP) end end; %% Existing session {ok, {Sid, Rid, Attrs, Payload1}} -> - StreamStart = case xml:get_attr_s(<<"xmpp:restart">>, + StreamStart = case fxml:get_attr_s(<<"xmpp:restart">>, Attrs) of <<"true">> -> true; _ -> false end, - Payload2 = case xml:get_attr_s(<<"type">>, Attrs) of + Payload2 = case fxml:get_attr_s(<<"type">>, Attrs) of <<"terminate">> -> Payload1 ++ [{xmlstreamend, <<"stream:stream">>}]; _ -> Payload1 @@ -264,7 +280,7 @@ process_request(Data, IP) -> handle_session_start(Pid, XmppDomain, Sid, Rid, Attrs, Payload, PayloadSize, IP) -> ?DEBUG("got pid: ~p", [Pid]), - Wait = case str:to_integer(xml:get_attr_s(<<"wait">>, + Wait = case str:to_integer(fxml:get_attr_s(<<"wait">>, Attrs)) of {error, _} -> ?MAX_WAIT; @@ -273,7 +289,7 @@ handle_session_start(Pid, XmppDomain, Sid, Rid, Attrs, true -> CWait end end, - Hold = case str:to_integer(xml:get_attr_s(<<"hold">>, + Hold = case str:to_integer(fxml:get_attr_s(<<"hold">>, Attrs)) of {error, _} -> (?MAX_REQUESTS) - 1; @@ -283,7 +299,7 @@ handle_session_start(Pid, XmppDomain, Sid, Rid, Attrs, end end, Pdelay = case - str:to_integer(xml:get_attr_s(<<"process-delay">>, + str:to_integer(fxml:get_attr_s(<<"process-delay">>, Attrs)) of {error, _} -> ?PROCESS_DELAY_DEFAULT; @@ -296,12 +312,12 @@ handle_session_start(Pid, XmppDomain, Sid, Rid, Attrs, ?PROCESS_DELAY_MIN]) end, Version = case catch - list_to_float(binary_to_list(xml:get_attr_s(<<"ver">>, Attrs))) + list_to_float(binary_to_list(fxml:get_attr_s(<<"ver">>, Attrs))) of {'EXIT', _} -> 0.0; V -> V end, - XmppVersion = xml:get_attr_s(<<"xmpp:version">>, Attrs), + XmppVersion = fxml:get_attr_s(<<"xmpp:version">>, Attrs), ?DEBUG("Create session: ~p", [Sid]), mnesia:dirty_write( #http_bind{id = Sid, @@ -319,17 +335,18 @@ handle_session_start(Pid, XmppDomain, Sid, Rid, Attrs, %%% Callback functions from gen_fsm %%%---------------------------------------------------------------------- -%%---------------------------------------------------------------------- -%% Func: init/1 -%% Returns: {ok, StateName, StateData} | -%% {ok, StateName, StateData, Timeout} | -%% ignore | -%% {stop, StopReason} -%%---------------------------------------------------------------------- -init([Sid, Key, IP]) -> +init([Sid, Key, IP, HOpts]) -> ?DEBUG("started: ~p", [{Sid, Key, IP}]), Opts1 = ejabberd_c2s_config:get_c2s_limits(), - Opts = [{xml_socket, true} | Opts1], + SOpts = lists:filtermap(fun({stream_managment, _}) -> true; + ({max_ack_queue, _}) -> true; + ({resume_timeout, _}) -> true; + ({max_resume_timeout, _}) -> true; + ({resend_on_timeout, _}) -> true; + (_) -> false + end, HOpts), + + Opts = [{xml_socket, true} | SOpts ++ Opts1], Shaper = none, ShaperState = shaper:new(Shaper), Socket = {http_bind, self(), IP}, @@ -343,12 +360,6 @@ init([Sid, Key, IP]) -> max_pause = ?MAX_PAUSE, timer = Timer}}. -%%---------------------------------------------------------------------- -%% Func: handle_event/3 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} -%%---------------------------------------------------------------------- handle_event({become_controller, C2SPid}, StateName, StateData) -> case StateData#state.input of cancel -> @@ -369,15 +380,6 @@ handle_event({change_shaper, Shaper}, StateName, handle_event(_Event, StateName, StateData) -> {next_state, StateName, StateData}. -%%---------------------------------------------------------------------- -%% Func: handle_sync_event/4 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {reply, Reply, NextStateName, NextStateData} | -%% {reply, Reply, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} | -%% {stop, Reason, Reply, NewStateData} -%%---------------------------------------------------------------------- handle_sync_event({send_xml, Packet}, _From, StateName, #state{http_receiver = undefined} = StateData) -> Output = [Packet | StateData#state.output], @@ -446,10 +448,8 @@ handle_sync_event(#http_put{payload_size = shaper_timer = NewShaperTimer}); %% HTTP GET: send packets to the client handle_sync_event({http_get, Rid, Wait, Hold}, From, StateName, StateData) -> - %% setup timer - TNow = tnow(), - if - (Hold > 0) and + TNow = p1_time_compat:system_time(micro_seconds), + if (Hold > 0) and ((StateData#state.output == []) or (StateData#state.rid < Rid)) and ((TNow - StateData#state.ctime) < (Wait*1000*1000)) and (StateData#state.rid =< Rid) and @@ -457,7 +457,6 @@ handle_sync_event({http_get, Rid, Wait, Hold}, From, StateName, StateData) -> send_receiver_reply(StateData#state.http_receiver, {ok, empty}), cancel_timer(StateData#state.wait_timer), WaitTimer = erlang:start_timer(Wait * 1000, self(), []), - %% MR: Not sure we should cancel the state timer here. cancel_timer(StateData#state.timer), {next_state, StateName, StateData#state{ http_receiver = From, @@ -467,34 +466,30 @@ handle_sync_event({http_get, Rid, Wait, Hold}, From, StateName, StateData) -> true -> cancel_timer(StateData#state.timer), Reply = {ok, StateData#state.output}, - %% save request ReqList = [#hbr{rid = Rid, key = StateData#state.key, - out = StateData#state.output - } | - [El || El <- StateData#state.req_list, - El#hbr.rid /= Rid ] - ], - if - (StateData#state.http_receiver /= undefined) and - StateData#state.out_of_order_receiver -> - {reply, Reply, StateName, StateData#state{ - output = [], - timer = undefined, - req_list = ReqList, - out_of_order_receiver = false}}; - true -> - send_receiver_reply(StateData#state.http_receiver, {ok, empty}), - cancel_timer(StateData#state.wait_timer), - Timer = set_inactivity_timer(StateData#state.pause, - StateData#state.max_inactivity), - {reply, Reply, StateName, - StateData#state{output = [], - http_receiver = undefined, - wait_timer = undefined, - timer = Timer, - req_list = ReqList}} - end + out = StateData#state.output} + | [El + || El <- StateData#state.req_list, + El#hbr.rid /= Rid]], + if (StateData#state.http_receiver /= undefined) and + StateData#state.out_of_order_receiver -> + {reply, Reply, StateName, + StateData#state{output = [], timer = undefined, + req_list = ReqList, + out_of_order_receiver = false}}; + true -> + send_receiver_reply(StateData#state.http_receiver, {ok, empty}), + cancel_timer(StateData#state.wait_timer), + Timer = set_inactivity_timer(StateData#state.pause, + StateData#state.max_inactivity), + {reply, Reply, StateName, + StateData#state{output = [], + http_receiver = undefined, + wait_timer = undefined, + timer = Timer, + req_list = ReqList}} + end end; handle_sync_event(peername, _From, StateName, StateData) -> @@ -507,13 +502,6 @@ handle_sync_event(_Event, _From, StateName, code_change(_OldVsn, StateName, StateData, _Extra) -> {ok, StateName, StateData}. -%%---------------------------------------------------------------------- -%% Func: handle_info/3 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} -%%---------------------------------------------------------------------- -%% We reached the max_inactivity timeout: handle_info({timeout, Timer, _}, _StateName, #state{id = SID, timer = Timer} = StateData) -> ?INFO_MSG("Session timeout. Closing the HTTP bind " @@ -546,11 +534,6 @@ handle_info({timeout, ShaperTimer, _}, StateName, handle_info(_, StateName, StateData) -> {next_state, StateName, StateData}. -%%---------------------------------------------------------------------- -%% Func: terminate/3 -%% Purpose: Shutdown the fsm -%% Returns: any -%%---------------------------------------------------------------------- terminate(_Reason, _StateName, StateData) -> ?DEBUG("terminate: Deleting session ~s", [StateData#state.id]), @@ -606,8 +589,8 @@ process_http_put(#http_put{rid = Rid, attrs = Attrs, Request, StateName, StateData, RidAllow) -> ?DEBUG("Actually processing request: ~p", [Request]), - Key = xml:get_attr_s(<<"key">>, Attrs), - NewKey = xml:get_attr_s(<<"newkey">>, Attrs), + Key = fxml:get_attr_s(<<"key">>, Attrs), + NewKey = fxml:get_attr_s(<<"newkey">>, Attrs), KeyAllow = case RidAllow of repeat -> true; false -> false; @@ -623,7 +606,7 @@ process_http_put(#http_put{rid = Rid, attrs = Attrs, end end end, - TNow = tnow(), + TNow = p1_time_compat:system_time(micro_seconds), LastPoll = if Payload == [] -> TNow; true -> 0 end, @@ -792,19 +775,22 @@ http_put(Sid, Rid, Attrs, Payload, PayloadSize, ?DEBUG("Looking for session: ~p", [Sid]), case mnesia:dirty_read({http_bind, Sid}) of [] -> - {error, not_exists}; - [#http_bind{pid = FsmRef, hold=Hold, to={To, StreamVersion}}=Sess] -> - NewStream = - case StreamStart of - true -> - {To, StreamVersion}; - _ -> - <<"">> - end, - {gen_fsm:sync_send_all_state_event( - FsmRef, #http_put{rid = Rid, attrs = Attrs, payload = Payload, - payload_size = PayloadSize, hold = Hold, - stream = NewStream, ip = IP}, 30000), Sess} + {error, not_exists}; + [#http_bind{pid = FsmRef, hold=Hold, + to= {To, StreamVersion}} = Sess] -> + NewStream = case StreamStart of + true -> {To, StreamVersion}; + _ -> <<"">> + end, + {gen_fsm:sync_send_all_state_event( + FsmRef, #http_put{rid = Rid, + attrs = Attrs, + payload = Payload, + payload_size = PayloadSize, + hold = Hold, + stream = NewStream, + ip = IP}, + 30000), Sess} end. handle_http_put_error(Reason, @@ -815,7 +801,7 @@ handle_http_put_error(Reason, case Reason of not_exists -> {200, ?HEADER, - xml:element_to_binary(#xmlel{name = <<"body">>, + fxml:element_to_binary(#xmlel{name = <<"body">>, attrs = [{<<"xmlns">>, ?NS_HTTP_BIND}, {<<"type">>, <<"terminate">>}, @@ -824,7 +810,7 @@ handle_http_put_error(Reason, children = []})}; bad_key -> {200, ?HEADER, - xml:element_to_binary(#xmlel{name = <<"body">>, + fxml:element_to_binary(#xmlel{name = <<"body">>, attrs = [{<<"xmlns">>, ?NS_HTTP_BIND}, {<<"type">>, <<"terminate">>}, @@ -833,7 +819,7 @@ handle_http_put_error(Reason, children = []})}; polling_too_frequently -> {200, ?HEADER, - xml:element_to_binary(#xmlel{name = <<"body">>, + fxml:element_to_binary(#xmlel{name = <<"body">>, attrs = [{<<"xmlns">>, ?NS_HTTP_BIND}, {<<"type">>, <<"terminate">>}, @@ -869,7 +855,7 @@ rid_allow(OldRid, NewRid, Attrs, Hold, MaxPause) -> %% We did not miss any packet, we can process it immediately: NewRid == OldRid + 1 -> case catch - jlib:binary_to_integer(xml:get_attr_s(<<"pause">>, + jlib:binary_to_integer(fxml:get_attr_s(<<"pause">>, Attrs)) of {'EXIT', _} -> {true, 0}; @@ -943,9 +929,9 @@ prepare_outpacket_response(#http_bind{id = Sid, _Rid, OutPacket, true) -> case OutPacket of [{xmlstreamstart, _, OutAttrs} | Els] -> - AuthID = xml:get_attr_s(<<"id">>, OutAttrs), - From = xml:get_attr_s(<<"from">>, OutAttrs), - Version = xml:get_attr_s(<<"version">>, OutAttrs), + AuthID = fxml:get_attr_s(<<"id">>, OutAttrs), + From = fxml:get_attr_s(<<"from">>, OutAttrs), + Version = fxml:get_attr_s(<<"version">>, OutAttrs), OutEls = case Els of [] -> []; [{xmlstreamelement, @@ -982,7 +968,7 @@ prepare_outpacket_response(#http_bind{id = Sid, MaxInactivity = get_max_inactivity(To, ?MAX_INACTIVITY), MaxPause = get_max_pause(To), {200, ?HEADER, - xml:element_to_binary(#xmlel{name = <<"body">>, + fxml:element_to_binary(#xmlel{name = <<"body">>, attrs = [{<<"xmlns">>, ?NS_HTTP_BIND}, {<<"sid">>, Sid}, @@ -1046,7 +1032,7 @@ send_outpacket(#http_bind{pid = FsmRef}, OutPacket) -> TypedEls = lists:foldl(fun ({xmlstreamelement, El}, Acc) -> Acc ++ - [xml:element_to_binary(check_default_xmlns(El))]; + [fxml:element_to_binary(check_default_xmlns(El))]; ({xmlstreamraw, R}, Acc) -> Acc ++ [R] end, @@ -1081,7 +1067,7 @@ send_outpacket(#http_bind{pid = FsmRef}, OutPacket) -> || {xmlstreamelement, OEl} <- StreamTail] end, {200, ?HEADER, - xml:element_to_binary(#xmlel{name = <<"body">>, + fxml:element_to_binary(#xmlel{name = <<"body">>, attrs = [{<<"xmlns">>, ?NS_HTTP_BIND}], @@ -1128,14 +1114,14 @@ parse_request(Data, PayloadSize, MaxStanzaSize) -> ?DEBUG("--- incoming data --- ~n~s~n --- END " "--- ", [Data]), - case xml_stream:parse_element(Data) of + case fxml_stream:parse_element(Data) of #xmlel{name = <<"body">>, attrs = Attrs, children = Els} -> - Xmlns = xml:get_attr_s(<<"xmlns">>, Attrs), + Xmlns = fxml:get_attr_s(<<"xmlns">>, Attrs), if Xmlns /= (?NS_HTTP_BIND) -> {error, bad_request}; true -> case catch - jlib:binary_to_integer(xml:get_attr_s(<<"rid">>, + jlib:binary_to_integer(fxml:get_attr_s(<<"rid">>, Attrs)) of {'EXIT', _} -> {error, bad_request}; @@ -1147,7 +1133,7 @@ parse_request(Data, PayloadSize, MaxStanzaSize) -> end end, Els), - Sid = xml:get_attr_s(<<"sid">>, Attrs), + Sid = fxml:get_attr_s(<<"sid">>, Attrs), if PayloadSize =< MaxStanzaSize -> {ok, {Sid, Rid, Attrs, FixedEls}}; true -> {size_limit, Sid} @@ -1177,10 +1163,9 @@ set_inactivity_timer(Pause, _MaxInactivity) set_inactivity_timer(_Pause, MaxInactivity) -> erlang:start_timer(MaxInactivity, self(), []). -%% TODO: Use tail recursion and list reverse ? elements_to_string([]) -> []; elements_to_string([El | Els]) -> - [xml:element_to_binary(El) | elements_to_string(Els)]. + [fxml:element_to_binary(El) | elements_to_string(Els)]. %% @spec (To, Default::integer()) -> integer() %% where To = [] | {Host::string(), Version::string()} @@ -1200,15 +1185,10 @@ get_max_pause({Host, _}) -> ?MAX_PAUSE); get_max_pause(_) -> ?MAX_PAUSE. -%% Current time as integer -tnow() -> - {TMegSec, TSec, TMSec} = now(), - (TMegSec * 1000000 + TSec) * 1000000 + TMSec. - check_default_xmlns(#xmlel{name = Name, attrs = Attrs, children = Els} = El) -> - case xml:get_tag_attr_s(<<"xmlns">>, El) of + case fxml:get_tag_attr_s(<<"xmlns">>, El) of <<"">> -> #xmlel{name = Name, attrs = [{<<"xmlns">>, ?NS_CLIENT} | Attrs], diff --git a/src/ejabberd_http_ws.erl b/src/ejabberd_http_ws.erl index a0cc31e2..e66cf33a 100644 --- a/src/ejabberd_http_ws.erl +++ b/src/ejabberd_http_ws.erl @@ -5,7 +5,7 @@ %%% Created : 09-10-2010 by Eric Cestari <ecestari@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -24,16 +24,17 @@ %%%---------------------------------------------------------------------- -module(ejabberd_http_ws). +-behaviour(ejabberd_config). + -author('ecestari@process-one.net'). -behaviour(gen_fsm). -% External exports -export([start/1, start_link/1, init/1, handle_event/3, handle_sync_event/4, code_change/4, handle_info/3, - terminate/3, send_xml/2, setopts/2, sockname/1, peername/1, - controlling_process/2, become_controller/2, close/1, - socket_handoff/6]). + terminate/3, send_xml/2, setopts/2, sockname/1, + peername/1, controlling_process/2, become_controller/2, + close/1, socket_handoff/6, opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -74,7 +75,7 @@ -export_type([ws_socket/0]). start(WS) -> - supervisor:start_child(ejabberd_wsloop_sup, [WS]). + gen_fsm:start(?MODULE, [WS], ?FSMOPTS). start_link(WS) -> gen_fsm:start_link(?MODULE, [WS], ?FSMOPTS). @@ -110,8 +111,15 @@ socket_handoff(LocalPath, Request, Socket, SockMod, Buf, Opts) -> %%% Internal -init([{#ws{ip = IP}, _} = WS]) -> - Opts = [{xml_socket, true} | ejabberd_c2s_config:get_c2s_limits()], +init([{#ws{ip = IP, http_opts = HOpts}, _} = WS]) -> + SOpts = lists:filtermap(fun({stream_managment, _}) -> true; + ({max_ack_queue, _}) -> true; + ({resume_timeout, _}) -> true; + ({max_resume_timeout, _}) -> true; + ({resend_on_timeout, _}) -> true; + (_) -> false + end, HOpts), + Opts = [{xml_socket, true} | ejabberd_c2s_config:get_c2s_limits() ++ SOpts], PingInterval = ejabberd_config:get_option( {websocket_ping_interval, ?MYNAME}, fun(I) when is_integer(I), I>=0 -> I end, @@ -163,11 +171,11 @@ handle_sync_event({send_xml, Packet}, _From, StateName, {true, {xmlstreamelement, #xmlel{name=Name2} = El2}} -> El3 = case Name2 of <<"stream:", _/binary>> -> - xml:replace_tag_attr(<<"xmlns:stream">>, ?NS_STREAM, El2); + fxml:replace_tag_attr(<<"xmlns:stream">>, ?NS_STREAM, El2); _ -> - case xml:get_tag_attr_s(<<"xmlns">>, El2) of + case fxml:get_tag_attr_s(<<"xmlns">>, El2) of <<"">> -> - xml:replace_tag_attr(<<"xmlns">>, <<"jabber:client">>, El2); + fxml:replace_tag_attr(<<"xmlns">>, <<"jabber:client">>, El2); _ -> El2 end @@ -178,12 +186,12 @@ handle_sync_event({send_xml, Packet}, _From, StateName, end, case Packet2 of {xmlstreamstart, Name, Attrs3} -> - B = xml:element_to_binary(#xmlel{name = Name, attrs = Attrs3}), + B = fxml:element_to_binary(#xmlel{name = Name, attrs = Attrs3}), WsPid ! {send, <<(binary:part(B, 0, byte_size(B)-2))/binary, ">">>}; {xmlstreamend, Name} -> WsPid ! {send, <<"</", Name/binary, ">">>}; {xmlstreamelement, El} -> - WsPid ! {send, xml:element_to_binary(El)}; + WsPid ! {send, fxml:element_to_binary(El)}; {xmlstreamraw, Bin} -> WsPid ! {send, Bin}; {xmlstreamcdata, Bin2} -> @@ -202,7 +210,7 @@ handle_sync_event(close, _From, StateName, #state{ws = {_, WsPid}, rfc_compilant when StateName /= stream_end_sent -> Close = #xmlel{name = <<"close">>, attrs = [{<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-framing">>}]}, - WsPid ! {send, xml:element_to_binary(Close)}, + WsPid ! {send, fxml:element_to_binary(Close)}, {stop, normal, StateData}; handle_sync_event(close, _From, _StateName, StateData) -> {stop, normal, StateData}. @@ -262,10 +270,9 @@ setup_timers(StateData) -> Timer = erlang:start_timer(StateData#state.timeout, self(), []), cancel_timer(StateData#state.ping_timer), - PingTimer = case {StateData#state.ping_interval, StateData#state.rfc_compilant} of - {0, _} -> StateData#state.ping_timer; - {_, false} -> StateData#state.ping_timer; - {V, _} -> erlang:start_timer(V, self(), []) + PingTimer = case StateData#state.ping_interval of + 0 -> StateData#state.ping_timer; + V -> erlang:start_timer(V, self(), []) end, StateData#state{timer = Timer, ping_timer = PingTimer, pong_expected = false}. @@ -309,9 +316,9 @@ get_human_html_xmlel() -> parse(#state{rfc_compilant = C} = State, Data) -> case C of undefined -> - P = xml_stream:new(self()), - P2 = xml_stream:parse(P, Data), - xml_stream:close(P2), + P = fxml_stream:new(self()), + P2 = fxml_stream:parse(P, Data), + fxml_stream:close(P2), case parsed_items([]) of error -> {State#state{rfc_compilant = true}, <<"parse error">>}; @@ -323,7 +330,7 @@ parse(#state{rfc_compilant = C} = State, Data) -> parse(State#state{rfc_compilant = false}, Data) end; true -> - El = xml_stream:parse_element(Data), + El = fxml_stream:parse_element(Data), case El of #xmlel{name = <<"open">>, attrs = Attrs} -> Attrs2 = [{<<"xmlns:stream">>, ?NS_STREAM}, {<<"xmlns">>, <<"jabber:client">>} | @@ -353,3 +360,10 @@ parsed_items(List) -> after 0 -> lists:reverse(List) end. + +opt_type(websocket_ping_interval) -> + fun (I) when is_integer(I), I >= 0 -> I end; +opt_type(websocket_timeout) -> + fun (I) when is_integer(I), I > 0 -> I end; +opt_type(_) -> + [websocket_ping_interval, websocket_timeout]. diff --git a/src/idna.erl b/src/ejabberd_idna.erl index dd19ad98..f889b411 100644 --- a/src/idna.erl +++ b/src/ejabberd_idna.erl @@ -1,11 +1,11 @@ %%%---------------------------------------------------------------------- -%%% File : idna.erl +%%% File : ejabberd_idna.erl %%% Author : Alexey Shchepin <alexey@process-one.net> %%% Purpose : Support for IDNA (RFC3490) %%% Created : 10 Apr 2004 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -23,7 +23,7 @@ %%% %%%---------------------------------------------------------------------- --module(idna). +-module(ejabberd_idna). -author('alexey@process-one.net'). diff --git a/src/ejabberd_listener.erl b/src/ejabberd_listener.erl index 0cfca0aa..a9cc441e 100644 --- a/src/ejabberd_listener.erl +++ b/src/ejabberd_listener.erl @@ -5,7 +5,7 @@ %%% Created : 16 Nov 2002 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -24,20 +24,15 @@ %%%---------------------------------------------------------------------- -module(ejabberd_listener). + +-behaviour(ejabberd_config). -author('alexey@process-one.net'). --export([start_link/0, init/1, start/3, - init/3, - start_listeners/0, - start_listener/3, - stop_listeners/0, - stop_listener/2, - parse_listener_portip/2, - add_listener/3, - delete_listener/2, - transform_options/1, - validate_cfg/1 - ]). +-export([start_link/0, init/1, start/3, init/3, + start_listeners/0, start_listener/3, stop_listeners/0, + stop_listener/2, parse_listener_portip/2, + add_listener/3, delete_listener/2, transform_options/1, + validate_cfg/1, opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -191,8 +186,8 @@ init_tcp(PortIP, Module, Opts, SockOpts, Port, IPS) -> listen_tcp(PortIP, Module, SockOpts, Port, IPS) -> case ets:lookup(listen_sockets, PortIP) of [{PortIP, ListenSocket}] -> - ?INFO_MSG("Reusing listening port for ~p", [Port]), - ets:delete(listen_sockets, Port), + ?INFO_MSG("Reusing listening port for ~p", [PortIP]), + ets:delete(listen_sockets, PortIP), ListenSocket; _ -> Res = gen_tcp:listen(Port, [binary, @@ -230,8 +225,8 @@ listen_tcp(PortIP, Module, SockOpts, Port, IPS) -> %% so the option inet/inet6 is only used when no IP is specified at all. parse_listener_portip(PortIP, Opts) -> {IPOpt, Opts2} = strip_ip_option(Opts), - {IPVOpt, OptsClean} = case lists:member(inet6, Opts2) of - true -> {inet6, Opts2 -- [inet6]}; + {IPVOpt, OptsClean} = case proplists:get_bool(inet6, Opts2) of + true -> {inet6, proplists:delete(inet6, Opts2)}; false -> {inet, Opts2} end, {Port, IPT, IPS, Proto} = @@ -297,12 +292,46 @@ get_ip_tuple(IPOpt, _IPVOpt) -> IPOpt. accept(ListenSocket, Module, Opts) -> + IntervalOpt = + case proplists:get_value(accept_interval, Opts) of + [{linear, [I1_, T1_, T2_, I2_]}] -> + {linear, I1_, T1_, T2_, I2_}; + I_ -> I_ + end, + Interval = + case IntervalOpt of + undefined -> + 0; + I when is_integer(I), I >= 0 -> + I; + {linear, I1, T1, T2, I2} + when is_integer(I1), + is_integer(T1), + is_integer(T2), + is_integer(I2), + I1 >= 0, + I2 >= 0, + T2 > 0 -> + {MSec, Sec, _USec} = os:timestamp(), + TS = MSec * 1000000 + Sec, + {linear, I1, TS + T1, T2, I2}; + I -> + ?WARNING_MSG("There is a problem in the configuration: " + "~p is a wrong accept_interval value. " + "Using 0 as fallback", + [I]), + 0 + end, + accept(ListenSocket, Module, Opts, Interval). + +accept(ListenSocket, Module, Opts, Interval) -> + NewInterval = check_rate_limit(Interval), case gen_tcp:accept(ListenSocket) of {ok, Socket} -> case {inet:sockname(Socket), inet:peername(Socket)} of {{ok, {Addr, Port}}, {ok, {PAddr, PPort}}} -> ?INFO_MSG("(~w) Accepted connection ~s:~p -> ~s:~p", - [Socket, inet_parse:ntoa(PAddr), PPort, + [Socket, ejabberd_config:may_hide_data(inet_parse:ntoa(PAddr)), PPort, inet_parse:ntoa(Addr), Port]); _ -> ok @@ -312,11 +341,11 @@ accept(ListenSocket, Module, Opts) -> false -> ejabberd_socket end, CallMod:start(strip_frontend(Module), gen_tcp, Socket, Opts), - accept(ListenSocket, Module, Opts); + accept(ListenSocket, Module, Opts, NewInterval); {error, Reason} -> ?ERROR_MSG("(~w) Failed TCP accept: ~w", [ListenSocket, Reason]), - accept(ListenSocket, Module, Opts) + accept(ListenSocket, Module, Opts, NewInterval) end. udp_recv(Socket, Module, Opts) -> @@ -560,6 +589,31 @@ format_error(Reason) -> ReasonStr end. +check_rate_limit(Interval) -> + NewInterval = receive + {rate_limit, AcceptInterval} -> + AcceptInterval + after 0 -> + Interval + end, + case NewInterval of + 0 -> ok; + Ms when is_integer(Ms) -> + timer:sleep(Ms); + {linear, I1, T1, T2, I2} -> + {MSec, Sec, _USec} = os:timestamp(), + TS = MSec * 1000000 + Sec, + I = + if + TS =< T1 -> I1; + TS >= T1 + T2 -> I2; + true -> + round((I2 - I1) * (TS - T1) / T2 + I1) + end, + timer:sleep(I) + end, + NewInterval. + -define(IS_CHAR(C), (is_integer(C) and (C >= 0) and (C =< 255))). -define(IS_UINT(U), (is_integer(U) and (U >= 0) and (U =< 65535))). -define(IS_PORT(P), (is_integer(P) and (P > 0) and (P =< 65535))). @@ -574,11 +628,8 @@ transform_option({{Port, IP, Transport}, Mod, Opts}) -> Opts1 = lists:map( fun({ip, IPT}) when is_tuple(IPT) -> {ip, list_to_binary(inet_parse:ntoa(IP))}; - (tls) -> {tls, true}; (ssl) -> {tls, true}; - (zlib) -> {zlib, true}; - (starttls) -> {starttls, true}; - (starttls_required) -> {starttls_required, true}; + (A) when is_atom(A) -> {A, true}; (Opt) -> Opt end, Opts), Opts2 = lists:foldl( @@ -598,11 +649,11 @@ transform_option({{Port, IP, Transport}, Mod, Opts}) -> IPOpt ++ TransportOpt ++ [{port, Port}, {module, Mod} | Opts2]; transform_option({{Port, Transport}, Mod, Opts}) when ?IS_TRANSPORT(Transport) -> - transform_option({{Port, {0,0,0,0}, Transport}, Mod, Opts}); + transform_option({{Port, all_zero_ip(Opts), Transport}, Mod, Opts}); transform_option({{Port, IP}, Mod, Opts}) -> transform_option({{Port, IP, tcp}, Mod, Opts}); transform_option({Port, Mod, Opts}) -> - transform_option({{Port, {0,0,0,0}, tcp}, Mod, Opts}); + transform_option({{Port, all_zero_ip(Opts), tcp}, Mod, Opts}); transform_option(Opt) -> Opt. @@ -638,7 +689,7 @@ validate_cfg(L) -> {Port, prepare_mod(Mod), Opts}; (Opt, {Port, Mod, Opts}) -> {Port, Mod, [Opt|Opts]} - end, {{5222, {0,0,0,0}, tcp}, ejabberd_c2s, []}, LOpts) + end, {{5222, all_zero_ip(LOpts), tcp}, ejabberd_c2s, []}, LOpts) end, L). prepare_ip({A, B, C, D} = IP) @@ -660,3 +711,12 @@ prepare_mod(sip) -> esip_socket; prepare_mod(Mod) when is_atom(Mod) -> Mod. + +all_zero_ip(Opts) -> + case proplists:get_bool(inet6, Opts) of + true -> {0,0,0,0,0,0,0,0}; + false -> {0,0,0,0} + end. + +opt_type(listen) -> fun validate_cfg/1; +opt_type(_) -> [listen]. diff --git a/src/ejabberd_local.erl b/src/ejabberd_local.erl index cec39ced..7c30f3b6 100644 --- a/src/ejabberd_local.erl +++ b/src/ejabberd_local.erl @@ -5,7 +5,7 @@ %%% Created : 30 Nov 2002 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -32,7 +32,7 @@ %% API -export([start_link/0]). --export([route/3, route_iq/4, route_iq/5, +-export([route/3, route_iq/4, route_iq/5, process_iq/3, process_iq_reply/3, register_iq_handler/4, register_iq_handler/5, register_iq_response_handler/4, register_iq_response_handler/5, unregister_iq_handler/2, @@ -175,16 +175,10 @@ bounce_resource_packet(From, To, Packet) -> %% gen_server callbacks %%==================================================================== -%%-------------------------------------------------------------------- -%% Function: init(Args) -> {ok, State} | -%% {ok, State, Timeout} | -%% ignore | -%% {stop, Reason} -%% Description: Initiates the server -%%-------------------------------------------------------------------- init([]) -> lists:foreach(fun (Host) -> ejabberd_router:register_route(Host, + Host, {apply, ?MODULE, route}), ejabberd_hooks:add(local_send_to_resource_hook, Host, @@ -200,28 +194,7 @@ init([]) -> mnesia:add_table_copy(iq_response, node(), ram_copies), {ok, #state{}}. -%%-------------------------------------------------------------------- -%% 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(_Request, _From, State) -> -%%-------------------------------------------------------------------- -%% Function: handle_cast(Msg, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling cast messages -%%-------------------------------------------------------------------- -%%-------------------------------------------------------------------- -%% Function: handle_info(Info, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling all non call/cast messages -%%-------------------------------------------------------------------- Reply = ok, {reply, Reply, State}. handle_cast(_Msg, State) -> {noreply, State}. @@ -272,26 +245,18 @@ handle_info(refresh_iq_handlers, State) -> handle_info({timeout, _TRef, ID}, State) -> process_iq_timeout(ID), {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. -%%-------------------------------------------------------------------- -%%-------------------------------------------------------------------- -%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} -%% Description: Convert process state when code is changed -%%-------------------------------------------------------------------- -%%-------------------------------------------------------------------- -%%% Internal functions -%%-------------------------------------------------------------------- -handle_info(_Info, State) -> {noreply, State}. +handle_info(_Info, State) -> + {noreply, State}. -terminate(_Reason, _State) -> ok. +terminate(_Reason, _State) -> + ok. -code_change(_OldVsn, State, _Extra) -> {ok, State}. +code_change(_OldVsn, State, _Extra) -> + {ok, State}. +%%-------------------------------------------------------------------- +%%% Internal functions +%%-------------------------------------------------------------------- do_route(From, To, Packet) -> ?DEBUG("local route~n\tfrom ~p~n\tto ~p~n\tpacket " "~P~n", @@ -308,7 +273,7 @@ do_route(From, To, Packet) -> end; true -> #xmlel{attrs = Attrs} = Packet, - case xml:get_attr_s(<<"type">>, Attrs) of + case fxml:get_attr_s(<<"type">>, Attrs) of <<"error">> -> ok; <<"result">> -> ok; _ -> diff --git a/src/ejabberd_logger.erl b/src/ejabberd_logger.erl index a00ac994..795d4f39 100644 --- a/src/ejabberd_logger.erl +++ b/src/ejabberd_logger.erl @@ -1,12 +1,11 @@ %%%------------------------------------------------------------------- %%% @author Evgeniy Khramtsov <ekhramtsov@process-one.net> -%%% @copyright (C) 2013, Evgeniy Khramtsov %%% @doc %%% %%% @end %%% Created : 12 May 2013 by Evgeniy Khramtsov <ekhramtsov@process-one.net> %%% -%%% ejabberd, Copyright (C) 2013-2015 ProcessOne +%%% ejabberd, Copyright (C) 2013-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,8 +24,10 @@ %%%------------------------------------------------------------------- -module(ejabberd_logger). +-behaviour(ejabberd_config). + %% API --export([start/0, reopen_log/0, get/0, set/1, get_log_path/0]). +-export([start/0, reopen_log/0, rotate_log/0, get/0, set/1, get_log_path/0, opt_type/1]). -include("ejabberd.hrl"). @@ -35,6 +36,7 @@ -spec start() -> ok. -spec get_log_path() -> string(). -spec reopen_log() -> ok. +-spec rotate_log() -> ok. -spec get() -> {loglevel(), atom(), string()}. -spec set(loglevel() | {loglevel(), list()}) -> {module, module()}. @@ -48,6 +50,7 @@ %% "ejabberd.log" in current directory. %% Note: If the directory where to place the ejabberd log file to not exist, %% it is not created and no log file will be generated. +%% @spec () -> string() get_log_path() -> case ejabberd_config:env_binary_to_list(ejabberd, log_path) of {ok, Path} -> @@ -61,7 +64,16 @@ get_log_path() -> end end. --ifdef(LAGER). +opt_type(log_rotate_date) -> + fun(S) -> binary_to_list(iolist_to_binary(S)) end; +opt_type(log_rotate_size) -> + fun(I) when is_integer(I), I >= 0 -> I end; +opt_type(log_rotate_count) -> + fun(I) when is_integer(I), I >= 0 -> I end; +opt_type(log_rate_limit) -> + fun(I) when is_integer(I), I >= 0 -> I end; +opt_type(_) -> + [log_rotate_date, log_rotate_size, log_rotate_count, log_rate_limit]. get_integer_env(Name, Default) -> case application:get_env(ejabberd, Name) of @@ -88,7 +100,33 @@ get_string_env(Name, Default) -> Default end. +%% @spec () -> ok start() -> + StartedApps = application:which_applications(5000), + case lists:keyfind(logger, 1, StartedApps) of + %% Elixir logger is started. We assume everything is in place + %% to use lager to Elixir logger bridge. + {logger, _, _} -> + error_logger:info_msg("Ignoring ejabberd logger options, using Elixir Logger.", []), + %% Do not start lager, we rely on Elixir Logger + do_start_for_logger(); + _ -> + do_start() + end. + +do_start_for_logger() -> + application:load(sasl), + application:set_env(sasl, sasl_error_logger, false), + application:load(lager), + application:set_env(lager, error_logger_redirect, false), + application:set_env(lager, error_logger_whitelist, ['Elixir.Logger.ErrorHandler']), + application:set_env(lager, crash_log, false), + application:set_env(lager, handlers, [{elixir_logger_backend, [{level, info}]}]), + ejabberd:start_app(lager), + ok. + +%% Start lager +do_start() -> application:load(sasl), application:set_env(sasl, sasl_error_logger, false), application:load(lager), @@ -115,7 +153,13 @@ start() -> ejabberd:start_app(lager), ok. +%% @spec () -> ok reopen_log() -> + %% Lager detects external log rotation automatically. + ok. + +%% @spec () -> ok +rotate_log() -> lager_crash_log ! rotate, lists:foreach( fun({lager_file_backend, File}) -> @@ -124,8 +168,9 @@ reopen_log() -> ok end, gen_event:which_handlers(lager_event)). +%% @spec () -> {loglevel(), atom(), string()} get() -> - case lager:get_loglevel(lager_console_backend) of + case get_lager_loglevel() of none -> {0, no_log, "No log"}; emergency -> {1, critical, "Critical"}; alert -> {1, critical, "Critical"}; @@ -137,6 +182,7 @@ get() -> debug -> {5, debug, "Debug"} end. +%% @spec (loglevel() | {loglevel(), list()}) -> {module, module()} set(LogLevel) when is_integer(LogLevel) -> LagerLogLevel = case LogLevel of 0 -> none; @@ -144,9 +190,10 @@ set(LogLevel) when is_integer(LogLevel) -> 2 -> error; 3 -> warning; 4 -> info; - 5 -> debug + 5 -> debug; + E -> throw({wrong_loglevel, E}) end, - case lager:get_loglevel(lager_console_backend) of + case get_lager_loglevel() of LagerLogLevel -> ok; _ -> @@ -156,6 +203,8 @@ set(LogLevel) when is_integer(LogLevel) -> lager:set_loglevel(H, LagerLogLevel); (lager_console_backend = H) -> lager:set_loglevel(H, LagerLogLevel); + (elixir_logger_backend = H) -> + lager:set_loglevel(H, LagerLogLevel); (_) -> ok end, gen_event:which_handlers(lager_event)) @@ -165,56 +214,21 @@ set({_LogLevel, _}) -> error_logger:error_msg("custom loglevels are not supported for 'lager'"), {module, lager}. --else. - -start() -> - set(4), - LogPath = get_log_path(), - error_logger:add_report_handler(p1_logger_h, LogPath), - ok. - -reopen_log() -> - %% TODO: Use the Reopen log API for logger_h ? - p1_logger_h:reopen_log(), - reopen_sasl_log(). - -get() -> - p1_loglevel:get(). - -set(LogLevel) -> - p1_loglevel:set(LogLevel). - -%%%=================================================================== -%%% Internal functions -%%%=================================================================== -reopen_sasl_log() -> - case application:get_env(sasl,sasl_error_logger) of - {ok, {file, SASLfile}} -> - error_logger:delete_report_handler(sasl_report_file_h), - rotate_sasl_log(SASLfile), - error_logger:add_report_handler(sasl_report_file_h, - {SASLfile, get_sasl_error_logger_type()}); - _ -> false - end, - ok. - -rotate_sasl_log(Filename) -> - case file:read_file_info(Filename) of - {ok, _FileInfo} -> - file:rename(Filename, [Filename, ".0"]), - ok; - {error, _Reason} -> - ok +get_lager_loglevel() -> + Handlers = get_lager_handlers(), + lists:foldl(fun(lager_console_backend, _Acc) -> + lager:get_loglevel(lager_console_backend); + (elixir_logger_backend, _Acc) -> + lager:get_loglevel(elixir_logger_backend); + (_, Acc) -> + Acc + end, + none, Handlers). + +get_lager_handlers() -> + case catch gen_event:which_handlers(lager_event) of + {'EXIT',noproc} -> + []; + Result -> + Result end. - -%% Function copied from Erlang/OTP lib/sasl/src/sasl.erl which doesn't export it -get_sasl_error_logger_type () -> - case application:get_env (sasl, errlog_type) of - {ok, error} -> error; - {ok, progress} -> progress; - {ok, all} -> all; - {ok, Bad} -> exit ({bad_config, {sasl, {errlog_type, Bad}}}); - _ -> all - end. - --endif. diff --git a/src/ejabberd_node_groups.erl b/src/ejabberd_node_groups.erl index da0bffe9..352757dd 100644 --- a/src/ejabberd_node_groups.erl +++ b/src/ejabberd_node_groups.erl @@ -5,7 +5,7 @@ %%% Created : 1 Nov 2006 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -24,6 +24,8 @@ %%%---------------------------------------------------------------------- -module(ejabberd_node_groups). + +-behaviour(ejabberd_config). -author('alexey@process-one.net'). -behaviour(gen_server). @@ -35,9 +37,8 @@ get_members/1, get_closest_node/1]). -%% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +-export([init/1, handle_call/3, handle_cast/2, + handle_info/2, terminate/2, code_change/3, opt_type/1]). -define(PG2, pg2). @@ -163,3 +164,10 @@ code_change(_OldVsn, State, _Extra) -> %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- + +opt_type(node_type) -> + fun (frontend) -> frontend; + (backend) -> backend; + (generic) -> generic + end; +opt_type(_) -> [node_type]. diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl new file mode 100644 index 00000000..d8e0a435 --- /dev/null +++ b/src/ejabberd_oauth.erl @@ -0,0 +1,490 @@ +%%%------------------------------------------------------------------- +%%% File : ejabberd_oauth.erl +%%% Author : Alexey Shchepin <alexey@process-one.net> +%%% Purpose : OAUTH2 support +%%% Created : 20 Mar 2015 by Alexey Shchepin <alexey@process-one.net> +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2016 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(ejabberd_oauth). + +-behaviour(gen_server). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, + handle_info/2, terminate/2, code_change/3]). + +-export([start/0, + start_link/0, + get_client_identity/2, + verify_redirection_uri/3, + authenticate_user/2, + authenticate_client/2, + verify_resowner_scope/3, + associate_access_code/3, + associate_access_token/3, + associate_refresh_token/3, + check_token/4, + check_token/2, + process/2, + opt_type/1]). + +-include("jlib.hrl"). + +-include("ejabberd.hrl"). +-include("logger.hrl"). + +-include("ejabberd_http.hrl"). +-include("ejabberd_web_admin.hrl"). + +-record(oauth_token, { + token = {<<"">>, <<"">>} :: {binary(), binary()}, + us = {<<"">>, <<"">>} :: {binary(), binary()}, + scope = [] :: [binary()], + expire :: integer() + }). + +-define(EXPIRE, 3600). + +start() -> + init_db(mnesia, ?MYNAME), + Expire = expire(), + application:set_env(oauth2, backend, ejabberd_oauth), + application:set_env(oauth2, expiry_time, Expire), + application:start(oauth2), + ChildSpec = {?MODULE, {?MODULE, start_link, []}, + temporary, 1000, worker, [?MODULE]}, + supervisor:start_child(ejabberd_sup, ChildSpec), + ok. + +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + + +init([]) -> + erlang:send_after(expire() * 1000, self(), clean), + {ok, ok}. + +handle_call(_Request, _From, State) -> + {reply, bad_request, State}. + +handle_cast(_Msg, State) -> {noreply, State}. + +handle_info(clean, State) -> + {MegaSecs, Secs, MiniSecs} = os:timestamp(), + TS = 1000000 * MegaSecs + Secs, + F = fun() -> + Ts = mnesia:select( + oauth_token, + [{#oauth_token{expire = '$1', _ = '_'}, + [{'<', '$1', TS}], + ['$_']}]), + lists:foreach(fun mnesia:delete_object/1, Ts) + end, + mnesia:async_dirty(F), + erlang:send_after(trunc(expire() * 1000 * (1 + MiniSecs / 1000000)), + self(), clean), + {noreply, State}; +handle_info(_Info, State) -> {noreply, State}. + +terminate(_Reason, _State) -> ok. + +code_change(_OldVsn, State, _Extra) -> {ok, State}. + + +init_db(mnesia, _Host) -> + mnesia:create_table(oauth_token, + [{disc_copies, [node()]}, + {attributes, + record_info(fields, oauth_token)}]), + mnesia:add_table_copy(oauth_token, node(), disc_copies); +init_db(_, _) -> + ok. + + +get_client_identity(Client, Ctx) -> {ok, {Ctx, {client, Client}}}. + +verify_redirection_uri(_, _, Ctx) -> {ok, Ctx}. + +authenticate_user({User, Server}, {password, Password} = Ctx) -> + case jid:make(User, Server, <<"">>) of + #jid{} = JID -> + Access = + ejabberd_config:get_option( + {oauth_access, JID#jid.lserver}, + fun(A) when is_atom(A) -> A end, + none), + case acl:match_rule(JID#jid.lserver, Access, JID) of + allow -> + case ejabberd_auth:check_password(User, <<"">>, Server, Password) of + true -> + {ok, {Ctx, {user, User, Server}}}; + false -> + {error, badpass} + end; + deny -> + {error, badpass} + end; + error -> + {error, badpass} + end. + +authenticate_client(Client, Ctx) -> {ok, {Ctx, {client, Client}}}. + +verify_resowner_scope({user, _User, _Server}, Scope, Ctx) -> + Cmds = ejabberd_commands:get_commands(), + Cmds1 = [sasl_auth | Cmds], + RegisteredScope = [atom_to_binary(C, utf8) || C <- Cmds1], + case oauth2_priv_set:is_subset(oauth2_priv_set:new(Scope), + oauth2_priv_set:new(RegisteredScope)) of + true -> + {ok, {Ctx, Scope}}; + false -> + {error, badscope} + end; +verify_resowner_scope(_, _, _) -> + {error, badscope}. + + +associate_access_code(_AccessCode, _Context, AppContext) -> + %put(?ACCESS_CODE_TABLE, AccessCode, Context), + {ok, AppContext}. + +associate_access_token(AccessToken, Context, AppContext) -> + {user, User, Server} = + proplists:get_value(<<"resource_owner">>, Context, <<"">>), + Scope = proplists:get_value(<<"scope">>, Context, []), + Expire = proplists:get_value(<<"expiry_time">>, Context, 0), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + R = #oauth_token{ + token = AccessToken, + us = {LUser, LServer}, + scope = Scope, + expire = Expire + }, + mnesia:dirty_write(R), + {ok, AppContext}. + +associate_refresh_token(_RefreshToken, _Context, AppContext) -> + %put(?REFRESH_TOKEN_TABLE, RefreshToken, Context), + {ok, AppContext}. + + +check_token(User, Server, Scope, Token) -> + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + case catch mnesia:dirty_read(oauth_token, Token) of + [#oauth_token{us = {LUser, LServer}, + scope = TokenScope, + expire = Expire}] -> + {MegaSecs, Secs, _} = os:timestamp(), + TS = 1000000 * MegaSecs + Secs, + oauth2_priv_set:is_member( + Scope, oauth2_priv_set:new(TokenScope)) andalso + Expire > TS; + _ -> + false + end. + +check_token(Scope, Token) -> + case catch mnesia:dirty_read(oauth_token, Token) of + [#oauth_token{us = {LUser, LServer}, + scope = TokenScope, + expire = Expire}] -> + {MegaSecs, Secs, _} = os:timestamp(), + TS = 1000000 * MegaSecs + Secs, + case oauth2_priv_set:is_member( + Scope, oauth2_priv_set:new(TokenScope)) andalso + Expire > TS of + true -> {ok, LUser, LServer}; + false -> false + end; + _ -> + false + end. + + +expire() -> + ejabberd_config:get_option( + oauth_expire, + fun(I) when is_integer(I) -> I end, + ?EXPIRE). + +-define(DIV(Class, Els), + ?XAE(<<"div">>, [{<<"class">>, Class}], Els)). +-define(INPUTID(Type, Name, Value), + ?XA(<<"input">>, + [{<<"type">>, Type}, {<<"name">>, Name}, + {<<"value">>, Value}, {<<"id">>, Name}])). +-define(LABEL(ID, Els), + ?XAE(<<"label">>, [{<<"for">>, ID}], Els)). + +process(_Handlers, + #request{method = 'GET', q = Q, lang = Lang, + path = [_, <<"authorization_token">>]}) -> + ResponseType = proplists:get_value(<<"response_type">>, Q, <<"">>), + ClientId = proplists:get_value(<<"client_id">>, Q, <<"">>), + RedirectURI = proplists:get_value(<<"redirect_uri">>, Q, <<"">>), + Scope = proplists:get_value(<<"scope">>, Q, <<"">>), + State = proplists:get_value(<<"state">>, Q, <<"">>), + Form = + ?XAE(<<"form">>, + [{<<"action">>, <<"authorization_token">>}, + {<<"method">>, <<"post">>}], + [?LABEL(<<"username">>, [?CT(<<"User">>), ?C(<<": ">>)]), + ?INPUTID(<<"text">>, <<"username">>, <<"">>), + ?BR, + ?LABEL(<<"server">>, [?CT(<<"Server">>), ?C(<<": ">>)]), + ?INPUTID(<<"text">>, <<"server">>, <<"">>), + ?BR, + ?LABEL(<<"password">>, [?CT(<<"Password">>), ?C(<<": ">>)]), + ?INPUTID(<<"password">>, <<"password">>, <<"">>), + ?INPUT(<<"hidden">>, <<"response_type">>, ResponseType), + ?INPUT(<<"hidden">>, <<"client_id">>, ClientId), + ?INPUT(<<"hidden">>, <<"redirect_uri">>, RedirectURI), + ?INPUT(<<"hidden">>, <<"scope">>, Scope), + ?INPUT(<<"hidden">>, <<"state">>, State), + ?BR, + ?INPUTT(<<"submit">>, <<"">>, <<"Accept">>) + ]), + Top = + ?DIV(<<"section">>, + [?DIV(<<"block">>, + [?A(<<"https://www.ejabberd.im">>, + [?XA(<<"img">>, + [{<<"height">>, <<"32">>}, + {<<"src">>, logo()}])] + )])]), + Middle = + ?DIV(<<"white section">>, + [?DIV(<<"block">>, + [?XC(<<"h1">>, <<"Authorization request">>), + ?XE(<<"p">>, + [?C(<<"Application ">>), + ?XC(<<"em">>, ClientId), + ?C(<<" wants to access scope ">>), + ?XC(<<"em">>, Scope)]), + Form + ])]), + Bottom = + ?DIV(<<"section">>, + [?DIV(<<"block">>, + [?XAC(<<"a">>, + [{<<"href">>, <<"https://www.ejabberd.im">>}, + {<<"title">>, <<"ejabberd XMPP server">>}], + <<"ejabberd">>), + ?C(" is maintained by "), + ?XAC(<<"a">>, + [{<<"href">>, <<"https://www.process-one.net">>}, + {<<"title">>, <<"ProcessOne - Leader in Instant Messaging and Push Solutions">>}], + <<"ProcessOne">>) + ])]), + Body = ?DIV(<<"container">>, [Top, Middle, Bottom]), + ejabberd_web:make_xhtml(web_head(), [Body]); +process(_Handlers, + #request{method = 'POST', q = Q, lang = _Lang, + path = [_, <<"authorization_token">>]}) -> + _ResponseType = proplists:get_value(<<"response_type">>, Q, <<"">>), + ClientId = proplists:get_value(<<"client_id">>, Q, <<"">>), + RedirectURI = proplists:get_value(<<"redirect_uri">>, Q, <<"">>), + SScope = proplists:get_value(<<"scope">>, Q, <<"">>), + Username = proplists:get_value(<<"username">>, Q, <<"">>), + Server = proplists:get_value(<<"server">>, Q, <<"">>), + Password = proplists:get_value(<<"password">>, Q, <<"">>), + State = proplists:get_value(<<"state">>, Q, <<"">>), + Scope = str:tokens(SScope, <<" ">>), + case oauth2:authorize_password({Username, Server}, + ClientId, + RedirectURI, + Scope, + {password, Password}) of + {ok, {_AppContext, Authorization}} -> + {ok, {_AppContext2, Response}} = + oauth2:issue_token(Authorization, none), + {ok, AccessToken} = oauth2_response:access_token(Response), + {ok, Type} = oauth2_response:token_type(Response), + {ok, Expires} = oauth2_response:expires_in(Response), + {ok, VerifiedScope} = oauth2_response:scope(Response), + %oauth2_wrq:redirected_access_token_response(ReqData, + % RedirectURI, + % AccessToken, + % Type, + % Expires, + % VerifiedScope, + % State, + % Context); + {302, [{<<"Location">>, + <<RedirectURI/binary, + "?access_token=", AccessToken/binary, + "&token_type=", Type/binary, + "&expires_in=", (integer_to_binary(Expires))/binary, + "&scope=", (str:join(VerifiedScope, <<" ">>))/binary, + "&state=", State/binary>> + }], + ejabberd_web:make_xhtml([?XC(<<"h1">>, <<"302 Found">>)])}; + {error, Error} when is_atom(Error) -> + %oauth2_wrq:redirected_error_response( + % ReqData, RedirectURI, Error, State, Context) + {302, [{<<"Location">>, + <<RedirectURI/binary, + "?error=", (atom_to_binary(Error, utf8))/binary, + "&state=", State/binary>> + }], + ejabberd_web:make_xhtml([?XC(<<"h1">>, <<"302 Found">>)])} + end; +process(_Handlers, _Request) -> + ejabberd_web:error(not_found). + + + + +web_head() -> + [?XA(<<"meta">>, [{<<"http-equiv">>, <<"X-UA-Compatible">>}, + {<<"content">>, <<"IE=edge">>}]), + ?XA(<<"meta">>, [{<<"name">>, <<"viewport">>}, + {<<"content">>, + <<"width=device-width, initial-scale=1">>}]), + ?XC(<<"title">>, <<"Authorization request">>), + ?XC(<<"style">>, css()) + ]. + +css() -> + <<" + body { + margin: 0; + padding: 0; + + font-family: sans-serif; + color: #fff; + } + + h1 { + font-size: 3em; + color: #444; + } + + p { + line-height: 1.5em; + color: #888; + } + + a { + color: #fff; + } + a:hover, + a:active { + text-decoration: underline; + } + + em { + display: inline-block; + padding: 0 5px; + + background: #f4f4f4; + border-radius: 5px; + + font-style: normal; + font-weight: bold; + color: #444; + } + + form { + color: #444; + } + label { + display: block; + font-weight: bold; + } + + input[type=text], + input[type=password] { + margin-bottom: 1em; + padding: 0.4em; + + max-width: 330px; + width: 100%; + + border: 1px solid #c4c4c4; + border-radius: 5px; + outline: 0; + + font-size: 1.2em; + } + input[type=text]:focus, + input[type=password]:focus, + input[type=text]:active, + input[type=password]:active { + border-color: #41AFCA; + } + + input[type=submit] { + font-size: 1em; + } + + .container { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + + background: #424A55; + background-image: -webkit-linear-gradient(270deg, rgba(48,52,62,0) 24%, #30353e 100%); + background-image: linear-gradient(-180deg, rgba(48,52,62,0) 24%, #30353e 100%); + } + + .section { + padding: 3em; + } + .white.section { + background: #fff; + border-bottom: 4px solid #41AFCA; + } + + .white.section a { + text-decoration: none; + color: #41AFCA; + } + .white.section a:hover, + .white.section a:active { + text-decoration: underline; + } + + .container > .section { + background: #424A55; + } + + .block { + margin: 0 auto; + max-width: 900px; + width: 100%; + } +">>. + +logo() -> + <<"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAABACAYAAACgPErgAAAVtklEQVR42uydeXhU1d2ADzuyCcii4FZBZdEqVYtaUGqlikVwaRWsrWBRKIh8KgXcEAX9VECtoljcURAJICJWQLaKLAEjIHtBSNhC9oQkk2SW+36/52P+iIE5d5kzk4Fn3ud5H/7gIbnD3PPOveeeuVedygD9xLlQMh9yJ0D+GeokJAg1xcEWzOWYj0DBaQm0fXdaMCu8bROgpJVKksQj0+F84OXw/jRd7KVOdbbCFI6jdDNktlAnESVQNwSzOI6yhZBaV1UzAXiB4wj8CBtbqiRJXDIILtgL+zmOb8aoU5Xh0MMiEr5x6iSiAIZYROLo/aoayYMeocjbNlklSeKC9VAjBDM5IcEQfNZFnYq8AM8SkcJV6iSiCGYRkT1LVDWSA08Tkfy9MLaeOsUA2ot9xJHiW2KK+LW4Mvxnijg1/Pd9xQvFGiqJLUOhcT5kE5Etg0/VYI3TBOvbkyxYKURk2dpqDZb2gyEjC1SjUyBQdcQe4mtimujDHWXiD+Lr4g1iXZXkhISgBZBLZIaoU5GR0I2IVDxxkgVrNhH5z+pqDZb2g+FAJjQ4aYMFtBAfEzdils3iP8TkhYkqlMEZFuQQmcHqVGUBTOI40lLhz02TwUoGKxJAI/FxMYPYckB8Smyikvw/vlM/WHqAP4ifiHPF0fBsYyUkg5UM1okAbhV/JL5sE29TSZLBOhHJYCWDVRXgNPFVqpfXxQbJYCWDlQxWMlgRAc4VV5EYrBZ/kQxWMljJYFVLsBomdLCAS8RdJBZ7xMuSwTIcLKCO2EG8VrxdvEfsLV4F1vkJstq9BlidwN8hVleSLLgUuFzsEv6zs9g4EYIFNBAvFi87tm3WZWI7oHZ8glWnkWbb6ortCG/bsT+tDsBpcYrVpeIhzBHAHJni5XGOd5PwvnKJif0DaCh2/Pn7y/linbgFKx/qrjkWpffEHZo3qVhMBWuC2EVVA4ehay6sAYJgBeDocuh9iYqCL+F04DbxLTFNPMzxWGI6WCuBJ8Rfxi5YK34WLKC5BX3FVzj22n8S/fwcX/i9+1wcLraLVbAqr8OqgNOA31nwPLBc3HWCNU1+cbe4WBwjXhbD08A9eOcn8SNxmHij+EuxQ3iwdxP/Jr4pbsU7+2J5ehiCNuI9FkwN78sZol+0xDSYd6OH75R2BcZasALYKwZO0IWd4fd3LIS6g1XHeLBWQV1gsLgJ9wTBWgSBm1ScuBYuAvI4jrx9cOEZyiXzoUUIxhbDPm+fvFlL4PPe5oO14FslvAjnrYeXgYO4p0T8UrzJbLB2HlbCZMqbfwOjgrDD41HLCrE/LK1pKFYNxFS8sVDsK1Y5ctQeRf42HLcy3JMmNjIc6+ss+ETMRUuRH9Zd7SBS9QMwCFiDNzZBYAT4G5dTUcuC7KiClQ3dj8B6osYSAzOgoq2KMatgChF5e5RyQQXcVwL7MMNXsKOTuWAd2FBG9uhyyMMI6XPBd7GZYOUd8pP+UCn8hBEOr4KCbgYG7Ju4Z43YU0VB+LRoLu6ZpgyQAhflw2e4Y7lYS0XADzdZ8ANGsLaX88PgACHvwfLBY4Afo4T2Q9lNMZ68W0lE/J8oB5RDkxDMwDhF+XC4v6tgxZdcKLov+mBhYR4/FD8VRTRu93CE97RYTxkCuFfMxR13qSjIhoE+Tx9qPh9Mb6WqcBRqBuBli5hguQ4WUGsPvGsRKyw/hAaoGJEPy4lI4Uxlw3XHzu/XEVNKHk3QYAmWWDEecmraBqtaCE6HrCYeJpX34Jwc8eYYXp3cjHPSxWbKA9/Da3im+CeY1kBV4gNoUQILiB/6YD0JNYFP4jMoGFINwZqhNHwKLbfB5ji9/kdMBss8vk9MBcs8pasht7GLSIzHOQfFX6kYApyFuw/FFz38jveJitxBqhI3QMsS2IiQMMFaDFOJHyF45fZECdYOqA0sIX5YMPkOk8Eyz96xpoJlnvIZDgduazEfZ+SJVyoNhqO1DWcUim1dfJf2DTxTVgxFY1Ql+kOtivDYSJhgTYX7cIx1EJgmDhSvEbuK12XAiHyYCxThCKsQHmiXCMHa5+5T+BBY74qVX39vcUIhbLBwSkUO7DonBsEqBCtFfBjoHt6+34mjLfg34HO4feL8mw0HqwKsr8XRwA2E9x3xYY693jzHvWfZCAdheAZnWG6+1wecKd4oDhFHicPFvm6WI4SXRBTijOcdzi3+BefkifPAehLoI14Pqcdt/1x3YyNb/BisYUC38PvbU3wivOQhGHWw+sK5ASjAltBB8D8MQe05NXCBBZOBCmxJXwbTa1RnsDbCL4Pgx5ajeeJjYDVXEZgDNSugVwhW44j1sw0GKwjrX4El2kGzHDqVwzvO1x3d2dhMsPbMgjnaRZGroW0OjHcY1VIYeZHNMoZ0nPFP5YDwIJwp5miWinwt3u7w5z3g4lS1sc2c1Tl+Z0eTRXBkLKy1PWp7Ga4EAthSXAhHn4BQa5vX2zUIX1rRBKvU0fluYDaUt3F5xa5rNmzFlsfurs5grYP52JK7AVZcpBxSCrU3wvPOPtgzuxsI1gG3l9/90Cfg6Ihm/ONRBssn3udyceNVftiBLXNnaAZHbxcLNZs6uPXMFJdHCF84OeICluIIfQTzHI3jrI0Q7Kwc4myaxEqDtR2VC9bCCMDvOliZ0NGCCv1pwb7XlEdmQCtgOVp2bYU69aojWB/AVUEIoWcpfNlMeQD4q/0bs/GL6IK1JR3Wd/T4/lxZDJloKTgMrzXzFqyDJfBNT+WB2dD2kO1Eb9AP/zrhAAQ+xhl/d7A6fm0U977qbvPzr3YYwlkqArnQ2f4swb8SZjZXDtkG3SxsWSZ6HRt9RJ+rYP0H/omWdVNUlGyDxpm2b/i4P1VHsPbYnhrl7IQ/NlNRAPzDfqnH0o4eg1UEEy+N8mkl3e1P378e4CFYFnzRR0VBVzgP26BumqyqANQX92JPhm5FOXC2uJXoKBR7KA3AQuzZLzaMcJbwqv2pfXFL5YKV8BFaCvfCH1uqKDgKA0NOgzUAGuRp7664eaUyxD3Qpkz7u0oWxDtY/aBhFhzUD7hBN6goeRVq7IRVNkEY4y1YRx5VBiiHV9ByaK77YOW+pwxQCn9DS/4W2Fy7SgCuEEPYM1ETkXriSsxwRLxQe7RhjyV2VVW4E+ofhj1oSe2rXDAaGhTBAbQM72vo6VDzHQXrX9r7nxeXwqrLYWcNoFa0hj8F7rQiTuAV5cHIFvEM1jvQ3UJH+QJliN9Dj5B2APkWuw/Wrm3QrJ6hZyC20q/C9qfDsgbOg1WYDxe2NbRtdfTfZbXKIbW9x8ns30QISA1xEmbZLDbRzJEdwp6hqgqvwVWWdt9avVy5ZDL8Wj9VstbYwUwpdALKbIP1HxhJRAJlwPfhHWWzATeJafpz9bd/H89grYBRaHn+RmWQn2CdZtsyYWhjd8HaPVQZZKl2HV7AgvEXOw9W1pvKIDNhGFrevbVKACY6vPLWSHN0NUp8ThxnyJfECzRHWSlermaugfvRcuQu5ZLv4F60pN+lDLIZUmyDtRRmklAUPBzPYK2GT3Vf4IX+jZRBPoYniUioHA62dx6sQAVkt1MGSYXeFjoC3ZwFKyRm/14Z5Bu4OKidZwsOqjL4P8eeZSqBAMZgz0JVhQ3wOhEpLoLxZyqXHIAJRKS0CF48UxnkWehnG6wj8BUJxfK3VZQUuAiW/ntRwa+UYR6BP+gH+f6eLoK1CQprKoMchIv82itNGfc5DFY+FLUyvG0NS2E/Ecl8qcrg/w573kuwYP0Re1JVFd7RfvAWbYIU1/vJ2/AhETm6HlJqKIOshA4hKNMGK5RwwZrygYFHqC9zGqygNlgrP4vBjQW7hcCKHKy9tzh/kGrqd8ow+6CVX3tPovShKkw2PKuJx2G4tKEyyF6o4YPvicjhN1UlHD5TcGKCBetG7Nkq/ixA+doPNv9i5YFp2iuE1mJlmBxoEbR7kKqVcMGaOU1FyTpYq7tbg/NgbZ+tDLMLrrcJVi9ViTLtKfsq48E6CK392p0m/e8qTDE8RUQOHobmRoN1AGqWQZomWFMq33FE/C/2vJBgwboee/aKdVzMfXk6U3hLGyyMBysXWga1i5jTBqmchAvWY31VlGRqbyrmn64qka8NVsj4/MZg6IPgNFj7tV9iLfsRVtdSBsmATgEIaoJ/mwqzEwYTkWABbDhLGSQdGpdp12OVPF7lmQO7HU1gJxDAI9izs+oRVlAfrH8rDyzQXoAp3gxv1zJ8UaVTSDtHec9f1bqEmXQPiuUfwjdR/Se8AG0sOEpEUidVmax8n4iU58OKFoaPsJ5xE6x/wwT9pPuTRifdv4K79Nv3QZdK8yb90PLazcogH8HlIQgQkfl3ewjWpwn2yLH92JOmhFgHqwSGE5EyH7x/vjJICtyjX362s6f6Cf6hX9tSOB8Q+SJGfimmQPogZYCXoD9avh1WZYAORcvwfsogpbDeTbBmwW1oKR6mDJIGs4iIvxAeOLPSpGxn/dGYb6rhJRdPod2jn7nGQ7DWaQJSQzxLbC22iqFtxDvE7ThjRTyCNQ1uRkvqYGWQQzBH/yV3+R7zFOhhab8u8smt6iThN1AjB1brd+rnuqhKvAFX6hdz/rgBLqqpDJAHPS2w3ARrIpzth2IiEtgBM+spA2RBB/33unZ9qyrxDNTLg52a11MAS842dDrYRH/XhfxDMLyhh2AViK0jBKum+K5YKObE0CLc8WE8gvUStC7XbtuWrXBeHWWA7tDJp933dq+F8TXVHmgS1M4LFByE/q3VScB826OrwG7IqqsqcQvU3Q+70OJ7SEVJO6iVARsQHAcrTBosR8vrI5UBNsBctBQ+raowD6ag5ev3lQFmw3i0FM9SgiZYOiJ+KHMs4iUkFk+aC5aeLbAULYERygBB+BIt+8ZV3lHfQ8uRRaDqqASmF7Sxf8xV6SR1AjbBi+gphPIuUcb0aQQvwfoWBqDFKoEXuqooWAID0RLyw56OJ7j319WAhZZp/aOMVTegDC0ZfaII1sdKA9CPxOLWeAVrMgy0Hxu9ukS5NvEh+33v6Q6VT4uusCCElpnz4Ie6KgFZAWctgTS0lPjg2osiLDRtFwAfWoKHIfRrj/NCI0oAr8EqgAZ+2IOew7DkWuWBAPQvBT9ayuZo5uUWo8cH6z1FKwTXFkEOWio2Q1ndKIJVILa1XYGeGBSJbeIVrObQKBPS0VJ6ABZ7itY2eLAIO1JmqKqUwzzsWQyB9soFwGliQxUj/HCNs/tiH5hqM2hfxxarCPwPughBI80dEBwHK3wUOMDCjtKjbp5GlAn1vodxgIWeoO6hDGvhmqDtzygXA0/BN3VcPF/yAYfzO3crQRcsE+uxgIfEUqqXVUowGyw9I2EY9uRCxV0uls803AwTsacC3uisqjIEOgMV2GLlAeMgdK7NbW47Aq+Ke8UD4jsw4yyDj9ruIE6yoAxbyo/AjjOVhnehVQFk4ojQIgjeDNSNsGO3FP9mwVYEE8FqAbUOwwoccWAh+HuCVS/C9jUNwZ+BNByx9A0Hd4+ciiOOpELx3WBFumNB/RD0smARjti5FPrVNBCsQvECB9G6SvwaZ/xX/F/xf8S5YojoGRXvYM2C+j7nT8uZC9ZvI42NhdA8AAOALTgi5TkVic/gSRxjFQDzxKFiH7G7eJv4WAhWBqCc49i/Gya2US7hWPzuFf8iTrBgGVCOY4r/5HCupF8AV2wDazrwiDhEfEFcKGYRxkiwwlRAeyAb5+wQPxFHioPEseLn4gEcU7gdLm3q4PubTVw+ay9DTBGfCm/bKPFTcSfOyYe+FyrBQLAQPnfzyHdxSvg1F4hlYrGYHn5d94hNTnC/qxK8Uya2Mx8se0rhCsCHc7aCVXnfG2fB/Ao4hGPSV8O59VQkfgs1N8IiYkrBK8oFW+E5wIdndkx2eVXkeYxgPljh7bs1CCHigr8Ull7u4r26pBgKiBtb7lCCqWCFeVi5AKgtthE7iL8QGykNwGi8s0AJcQ9WmOnwIHEjlAFp5yg7noNmQCoxo2yNizmWoUG8Yon73oOzayiXAG8mZLDC/BkGlIBFTMkvg1v6eFhPc31mzKNVIf59hBJiECy/2FvFCOCcKObBesQjWNV/8aFiPxR0cbNRrcVVxITgauWAR6H+IdiNZxZPinJdyKSEDFaY5dAXyCY2HIJPeyqPvAlXANuJDT74/n4leA6WPUfFXjEKVjMxD/css/m5c7TBMsgmGBGI2VG+9T0UtvdyI7cme+EzjPPhROWACmiJp0/q8lzY/RdlgFUwDCjGGFZYXbDSb3Fx4aGzH77DKBmrIPdCFSUcWyU9B6Pk7ICMbkowFiz9fNFAZRjgGjGIOwLi1UrDBv0R1hJlmCLoFYIMs2Mj8D74m6poAIZbkGtmg/gIbmumHLID5uOK0GzIb294B7skCAssoiYLsh6FdWIk/D7Ycp7LZ7vV2Qejifo9CmYBo+Hb2oY/jQcC+4iKUAnwCmxvrgTjwdLzttjS0L7U3uOjwqYoG3ZqF19WvKVigA/aBGEaUEFUWFsh+CeTa50uCMAUC3LwRioEb1MueRw62a/DsXLE9yH4GxVD/HCjBfPEYlxxdDsUjgPrbCXMprTF/oiPnip9QnnEgnND8KwFu3DHdigfC4vaqhgBNA3CwxZsxB37xFdhUSelQRssM+wVh4iNPb7+RhwLyhHcs8PJM/+OHLv1ziaOw5cNmReoGAL8KgRvBWC/5e4IZh2EBomnqVgQgrYWDBUXApkRlhaUillgLRVfFm8AaiqPAN3ENWJ+2ExxJVjTIHQXBM9UcSQI51twLzBNXMSxQOSIeeEdchNYC469dq6HH+qd4DYpvwA+FPeIeeF/86Ch9+g0C34HTBC/FLeK2eHfkyWmiZ+LT4rXhddqxYUQ1LLgGmCMOF/8QcwKb1tOeFsXhNcu3QyWLhDmg2XPLvFZ8UpRO8iAVuL14nPiTrxRLnZVDjkC54TveJAfdjEcuUzFia/h9FLoDbwifhV+3XmVxsZ6cQ5Yj0LoChVPkI0TO4q9wLoDuFW8TrxAbGr4d9WwoAVwhni6SiCA+mIzsXn4/6S2i3/bIPzvasdw++pW2r6mYi2VIAC1wtvUPLyNdZUGz8EyTyj8s78KnzK+LL4kviPOE9eJeUTP/R5f+xlgnZEIY8OC5pXGRg2VJEkSTbBOXsaoJEmSJIN1EvC0SvJ/7dSxigEAHIDxf5FisAilbNbbWGSi5OZ7BgZPcS/hAZTNwFtYSVltit2iU/c9gxv8u75ffa/wSQ4ruR+ahySHldyZJiHJYSW3pnZIcliJHekrJDmsxMM60JQqIUkJh3WhJX1SKSTpzcN60J1udKQNfdOIaiFJLwxrTwua0Zj61PtjXfqgDtWpEJL04rCetKKBM5GUeVgnGoYkJR1Wka60o0ZIUuJhVWlLzZCk5MMqUyv0r/wCSDD/4sxS1q8AAAAASUVORK5CYII=">>. + +opt_type(oauth_expire) -> + fun(I) when is_integer(I), I >= 0 -> I end; +opt_type(oauth_access) -> + fun(A) when is_atom(A) -> A end; +opt_type(_) -> [oauth_expire, oauth_access]. diff --git a/src/ejabberd_odbc.erl b/src/ejabberd_odbc.erl index 09f17a63..e9866586 100644 --- a/src/ejabberd_odbc.erl +++ b/src/ejabberd_odbc.erl @@ -5,7 +5,7 @@ %%% Created : 8 Dec 2004 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,6 +25,8 @@ -module(ejabberd_odbc). +-behaviour(ejabberd_config). + -author('alexey@process-one.net'). -define(GEN_FSM, p1_fsm). @@ -39,11 +41,16 @@ sql_bloc/2, escape/1, escape_like/1, + escape_like_arg/1, to_bool/1, sqlite_db/1, sqlite_file/1, - encode_term/1, - decode_term/1, + encode_term/1, + decode_term/1, + odbc_config/0, + freetds_config/0, + odbcinst_config/0, + init_mssql/1, keep_alive/1]). %% gen_fsm callbacks @@ -51,20 +58,22 @@ handle_info/3, terminate/3, print_state/1, code_change/4]). -%% gen_fsm states -export([connecting/2, connecting/3, - session_established/2, session_established/3]). + session_established/2, session_established/3, + opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). +-include("ejabberd_sql_pt.hrl"). -record(state, {db_ref = self() :: pid(), - db_type = odbc :: pgsql | mysql | sqlite | odbc, - start_interval = 0 :: non_neg_integer(), - host = <<"">> :: binary(), + db_type = odbc :: pgsql | mysql | sqlite | odbc | mssql, + db_version = undefined :: undefined | non_neg_integer(), + start_interval = 0 :: non_neg_integer(), + host = <<"">> :: binary(), max_pending_requests_len :: non_neg_integer(), - pending_requests = {0, queue:new()} :: {non_neg_integer(), ?TQUEUE}}). + pending_requests = {0, queue:new()} :: {non_neg_integer(), ?TQUEUE}}). -define(STATE_KEY, ejabberd_odbc_state). @@ -76,13 +85,17 @@ -define(MYSQL_PORT, 3306). +-define(MSSQL_PORT, 1433). + -define(MAX_TRANSACTION_RESTARTS, 10). -define(TRANSACTION_TIMEOUT, 60000). -define(KEEPALIVE_TIMEOUT, 60000). --define(KEEPALIVE_QUERY, <<"SELECT 1;">>). +-define(KEEPALIVE_QUERY, [<<"SELECT 1;">>]). + +-define(PREPARE_KEY, ejabberd_odbc_prepare). %%-define(DBGFSM, true). @@ -108,16 +121,18 @@ start_link(Host, StartInterval) -> [Host, StartInterval], fsm_limit_opts() ++ (?FSMOPTS)). --type sql_query() :: [sql_query() | binary()]. +-type sql_query() :: [sql_query() | binary()] | #sql_query{} | + fun(() -> any()) | fun((atom(), _) -> any()). -type sql_query_result() :: {updated, non_neg_integer()} | {error, binary()} | {selected, [binary()], - [[binary()]]}. + [[binary()]]} | + {selected, [any()]}. -spec sql_query(binary(), sql_query()) -> sql_query_result(). sql_query(Host, Query) -> - sql_call(Host, {sql_query, Query}). + check_error(sql_call(Host, {sql_query, Query}), Query). %% SQL transaction based on a list of queries %% This function automatically @@ -145,7 +160,8 @@ sql_call(Host, Msg) -> case ejabberd_odbc_sup:get_random_pid(Host) of none -> {error, <<"Unknown Host">>}; Pid -> - (?GEN_FSM):sync_send_event(Pid,{sql_cmd, Msg, now()}, + (?GEN_FSM):sync_send_event(Pid,{sql_cmd, Msg, + p1_time_compat:monotonic_time(milli_seconds)}, ?TRANSACTION_TIMEOUT) end; _State -> nested_op(Msg) @@ -153,7 +169,8 @@ sql_call(Host, Msg) -> keep_alive(PID) -> (?GEN_FSM):sync_send_event(PID, - {sql_cmd, {sql_query, ?KEEPALIVE_QUERY}, now()}, + {sql_cmd, {sql_query, ?KEEPALIVE_QUERY}, + p1_time_compat:monotonic_time(milli_seconds)}, ?KEEPALIVE_TIMEOUT). -spec sql_query_t(sql_query()) -> sql_query_result(). @@ -184,6 +201,13 @@ escape_like($%) -> <<"\\%">>; escape_like($_) -> <<"\\_">>; escape_like(C) when is_integer(C), C >= 0, C =< 255 -> odbc_queries:escape(C). +escape_like_arg(S) when is_binary(S) -> + << <<(escape_like_arg(C))/binary>> || <<C>> <= S >>; +escape_like_arg($%) -> <<"\\%">>; +escape_like_arg($_) -> <<"\\_">>; +escape_like_arg($\\) -> <<"\\\\">>; +escape_like_arg(C) when is_integer(C), C >= 0, C =< 255 -> <<C>>. + to_bool(<<"t">>) -> true; to_bool(<<"true">>) -> true; to_bool(<<"1">>) -> true; @@ -193,7 +217,8 @@ to_bool(_) -> false. encode_term(Term) -> escape(list_to_binary( - erl_prettypr:format(erl_syntax:abstract(Term)))). + erl_prettypr:format(erl_syntax:abstract(Term), + [{paper, 65535}, {ribbon, 65535}]))). decode_term(Bin) -> Str = binary_to_list(<<Bin/binary, ".">>), @@ -248,15 +273,16 @@ connecting(connect, #state{host = Host} = State) -> end, {_, PendingRequests} = State#state.pending_requests, case ConnectRes of - {ok, Ref} -> - erlang:monitor(process, Ref), - lists:foreach(fun (Req) -> - (?GEN_FSM):send_event(self(), Req) - end, - queue:to_list(PendingRequests)), - {next_state, session_established, - State#state{db_ref = Ref, - pending_requests = {0, queue:new()}}}; + {ok, Ref} -> + erlang:monitor(process, Ref), + lists:foreach(fun (Req) -> + (?GEN_FSM):send_event(self(), Req) + end, + queue:to_list(PendingRequests)), + State1 = State#state{db_ref = Ref, + pending_requests = {0, queue:new()}}, + State2 = get_db_version(State1), + {next_state, session_established, State2}; {error, Reason} -> ?INFO_MSG("~p connection failed:~n** Reason: ~p~n** " "Retry after: ~p seconds", @@ -364,7 +390,7 @@ print_state(State) -> State. %%%---------------------------------------------------------------------- run_sql_cmd(Command, From, State, Timestamp) -> - case timer:now_diff(now(), Timestamp) div 1000 of + case p1_time_compat:monotonic_time(milli_seconds) - Timestamp of Age when Age < (?TRANSACTION_TIMEOUT) -> put(?NESTING_KEY, ?TOP_LEVEL_TXN), put(?STATE_KEY, State), @@ -429,13 +455,13 @@ outer_transaction(F, NRestarts, _Reason) -> [T]), erlang:exit(implementation_faulty) end, - sql_query_internal(<<"begin;">>), + sql_query_internal([<<"begin;">>]), put(?NESTING_KEY, PreviousNestingLevel + 1), Result = (catch F()), put(?NESTING_KEY, PreviousNestingLevel), case Result of {aborted, Reason} when NRestarts > 0 -> - sql_query_internal(<<"rollback;">>), + sql_query_internal([<<"rollback;">>]), outer_transaction(F, NRestarts - 1, Reason); {aborted, Reason} when NRestarts =:= 0 -> ?ERROR_MSG("SQL transaction restarts exceeded~n** " @@ -444,11 +470,11 @@ outer_transaction(F, NRestarts, _Reason) -> "== ~p", [?MAX_TRANSACTION_RESTARTS, Reason, erlang:get_stacktrace(), get(?STATE_KEY)]), - sql_query_internal(<<"rollback;">>), + sql_query_internal([<<"rollback;">>]), {aborted, Reason}; {'EXIT', Reason} -> - sql_query_internal(<<"rollback;">>), {aborted, Reason}; - Res -> sql_query_internal(<<"commit;">>), {atomic, Res} + sql_query_internal([<<"rollback;">>]), {aborted, Reason}; + Res -> sql_query_internal([<<"commit;">>]), {atomic, Res} end. execute_bloc(F) -> @@ -458,8 +484,74 @@ execute_bloc(F) -> Res -> {atomic, Res} end. +execute_fun(F) when is_function(F, 0) -> + F(); +execute_fun(F) when is_function(F, 2) -> + State = get(?STATE_KEY), + F(State#state.db_type, State#state.db_version). + +sql_query_internal([{_, _} | _] = Queries) -> + State = get(?STATE_KEY), + case select_sql_query(Queries, State) of + undefined -> + {error, <<"no matching query for the current DBMS found">>}; + Query -> + sql_query_internal(Query) + end; +sql_query_internal(#sql_query{} = Query) -> + State = get(?STATE_KEY), + Res = + try + case State#state.db_type of + odbc -> + generic_sql_query(Query); + pgsql -> + Key = {?PREPARE_KEY, Query#sql_query.hash}, + case get(Key) of + undefined -> + case pgsql_prepare(Query, State) of + {ok, _, _, _} -> + put(Key, prepared); + {error, Error} -> + ?ERROR_MSG("PREPARE failed for SQL query " + "at ~p: ~p", + [Query#sql_query.loc, Error]), + put(Key, ignore) + end; + _ -> + ok + end, + case get(Key) of + prepared -> + pgsql_execute_sql_query(Query, State); + _ -> + generic_sql_query(Query) + end; + mysql -> + generic_sql_query(Query); + sqlite -> + generic_sql_query(Query) + end + catch + Class:Reason -> + ST = erlang:get_stacktrace(), + ?ERROR_MSG("Internal error while processing SQL query: ~p", + [{Class, Reason, ST}]), + {error, <<"internal error">>} + end, + case Res of + {error, <<"No SQL-driver information available.">>} -> + {updated, 0}; + _Else -> Res + end; +sql_query_internal(F) when is_function(F) -> + case catch execute_fun(F) of + {'EXIT', Reason} -> {error, Reason}; + Res -> Res + end; sql_query_internal(Query) -> State = get(?STATE_KEY), + ?DEBUG("SQL: \"~s\"", [Query]), Res = case State#state.db_type of odbc -> to_odbc(odbc:sql_query(State#state.db_ref, Query, @@ -467,10 +559,6 @@ sql_query_internal(Query) -> pgsql -> pgsql_to_odbc(pgsql:squery(State#state.db_ref, Query)); mysql -> - ?DEBUG("MySQL, Send query~n~p~n", [Query]), - %%squery to be able to specify result_type = binary - %%[Query] because p1_mysql_conn expect query to be a list (elements can be binaries, or iolist) - %% but doesn't accept just a binary R = mysql_to_odbc(p1_mysql_conn:squery(State#state.db_ref, [Query], self(), [{timeout, (?TRANSACTION_TIMEOUT) - 1000}, @@ -487,6 +575,93 @@ sql_query_internal(Query) -> _Else -> Res end. +select_sql_query(Queries, State) -> + select_sql_query( + Queries, State#state.db_type, State#state.db_version, undefined). + +select_sql_query([], _Type, _Version, undefined) -> + undefined; +select_sql_query([], _Type, _Version, Query) -> + Query; +select_sql_query([{any, Query} | _], _Type, _Version, _) -> + Query; +select_sql_query([{Type, Query} | _], Type, _Version, _) -> + Query; +select_sql_query([{{Type, _Version1}, Query1} | Rest], Type, undefined, _) -> + select_sql_query(Rest, Type, undefined, Query1); +select_sql_query([{{Type, Version1}, Query1} | Rest], Type, Version, Query) -> + if + Version >= Version1 -> + Query1; + true -> + select_sql_query(Rest, Type, Version, Query) + end; +select_sql_query([{_, _} | Rest], Type, Version, Query) -> + select_sql_query(Rest, Type, Version, Query). + +generic_sql_query(SQLQuery) -> + sql_query_format_res( + sql_query_internal(generic_sql_query_format(SQLQuery)), + SQLQuery). + +generic_sql_query_format(SQLQuery) -> + Args = (SQLQuery#sql_query.args)(generic_escape()), + (SQLQuery#sql_query.format_query)(Args). + +generic_escape() -> + #sql_escape{string = fun(X) -> <<"'", (escape(X))/binary, "'">> end, + integer = fun(X) -> integer_to_binary(X) end, + boolean = fun(true) -> <<"1">>; + (false) -> <<"0">> + end + }. + +pgsql_prepare(SQLQuery, State) -> + Escape = #sql_escape{_ = fun(X) -> X end}, + N = length((SQLQuery#sql_query.args)(Escape)), + Args = [<<$$, (integer_to_binary(I))/binary>> || I <- lists:seq(1, N)], + Query = (SQLQuery#sql_query.format_query)(Args), + pgsql:prepare(State#state.db_ref, SQLQuery#sql_query.hash, Query). + +pgsql_execute_escape() -> + #sql_escape{string = fun(X) -> X end, + integer = fun(X) -> [integer_to_binary(X)] end, + boolean = fun(true) -> "1"; + (false) -> "0" + end + }. + +pgsql_execute_sql_query(SQLQuery, State) -> + Args = (SQLQuery#sql_query.args)(pgsql_execute_escape()), + ExecuteRes = + pgsql:execute(State#state.db_ref, SQLQuery#sql_query.hash, Args), +% {T, ExecuteRes} = +% timer:tc(pgsql, execute, [State#state.db_ref, SQLQuery#sql_query.hash, Args]), +% io:format("T ~s ~p~n", [SQLQuery#sql_query.hash, T]), + Res = pgsql_execute_to_odbc(ExecuteRes), + sql_query_format_res(Res, SQLQuery). + + +sql_query_format_res({selected, _, Rows}, SQLQuery) -> + Res = + lists:flatmap( + fun(Row) -> + try + [(SQLQuery#sql_query.format_res)(Row)] + catch + Class:Reason -> + ST = erlang:get_stacktrace(), + ?ERROR_MSG("Error while processing " + "SQL query result: ~p~n" + "row: ~p", + [{Class, Reason, ST}, Row]), + [] + end + end, Rows), + {selected, Res}; +sql_query_format_res(Res, _SQLQuery) -> + Res. + %% Generate the OTP callback return tuple depending on the driver result. abort_on_driver_error({error, <<"query timed out">>} = Reply, @@ -509,7 +684,10 @@ abort_on_driver_error(Reply, From) -> %% Open an ODBC database connection odbc_connect(SQLServer) -> ejabberd:start_app(odbc), - odbc:connect(binary_to_list(SQLServer), [{scrollable_cursors, off}]). + odbc:connect(binary_to_list(SQLServer), + [{scrollable_cursors, off}, + {tuple_row, off}, + {binary_strings, on}]). %% == Native SQLite code @@ -595,20 +773,33 @@ pgsql_item_to_odbc(<<"UPDATE ", N/binary>>) -> pgsql_item_to_odbc({error, Error}) -> {error, Error}; pgsql_item_to_odbc(_) -> {updated, undefined}. +pgsql_execute_to_odbc({ok, {<<"SELECT", _/binary>>, Rows}}) -> + {selected, [], [[Field || {_, Field} <- Row] || Row <- Rows]}; +pgsql_execute_to_odbc({ok, {'INSERT', N}}) -> + {updated, N}; +pgsql_execute_to_odbc({ok, {'DELETE', N}}) -> + {updated, N}; +pgsql_execute_to_odbc({ok, {'UPDATE', N}}) -> + {updated, N}; +pgsql_execute_to_odbc({error, Error}) -> {error, Error}; +pgsql_execute_to_odbc(_) -> {updated, undefined}. + + %% == Native MySQL code %% part of init/1 %% Open a database connection to MySQL mysql_connect(Server, Port, DB, Username, Password) -> case p1_mysql_conn:start(binary_to_list(Server), Port, - binary_to_list(Username), binary_to_list(Password), - binary_to_list(DB), fun log/3) + binary_to_list(Username), + binary_to_list(Password), + binary_to_list(DB), fun log/3) of - {ok, Ref} -> - p1_mysql_conn:fetch(Ref, [<<"set names 'utf8';">>], - self()), - {ok, Ref}; - Err -> Err + {ok, Ref} -> + p1_mysql_conn:fetch( + Ref, [<<"set names 'utf8mb4' collate 'utf8mb4_bin';">>], self()), + {ok, Ref}; + Err -> Err end. %% Convert MySQL query result to Erlang ODBC result formalism @@ -634,10 +825,36 @@ mysql_item_to_odbc(Columns, Recs) -> {selected, [element(2, Column) || Column <- Columns], Recs}. to_odbc({selected, Columns, Recs}) -> - {selected, Columns, [tuple_to_list(Rec) || Rec <- Recs]}; + Rows = [lists:map( + fun(I) when is_integer(I) -> + jlib:integer_to_binary(I); + (B) -> + B + end, Row) || Row <- Recs], + {selected, [list_to_binary(C) || C <- Columns], Rows}; +to_odbc({error, Reason}) when is_list(Reason) -> + {error, list_to_binary(Reason)}; to_odbc(Res) -> Res. +get_db_version(#state{db_type = pgsql} = State) -> + case pgsql:squery(State#state.db_ref, + <<"select current_setting('server_version_num')">>) of + {ok, [{_, _, [[SVersion]]}]} -> + case catch binary_to_integer(SVersion) of + Version when is_integer(Version) -> + State#state{db_version = Version}; + Error -> + ?WARNING_MSG("error getting pgsql version: ~p", [Error]), + State + end; + Res -> + ?WARNING_MSG("error getting pgsql version: ~p", [Res]), + State + end; +get_db_version(State) -> + State. + log(Level, Format, Args) -> case Level of debug -> ?DEBUG(Format, Args); @@ -650,6 +867,7 @@ db_opts(Host) -> fun(mysql) -> mysql; (pgsql) -> pgsql; (sqlite) -> sqlite; + (mssql) -> mssql; (odbc) -> odbc end, odbc), Server = ejabberd_config:get_option({odbc_server, Host}, @@ -665,6 +883,7 @@ db_opts(Host) -> {odbc_port, Host}, fun(P) when is_integer(P), P > 0, P < 65536 -> P end, case Type of + mssql -> ?MSSQL_PORT; mysql -> ?MYSQL_PORT; pgsql -> ?PGSQL_PORT end), @@ -677,9 +896,80 @@ db_opts(Host) -> Pass = ejabberd_config:get_option({odbc_password, Host}, fun iolist_to_binary/1, <<"">>), - [Type, Server, Port, DB, User, Pass] + case Type of + mssql -> + [odbc, <<"DSN=", Host/binary, ";UID=", User/binary, + ";PWD=", Pass/binary>>]; + _ -> + [Type, Server, Port, DB, User, Pass] + end end. +init_mssql(Host) -> + Server = ejabberd_config:get_option({odbc_server, Host}, + fun iolist_to_binary/1, + <<"localhost">>), + Port = ejabberd_config:get_option( + {odbc_port, Host}, + fun(P) when is_integer(P), P > 0, P < 65536 -> P end, + ?MSSQL_PORT), + DB = ejabberd_config:get_option({odbc_database, Host}, + fun iolist_to_binary/1, + <<"ejabberd">>), + FreeTDS = io_lib:fwrite("[~s]~n" + "\thost = ~s~n" + "\tport = ~p~n" + "\ttds version = 7.1~n", + [Host, Server, Port]), + ODBCINST = io_lib:fwrite("[freetds]~n" + "Description = MSSQL connection~n" + "Driver = libtdsodbc.so~n" + "Setup = libtdsS.so~n" + "UsageCount = 1~n" + "FileUsage = 1~n", []), + ODBCINI = io_lib:fwrite("[~s]~n" + "Description = MS SQL~n" + "Driver = freetds~n" + "Servername = ~s~n" + "Database = ~s~n" + "Port = ~p~n", + [Host, Host, DB, Port]), + ?DEBUG("~s:~n~s", [freetds_config(), FreeTDS]), + ?DEBUG("~s:~n~s", [odbcinst_config(), ODBCINST]), + ?DEBUG("~s:~n~s", [odbc_config(), ODBCINI]), + case filelib:ensure_dir(freetds_config()) of + ok -> + try + ok = file:write_file(freetds_config(), FreeTDS, [append]), + ok = file:write_file(odbcinst_config(), ODBCINST), + ok = file:write_file(odbc_config(), ODBCINI, [append]), + os:putenv("ODBCSYSINI", tmp_dir()), + os:putenv("FREETDS", freetds_config()), + os:putenv("FREETDSCONF", freetds_config()), + ok + catch error:{badmatch, {error, Reason} = Err} -> + ?ERROR_MSG("failed to create temporary files in ~s: ~s", + [tmp_dir(), file:format_error(Reason)]), + Err + end; + {error, Reason} = Err -> + ?ERROR_MSG("failed to create temporary directory ~s: ~s", + [tmp_dir(), file:format_error(Reason)]), + Err + end. + +tmp_dir() -> + filename:join(["/tmp", "ejabberd"]). + +odbc_config() -> + filename:join(tmp_dir(), "odbc.ini"). + +freetds_config() -> + filename:join(tmp_dir(), "freetds.conf"). + +odbcinst_config() -> + filename:join(tmp_dir(), "odbcinst.ini"). + max_fsm_queue() -> ejabberd_config:get_option( max_fsm_queue, @@ -690,3 +980,40 @@ fsm_limit_opts() -> N when is_integer(N) -> [{max_queue, N}]; _ -> [] end. + +check_error({error, Why} = Err, #sql_query{} = Query) -> + ?ERROR_MSG("SQL query '~s' at ~p failed: ~p", + [Query#sql_query.hash, Query#sql_query.loc, Why]), + Err; +check_error({error, Why} = Err, Query) -> + case catch iolist_to_binary(Query) of + SQuery when is_binary(SQuery) -> + ?ERROR_MSG("SQL query '~s' failed: ~p", [SQuery, Why]); + _ -> + ?ERROR_MSG("SQL query ~p failed: ~p", [Query, Why]) + end, + Err; +check_error(Result, _Query) -> + Result. + +opt_type(max_fsm_queue) -> + fun (N) when is_integer(N), N > 0 -> N end; +opt_type(odbc_database) -> fun iolist_to_binary/1; +opt_type(odbc_keepalive_interval) -> + fun (I) when is_integer(I), I > 0 -> I end; +opt_type(odbc_password) -> fun iolist_to_binary/1; +opt_type(odbc_port) -> + fun (P) when is_integer(P), P > 0, P < 65536 -> P end; +opt_type(odbc_server) -> fun iolist_to_binary/1; +opt_type(odbc_type) -> + fun (mysql) -> mysql; + (pgsql) -> pgsql; + (sqlite) -> sqlite; + (mssql) -> mssql; + (odbc) -> odbc + end; +opt_type(odbc_username) -> fun iolist_to_binary/1; +opt_type(_) -> + [max_fsm_queue, odbc_database, odbc_keepalive_interval, + odbc_password, odbc_port, odbc_server, odbc_type, + odbc_username]. diff --git a/src/ejabberd_odbc_sup.erl b/src/ejabberd_odbc_sup.erl index 37128e26..65fa3c1c 100644 --- a/src/ejabberd_odbc_sup.erl +++ b/src/ejabberd_odbc_sup.erl @@ -5,7 +5,7 @@ %%% Created : 22 Dec 2004 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,11 +25,13 @@ -module(ejabberd_odbc_sup). +-behaviour(ejabberd_config). + -author('alexey@process-one.net'). -%% API -export([start_link/1, init/1, add_pid/2, remove_pid/2, - get_pids/1, get_random_pid/1, transform_options/1]). + get_pids/1, get_random_pid/1, transform_options/1, + opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -71,11 +73,14 @@ init([Host]) -> fun(mysql) -> mysql; (pgsql) -> pgsql; (sqlite) -> sqlite; + (mssql) -> mssql; (odbc) -> odbc end, odbc), case Type of sqlite -> check_sqlite_db(Host); + mssql -> + ejabberd_odbc:init_mssql(Host); _ -> ok end, @@ -97,7 +102,7 @@ get_pids(Host) -> get_random_pid(Host) -> case get_pids(Host) of [] -> none; - Pids -> lists:nth(erlang:phash(now(), length(Pids)), Pids) + Pids -> lists:nth(erlang:phash(p1_time_compat:unique_integer(), length(Pids)), Pids) end. add_pid(Host, Pid) -> @@ -205,3 +210,17 @@ read_lines(Fd, File, Acc) -> ?ERROR_MSG("Failed read from lite.sql, reason: ~p", [Err]), [] end. + +opt_type(odbc_pool_size) -> + fun (I) when is_integer(I), I > 0 -> I end; +opt_type(odbc_start_interval) -> + fun (I) when is_integer(I), I > 0 -> I end; +opt_type(odbc_type) -> + fun (mysql) -> mysql; + (pgsql) -> pgsql; + (sqlite) -> sqlite; + (mssql) -> mssql; + (odbc) -> odbc + end; +opt_type(_) -> + [odbc_pool_size, odbc_start_interval, odbc_type]. diff --git a/src/ejabberd_piefxis.erl b/src/ejabberd_piefxis.erl index 641db497..123189dd 100644 --- a/src/ejabberd_piefxis.erl +++ b/src/ejabberd_piefxis.erl @@ -5,11 +5,10 @@ %%% Created : 17 Jul 2008 by Pablo Polvorin <pablo.polvorin@process-one.net> %%%------------------------------------------------------------------- %%% @author Evgeniy Khramtsov <ekhramtsov@process-one.net> -%%% @copyright (C) 2012, Evgeniy Khramtsov %%% @doc %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -38,8 +37,12 @@ -module(ejabberd_piefxis). -%% API --export([import_file/1, export_server/1, export_host/2]). +-behaviour(ejabberd_config). + +-protocol({xep, 227, '1.0'}). + +-export([import_file/1, export_server/1, export_host/2, + opt_type/1]). -define(CHUNK_SIZE, 1024*20). %20k @@ -61,7 +64,7 @@ -define(NS_PIEFXIS, <<"http://www.xmpp.org/extensions/xep-0227.html#ns">>). -define(NS_XI, <<"http://www.w3.org/2001/XInclude">>). --record(state, {xml_stream_state :: xml_stream:xml_stream_state(), +-record(state, {xml_stream_state :: fxml_stream:xml_stream_state(), user = <<"">> :: binary(), server = <<"">> :: binary(), fd :: file:io_device(), @@ -77,12 +80,11 @@ import_file(FileName) -> import_file(FileName, #state{}). -spec import_file(binary(), state()) -> ok | {error, atom()}. - import_file(FileName, State) -> case file:open(FileName, [read, binary]) of {ok, Fd} -> Dir = filename:dirname(FileName), - XMLStreamState = xml_stream:new(self(), infinity), + XMLStreamState = fxml_stream:new(self(), infinity), Res = process(State#state{xml_stream_state = XMLStreamState, fd = Fd, dir = Dir}), @@ -94,72 +96,14 @@ import_file(FileName, State) -> {error, Reason} end. -%%%================================== -%%%% Process Elements -%%%================================== -%%%% Process Element -%%%================================== -%%%% Add user -%% @spec (El::xmlel(), Domain::string(), User::binary(), Password::binary() | none) -%% -> ok | {error, ErrorText::string()} -%% @doc Add a new user to the database. -%% If user already exists, it will be only updated. -spec export_server(binary()) -> any(). - -%% @spec (User::string(), Password::string(), Domain::string()) -%% -> ok | {atomic, exists} | {error, not_allowed} -%% @doc Create a new user export_server(Dir) -> export_hosts(?MYHOSTS, Dir). -%%%================================== -%%%% Populate user -%% @spec (User::string(), Domain::string(), El::xml()) -%% -> ok | {error, not_found} -%% -%% @doc Add a new user from a XML file with a roster list. -%% -%% Example of a file: -%% ``` -%% <?xml version='1.0' encoding='UTF-8'?> -%% <server-data xmlns='http://www.xmpp.org/extensions/xep-0227.html#ns'> -%% <host jid='localhost'> -%% <user name='juliet' password='s3crEt'> -%% <query xmlns='jabber:iq:roster'> -%% <item jid='romeo@montague.net' -%% name='Romeo' -%% subscription='both'> -%% <group>Friends</group> -%% </item> -%% </query> -%% </user> -%% </host> -%% </server-data> -%% ''' -spec export_host(binary(), binary()) -> any(). - export_host(Dir, Host) -> export_hosts([Host], Dir). -%% @spec User = String with the user name -%% Domain = String with a domain name -%% El = Sub XML element with vCard tags values -%% @ret ok | {error, not_found} -%% @doc Read vcards from the XML and send it to the server -%% -%% Example: -%% ``` -%% <?xml version='1.0' encoding='UTF-8'?> -%% <server-data xmlns='http://www.xmpp.org/extensions/xep-0227.html#ns'> -%% <host jid='localhost'> -%% <user name='admin' password='s3crEt'> -%% <vCard xmlns='vcard-temp'> -%% <FN>Admin</FN> -%% </vCard> -%% </user> -%% </host> -%% </server-data> -%% ''' %%%=================================================================== %%% Internal functions %%%=================================================================== @@ -191,11 +135,6 @@ export_hosts(Hosts, Dir) -> {error, Reason} end. -%% @spec User = String with the user name -%% Domain = String with a domain name -%% El = Sub XML element with offline messages values -%% @ret ok | {error, not_found} -%% @doc Read off-line message from the XML and send it to the server export_host(Dir, FnH, Host) -> DFn = make_host_basefilename(Dir, FnH), case file:open(DFn, [raw, write]) of @@ -220,11 +159,6 @@ export_host(Dir, FnH, Host) -> {error, Reason} end. -%% @spec User = String with the user name -%% Domain = String with a domain name -%% El = Sub XML element with private storage values -%% @ret ok | {error, not_found} -%% @doc Private storage parsing export_users([{User, _S}|Users], Server, Fd) -> case export_user(User, Server, Fd) of ok -> @@ -235,11 +169,9 @@ export_users([{User, _S}|Users], Server, Fd) -> export_users([], _Server, _Fd) -> ok. -%%%================================== -%%%% Utilities export_user(User, Server, Fd) -> Password = ejabberd_auth:get_password_s(User, Server), - LServer = jlib:nameprep(Server), + LServer = jid:nameprep(Server), PasswordFormat = ejabberd_config:get_option({auth_password_format, LServer}, fun(X) -> X end, plain), Pass = case Password of {_,_,_,_} -> @@ -254,7 +186,7 @@ export_user(User, Server, Fd) -> get_privacy(User, Server) ++ get_roster(User, Server) ++ get_private(User, Server), - print(Fd, xml:element_to_binary( + print(Fd, fxml:element_to_binary( #xmlel{name = <<"user">>, attrs = [{<<"name">>, User}, {<<"password">>, Pass}], @@ -278,7 +210,7 @@ parse_scram_password(PassData) -> }. get_vcard(User, Server) -> - JID = jlib:make_jid(User, Server, <<>>), + JID = jid:make(User, Server, <<>>), case mod_vcard:process_sm_iq(JID, JID, #iq{type = get}) of #iq{type = result, sub_el = [_|_] = VCardEls} -> VCardEls; @@ -286,7 +218,6 @@ get_vcard(User, Server) -> [] end. -%%%================================== get_offline(User, Server) -> case mod_offline:get_offline_els(User, Server) of [] -> @@ -303,7 +234,6 @@ get_offline(User, Server) -> [#xmlel{name = <<"offline-messages">>, children = NewEls}] end. -%%%% Export hosts get_privacy(User, Server) -> case mod_privacy:get_user_lists(User, Server) of {ok, #privacy{default = Default, @@ -330,9 +260,8 @@ get_privacy(User, Server) -> [] end. -%% @spec (Dir::string(), Hosts::[string()]) -> ok get_roster(User, Server) -> - JID = jlib:make_jid(User, Server, <<>>), + JID = jid:make(User, Server, <<>>), case mod_roster:get_roster(User, Server) of [_|_] = Items -> Subs = @@ -346,8 +275,8 @@ get_roster(User, Server) -> [#xmlel{name = <<"presence">>, attrs = [{<<"from">>, - jlib:jid_to_string(R#roster.jid)}, - {<<"to">>, jlib:jid_to_string(JID)}, + jid:to_string(R#roster.jid)}, + {<<"to">>, jid:to_string(JID)}, {<<"xmlns">>, <<"jabber:client">>}, {<<"type">>, <<"subscribe">>}], children = @@ -384,17 +313,17 @@ get_private(User, Server) -> process(#state{xml_stream_state = XMLStreamState, fd = Fd} = State) -> case file:read(Fd, ?CHUNK_SIZE) of {ok, Data} -> - NewXMLStreamState = xml_stream:parse(XMLStreamState, Data), + NewXMLStreamState = fxml_stream:parse(XMLStreamState, Data), case process_els(State#state{xml_stream_state = NewXMLStreamState}) of {ok, NewState} -> process(NewState); Err -> - xml_stream:close(NewXMLStreamState), + fxml_stream:close(NewXMLStreamState), Err end; eof -> - xml_stream:close(XMLStreamState), + fxml_stream:close(XMLStreamState), ok end. @@ -412,7 +341,7 @@ process_els(State) -> end. process_el({xmlstreamstart, <<"server-data">>, Attrs}, State) -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of + case fxml:get_attr_s(<<"xmlns">>, Attrs) of ?NS_PIEFXIS -> {ok, State}; ?NS_PIE -> @@ -427,7 +356,7 @@ process_el({xmlstreamcdata, _}, State) -> process_el({xmlstreamelement, #xmlel{name = <<"xi:include">>, attrs = Attrs}}, #state{dir = Dir, user = <<"">>} = State) -> - FileName = xml:get_attr_s(<<"href">>, Attrs), + FileName = fxml:get_attr_s(<<"href">>, Attrs), case import_file(filename:join([Dir, FileName]), State) of ok -> {ok, State}; @@ -440,8 +369,8 @@ process_el({xmlstreamstart, <<"host">>, Attrs}, State) -> process_el({xmlstreamelement, #xmlel{name = <<"host">>, attrs = Attrs, children = Els}}, State) -> - JIDS = xml:get_attr_s(<<"jid">>, Attrs), - case jlib:string_to_jid(JIDS) of + JIDS = fxml:get_attr_s(<<"jid">>, Attrs), + case jid:from_string(JIDS) of #jid{lserver = S} -> case lists:member(S, ?MYHOSTS) of true -> @@ -483,8 +412,8 @@ process_users([], State) -> process_user(#xmlel{name = <<"user">>, attrs = Attrs, children = Els}, #state{server = LServer} = State) -> - Name = xml:get_attr_s(<<"name">>, Attrs), - Password = xml:get_attr_s(<<"password">>, Attrs), + Name = fxml:get_attr_s(<<"name">>, Attrs), + Password = fxml:get_attr_s(<<"password">>, Attrs), PasswordFormat = ejabberd_config:get_option({auth_password_format, LServer}, fun(X) -> X end, plain), Pass = case PasswordFormat of scram -> @@ -496,7 +425,7 @@ process_user(#xmlel{name = <<"user">>, attrs = Attrs, children = Els}, _ -> Password end, - case jlib:nodeprep(Name) of + case jid:nodeprep(Name) of error -> stop("Invalid 'user': ~s", [Name]); LUser -> @@ -522,7 +451,7 @@ process_user_els([], State) -> process_user_el(#xmlel{name = Name, attrs = Attrs, children = Els} = El, State) -> - case {Name, xml:get_attr_s(<<"xmlns">>, Attrs)} of + case {Name, fxml:get_attr_s(<<"xmlns">>, Attrs)} of {<<"query">>, ?NS_ROSTER} -> process_roster(El, State); {<<"query">>, ?NS_PRIVACY} -> @@ -573,15 +502,13 @@ process_roster(El, State = #state{user = U, server = S}) -> stop("Failed to write roster: ~p", [Err]) end. -%%%================================== -%%%% Export server process_privacy(El, State = #state{user = U, server = S}) -> - JID = jlib:make_jid(U, S, <<"">>), + JID = jid:make(U, S, <<"">>), case mod_privacy:process_iq_set( [], JID, JID, #iq{type = set, sub_el = El}) of {error, Error} = Err -> #xmlel{children = Els} = El, - Name = case xml:remove_cdata(Els) of + Name = case fxml:remove_cdata(Els) of [#xmlel{name = N}] -> N; _ -> undefined end, @@ -600,9 +527,8 @@ process_privacy(El, State = #state{user = U, server = S}) -> {ok, State} end. -%% @spec (Dir::string()) -> ok process_private(El, State = #state{user = U, server = S}) -> - JID = jlib:make_jid(U, S, <<"">>), + JID = jid:make(U, S, <<"">>), case mod_private:process_sm_iq( JID, JID, #iq{type = set, sub_el = El}) of #iq{type = result} -> @@ -611,10 +537,8 @@ process_private(El, State = #state{user = U, server = S}) -> stop("Failed to write private: ~p", [Err]) end. -%%%================================== -%%%% Export host process_vcard(El, State = #state{user = U, server = S}) -> - JID = jlib:make_jid(U, S, <<"">>), + JID = jid:make(U, S, <<"">>), case mod_vcard:process_sm_iq( JID, JID, #iq{type = set, sub_el = El}) of #iq{type = result} -> @@ -623,12 +547,11 @@ process_vcard(El, State = #state{user = U, server = S}) -> stop("Failed to write vcard: ~p", [Err]) end. -%% @spec (Dir::string(), Host::string()) -> ok process_offline_msg(El, State = #state{user = U, server = S}) -> - FromS = xml:get_attr_s(<<"from">>, El#xmlel.attrs), - case jlib:string_to_jid(FromS) of + FromS = fxml:get_attr_s(<<"from">>, El#xmlel.attrs), + case jid:from_string(FromS) of #jid{} = From -> - To = jlib:make_jid(U, S, <<>>), + To = jid:make(U, S, <<>>), NewEl = jlib:replace_from_to(From, To, El), case catch mod_offline:store_packet(From, To, NewEl) of {'EXIT', _} = Err -> @@ -640,12 +563,11 @@ process_offline_msg(El, State = #state{user = U, server = S}) -> stop("Invalid 'from' = ~s", [FromS]) end. -%% @spec (Dir::string(), Fn::string(), Host::string()) -> ok process_presence(El, #state{user = U, server = S} = State) -> - FromS = xml:get_attr_s(<<"from">>, El#xmlel.attrs), - case jlib:string_to_jid(FromS) of + FromS = fxml:get_attr_s(<<"from">>, El#xmlel.attrs), + case jid:from_string(FromS) of #jid{} = From -> - To = jlib:make_jid(U, S, <<>>), + To = jid:make(U, S, <<>>), NewEl = jlib:replace_from_to(From, To, El), ejabberd_router:route(From, To, NewEl), {ok, State}; @@ -710,28 +632,9 @@ make_xinclude(Fn) -> Base = filename:basename(Fn), io_lib:format("<xi:include href='~s'/>", [Base]). -%%%================================== -%%%% Export user -%% @spec (Fd, Username::string(), Host::string()) -> ok -%% @doc Extract user information and print it. -%% @spec (Username::string(), Host::string()) -> string() -%% @spec (InfoName::atom(), Username::string(), Host::string()) -> string() -%%%================================== -%%%% Interface with ejabberd offline storage -%% Copied from mod_offline.erl and customized -%%%================================== -%%%% Interface with ejabberd private storage -%%%================================== -%%%% Disk file access -%% @spec () -> string() -%% @spec (Dir::string(), FnT::string()) -> string() -%% @spec (FnT::string(), Host::string()) -> FnH::string() -%% @doc Make the filename for the host. -%% Example: ``("20080804-231550", "jabber.example.org") -> "20080804-231550_jabber_example_org.xml"'' -%% @spec (Fn::string()) -> {ok, Fd} -%% @spec (Fd) -> ok -%% @spec (Fd, String::string()) -> ok print(Fd, String) -> -%%%================================== -%%% vim: set filetype=erlang tabstop=8 foldmarker=%%%%,%%%= foldmethod=marker: file:write(Fd, String). + +opt_type(auth_password_format) -> fun (X) -> X end; +opt_type(_) -> [auth_password_format]. + diff --git a/src/ejabberd_rdbms.erl b/src/ejabberd_rdbms.erl index 93bd9fc4..ae405db0 100644 --- a/src/ejabberd_rdbms.erl +++ b/src/ejabberd_rdbms.erl @@ -5,7 +5,7 @@ %%% Created : 31 Jan 2003 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,14 +25,19 @@ -module(ejabberd_rdbms). +-behaviour(ejabberd_config). + -author('alexey@process-one.net'). --export([start/0]). +-export([start/0, opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). start() -> + file:delete(ejabberd_odbc:freetds_config()), + file:delete(ejabberd_odbc:odbc_config()), + file:delete(ejabberd_odbc:odbcinst_config()), case lists:any(fun(H) -> needs_odbc(H) /= false end, ?MYHOSTS) of true -> @@ -70,16 +75,27 @@ start_odbc(Host, App) -> %% Returns {true, App} if we have configured odbc for the given host needs_odbc(Host) -> - LHost = jlib:nameprep(Host), + LHost = jid:nameprep(Host), case ejabberd_config:get_option({odbc_type, LHost}, fun(mysql) -> mysql; (pgsql) -> pgsql; (sqlite) -> sqlite; + (mssql) -> mssql; (odbc) -> odbc end, undefined) of mysql -> {true, p1_mysql}; pgsql -> {true, p1_pgsql}; sqlite -> {true, sqlite3}; + mssql -> {true, odbc}; odbc -> {true, odbc}; undefined -> false end. + +opt_type(odbc_type) -> + fun (mysql) -> mysql; + (pgsql) -> pgsql; + (sqlite) -> sqlite; + (mssql) -> mssql; + (odbc) -> odbc + end; +opt_type(_) -> [odbc_type]. diff --git a/src/ejabberd_receiver.erl b/src/ejabberd_receiver.erl index f63ae1cc..0a33e30e 100644 --- a/src/ejabberd_receiver.erl +++ b/src/ejabberd_receiver.erl @@ -5,7 +5,7 @@ %%% Created : 10 Nov 2003 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -48,23 +48,17 @@ -include("logger.hrl"). -record(state, - {socket :: inet:socket() | p1_tls:tls_socket() | ezlib:zlib_socket(), - sock_mod = gen_tcp :: gen_tcp | p1_tls | ezlib, + {socket :: inet:socket() | fast_tls:tls_socket() | ezlib:zlib_socket(), + sock_mod = gen_tcp :: gen_tcp | fast_tls | ezlib, shaper_state = none :: shaper:shaper(), c2s_pid :: pid(), max_stanza_size = infinity :: non_neg_integer() | infinity, - xml_stream_state :: xml_stream:xml_stream_state(), + xml_stream_state :: fxml_stream:xml_stream_state(), timeout = infinity:: timeout()}). --define(HIBERNATE_TIMEOUT, 90000). +-define(HIBERNATE_TIMEOUT, ejabberd_config:get_option(receiver_hibernate, fun(X) when is_integer(X); X == hibernate-> X end, 90000)). + -%%==================================================================== -%% API -%%==================================================================== -%%-------------------------------------------------------------------- -%% Function: start_link() -> {ok,Pid} | ignore | {error,Error} -%% Description: Starts the server -%%-------------------------------------------------------------------- -spec start_link(inet:socket(), atom(), shaper:shaper(), non_neg_integer() | infinity) -> ignore | {error, any()} | @@ -76,10 +70,6 @@ start_link(Socket, SockMod, Shaper, MaxStanzaSize) -> -spec start(inet:socket(), atom(), shaper:shaper()) -> undefined | pid(). -%%-------------------------------------------------------------------- -%% Function: start() -> {ok,Pid} | ignore | {error,Error} -%% Description: Starts the server -%%-------------------------------------------------------------------- start(Socket, SockMod, Shaper) -> start(Socket, SockMod, Shaper, infinity). @@ -87,9 +77,8 @@ start(Socket, SockMod, Shaper) -> non_neg_integer() | infinity) -> undefined | pid(). start(Socket, SockMod, Shaper, MaxStanzaSize) -> - {ok, Pid} = - supervisor:start_child(ejabberd_receiver_sup, - [Socket, SockMod, Shaper, MaxStanzaSize]), + {ok, Pid} = gen_server:start(ejabberd_receiver, + [Socket, SockMod, Shaper, MaxStanzaSize], []), Pid. -spec change_shaper(pid(), shaper:shaper()) -> ok. @@ -101,7 +90,7 @@ change_shaper(Pid, Shaper) -> reset_stream(Pid) -> do_call(Pid, reset_stream). --spec starttls(pid(), p1_tls:tls_socket()) -> ok. +-spec starttls(pid(), fast_tls:tls_socket()) -> ok. starttls(Pid, TLSSocket) -> do_call(Pid, {starttls, TLSSocket}). @@ -127,13 +116,6 @@ close(Pid) -> %% gen_server callbacks %%==================================================================== -%%-------------------------------------------------------------------- -%% Function: init(Args) -> {ok, State} | -%% {ok, State, Timeout} | -%% ignore | -%% {stop, Reason} -%% Description: Initiates the server -%%-------------------------------------------------------------------- init([Socket, SockMod, Shaper, MaxStanzaSize]) -> ShaperState = shaper:new(Shaper), Timeout = case SockMod of @@ -145,66 +127,42 @@ init([Socket, SockMod, Shaper, MaxStanzaSize]) -> shaper_state = ShaperState, max_stanza_size = MaxStanzaSize, timeout = Timeout}}. -%%-------------------------------------------------------------------- -%% 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({starttls, TLSSocket}, _From, - #state{xml_stream_state = XMLStreamState, - c2s_pid = C2SPid, - max_stanza_size = MaxStanzaSize} = State) -> - close_stream(XMLStreamState), - NewXMLStreamState = xml_stream:new(C2SPid, - MaxStanzaSize), - NewState = State#state{socket = TLSSocket, - sock_mod = p1_tls, - xml_stream_state = NewXMLStreamState}, - case p1_tls:recv_data(TLSSocket, <<"">>) of +handle_call({starttls, TLSSocket}, _From, State) -> + State1 = reset_parser(State), + NewState = State1#state{socket = TLSSocket, + sock_mod = fast_tls}, + case fast_tls:recv_data(TLSSocket, <<"">>) of {ok, TLSData} -> - {reply, ok, process_data(TLSData, NewState), ?HIBERNATE_TIMEOUT}; + {reply, ok, + process_data(TLSData, NewState), ?HIBERNATE_TIMEOUT}; {error, _Reason} -> {stop, normal, ok, NewState} end; handle_call({compress, Data}, _From, - #state{xml_stream_state = XMLStreamState, - c2s_pid = C2SPid, socket = Socket, sock_mod = SockMod, - max_stanza_size = MaxStanzaSize} = + #state{socket = Socket, sock_mod = SockMod} = State) -> + ejabberd:start_app(ezlib), {ok, ZlibSocket} = ezlib:enable_zlib(SockMod, Socket), if Data /= undefined -> do_send(State, Data); true -> ok end, - close_stream(XMLStreamState), - NewXMLStreamState = xml_stream:new(C2SPid, - MaxStanzaSize), - NewState = State#state{socket = ZlibSocket, - sock_mod = ezlib, - xml_stream_state = NewXMLStreamState}, + State1 = reset_parser(State), + NewState = State1#state{socket = ZlibSocket, + sock_mod = ezlib}, case ezlib:recv_data(ZlibSocket, <<"">>) of {ok, ZlibData} -> - {reply, {ok, ZlibSocket}, - process_data(ZlibData, NewState), ?HIBERNATE_TIMEOUT}; - {error, _Reason} -> {stop, normal, ok, NewState} + {reply, {ok, ZlibSocket}, + process_data(ZlibData, NewState), ?HIBERNATE_TIMEOUT}; + {error, _Reason} -> + {stop, normal, ok, NewState} end; -handle_call(reset_stream, _From, - #state{xml_stream_state = XMLStreamState, - c2s_pid = C2SPid, max_stanza_size = MaxStanzaSize} = - State) -> - close_stream(XMLStreamState), - NewXMLStreamState = xml_stream:new(C2SPid, - MaxStanzaSize), +handle_call(reset_stream, _From, State) -> + NewState = reset_parser(State), Reply = ok, - {reply, Reply, - State#state{xml_stream_state = NewXMLStreamState}, - ?HIBERNATE_TIMEOUT}; + {reply, Reply, NewState, ?HIBERNATE_TIMEOUT}; handle_call({become_controller, C2SPid}, _From, State) -> - XMLStreamState = xml_stream:new(C2SPid, State#state.max_stanza_size), + XMLStreamState = fxml_stream:new(C2SPid, State#state.max_stanza_size), NewState = State#state{c2s_pid = C2SPid, xml_stream_state = XMLStreamState}, activate_socket(NewState), @@ -213,12 +171,6 @@ handle_call({become_controller, C2SPid}, _From, State) -> handle_call(_Request, _From, State) -> Reply = ok, {reply, Reply, State, ?HIBERNATE_TIMEOUT}. -%%-------------------------------------------------------------------- -%% Function: handle_cast(Msg, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling cast messages -%%-------------------------------------------------------------------- handle_cast({change_shaper, Shaper}, State) -> NewShaperState = shaper:new(Shaper), {noreply, State#state{shaper_state = NewShaperState}, @@ -227,25 +179,19 @@ handle_cast(close, State) -> {stop, normal, State}; handle_cast(_Msg, State) -> {noreply, State, ?HIBERNATE_TIMEOUT}. -%%-------------------------------------------------------------------- -%% Function: handle_info(Info, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling all non call/cast messages -%%-------------------------------------------------------------------- handle_info({Tag, _TCPSocket, Data}, #state{socket = Socket, sock_mod = SockMod} = State) when (Tag == tcp) or (Tag == ssl) or (Tag == ejabberd_xml) -> case SockMod of - p1_tls -> - case p1_tls:recv_data(Socket, Data) of + fast_tls -> + case fast_tls:recv_data(Socket, Data) of {ok, TLSData} -> {noreply, process_data(TLSData, State), ?HIBERNATE_TIMEOUT}; {error, Reason} -> if is_binary(Reason) -> - ?ERROR_MSG("TLS error = ~s", [Reason]); + ?DEBUG("TLS error = ~s", [Reason]); true -> ok end, @@ -280,13 +226,6 @@ handle_info(timeout, State) -> handle_info(_Info, State) -> {noreply, State, ?HIBERNATE_TIMEOUT}. -%%-------------------------------------------------------------------- -%% 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{xml_stream_state = XMLStreamState, c2s_pid = C2SPid} = @@ -299,10 +238,6 @@ terminate(_Reason, catch (State#state.sock_mod):close(State#state.socket), ok. -%%-------------------------------------------------------------------- -%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} -%% Description: Convert process state when code is changed -%%-------------------------------------------------------------------- code_change(_OldVsn, State, _Extra) -> {ok, State}. %%-------------------------------------------------------------------- @@ -347,7 +282,12 @@ process_data(Data, shaper_state = ShaperState, c2s_pid = C2SPid} = State) -> ?DEBUG("Received XML on stream = ~p", [(Data)]), - XMLStreamState1 = xml_stream:parse(XMLStreamState, Data), + XMLStreamState1 = case XMLStreamState of + undefined -> + XMLStreamState; + _ -> + fxml_stream:parse(XMLStreamState, Data) + end, {NewShaperState, Pause} = shaper:update(ShaperState, byte_size(Data)), if C2SPid == undefined -> @@ -371,7 +311,25 @@ element_wrapper(Element) -> Element. close_stream(undefined) -> ok; close_stream(XMLStreamState) -> - xml_stream:close(XMLStreamState). + fxml_stream:close(XMLStreamState). + +reset_parser(#state{xml_stream_state = undefined} = State) -> + State; +reset_parser(#state{c2s_pid = C2SPid, + max_stanza_size = MaxStanzaSize, + xml_stream_state = XMLStreamState} + = State) -> + NewStreamState = try fxml_stream:reset(XMLStreamState) + catch error:_ -> + close_stream(XMLStreamState), + case C2SPid of + undefined -> + undefined; + _ -> + fxml_stream:new(C2SPid, MaxStanzaSize) + end + end, + State#state{xml_stream_state = NewStreamState}. do_send(State, Data) -> (State#state.sock_mod):send(State#state.socket, Data). diff --git a/src/ejabberd_regexp.erl b/src/ejabberd_regexp.erl index 86ebd184..b79774e3 100644 --- a/src/ejabberd_regexp.erl +++ b/src/ejabberd_regexp.erl @@ -5,7 +5,7 @@ %%% Created : 8 Dec 2011 by Badlop %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_riak.erl b/src/ejabberd_riak.erl index c8084674..575810ac 100644 --- a/src/ejabberd_riak.erl +++ b/src/ejabberd_riak.erl @@ -4,7 +4,9 @@ %%% Interface for Riak database %%% @end %%% Created : 29 Dec 2011 by Alexey Shchepin <alexey@process-one.net> -%%% @copyright (C) 2002-2015 ProcessOne +%%% @copyright (C) 2002-2016 ProcessOne +%%% +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -26,7 +28,7 @@ -behaviour(gen_server). %% API --export([start_link/4, get_proc/1, make_bucket/1, put/2, put/3, +-export([start_link/5, get_proc/1, make_bucket/1, put/2, put/3, get/2, get/3, get_by_index/4, delete/1, delete/2, count_by_index/3, get_by_index_range/5, get_keys/1, get_keys_by_index/3, is_connected/0, @@ -66,12 +68,20 @@ %%% API %%%=================================================================== %% @private -start_link(Num, Server, Port, _StartInterval) -> - gen_server:start_link({local, get_proc(Num)}, ?MODULE, [Server, Port], []). +start_link(Num, Server, Port, _StartInterval, Options) -> + gen_server:start_link({local, get_proc(Num)}, ?MODULE, [Server, Port, Options], []). %% @private is_connected() -> - catch riakc_pb_socket:is_connected(get_random_pid()). + lists:all( + fun({_Id, Pid, _Type, _Modules}) when is_pid(Pid) -> + case catch riakc_pb_socket:is_connected(get_riak_pid(Pid)) of + true -> true; + _ -> false + end; + (_) -> + false + end, supervisor:which_children(ejabberd_riak_sup)). %% @private get_proc(I) -> @@ -427,10 +437,8 @@ map_key(Obj, _, _) -> %%% gen_server API %%%=================================================================== %% @private -init([Server, Port]) -> - case riakc_pb_socket:start( - Server, Port, - [auto_reconnect]) of +init([Server, Port, Options]) -> + case riakc_pb_socket:start(Server, Port, Options) of {ok, Pid} -> erlang:monitor(process, Pid), {ok, #state{pid = Pid}}; @@ -515,6 +523,9 @@ make_invalid_object(Val) -> get_random_pid() -> PoolPid = ejabberd_riak_sup:get_random_pid(), + get_riak_pid(PoolPid). + +get_riak_pid(PoolPid) -> case catch gen_server:call(PoolPid, get_pid) of {ok, Pid} -> Pid; diff --git a/src/ejabberd_riak_sup.erl b/src/ejabberd_riak_sup.erl index bb36eb44..af811441 100644 --- a/src/ejabberd_riak_sup.erl +++ b/src/ejabberd_riak_sup.erl @@ -5,7 +5,7 @@ %%% Created : 29 Dec 2011 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -24,17 +24,13 @@ %%%---------------------------------------------------------------------- -module(ejabberd_riak_sup). + +-behaviour(ejabberd_config). -author('alexey@process-one.net'). -%% API --export([start/0, - start_link/0, - init/1, - get_pids/0, - transform_options/1, - get_random_pid/0, - get_random_pid/1 - ]). +-export([start/0, start_link/0, init/1, get_pids/0, + transform_options/1, get_random_pid/0, get_random_pid/1, + opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -107,12 +103,27 @@ init([]) -> StartInterval = get_start_interval(), Server = get_riak_server(), Port = get_riak_port(), + CACertFile = get_riak_cacertfile(), + Username = get_riak_username(), + Password = get_riak_password(), + Options = lists:filter( + fun(X) -> X /= nil end, + [auto_reconnect, + {keepalive, true}, + if CACertFile /= nil -> {cacertfile ,CACertFile}; + true -> nil + end, + if (Username /= nil) and (Password /= nil) -> + {credentials, Username, Password}; + true -> nil + end + ]), {ok, {{one_for_one, PoolSize*10, 1}, lists:map( fun(I) -> {ejabberd_riak:get_proc(I), {ejabberd_riak, start_link, - [I, Server, Port, StartInterval*1000]}, + [I, Server, Port, StartInterval*1000, Options]}, transient, 2000, worker, [?MODULE]} end, lists:seq(1, PoolSize))}}. @@ -135,6 +146,27 @@ get_riak_server() -> binary_to_list(iolist_to_binary(S)) end, ?DEFAULT_RIAK_HOST). +get_riak_cacertfile() -> + ejabberd_config:get_option( + riak_cacertfile, + fun(S) -> + binary_to_list(iolist_to_binary(S)) + end, nil). + +get_riak_username() -> + ejabberd_config:get_option( + riak_username, + fun(S) -> + binary_to_list(iolist_to_binary(S)) + end, nil). + +get_riak_password() -> + ejabberd_config:get_option( + riak_password, + fun(S) -> + binary_to_list(iolist_to_binary(S)) + end, nil). + get_riak_port() -> ejabberd_config:get_option( riak_port, @@ -145,7 +177,7 @@ get_pids() -> [ejabberd_riak:get_proc(I) || I <- lists:seq(1, get_pool_size())]. get_random_pid() -> - get_random_pid(now()). + get_random_pid(p1_time_compat:monotonic_time()). get_random_pid(Term) -> I = erlang:phash2(Term, get_pool_size()) + 1, @@ -158,3 +190,17 @@ transform_options({riak_server, {S, P}}, Opts) -> [{riak_server, S}, {riak_port, P}|Opts]; transform_options(Opt, Opts) -> [Opt|Opts]. + +opt_type(modules) -> fun (L) when is_list(L) -> L end; +opt_type(riak_pool_size) -> + fun (N) when is_integer(N), N >= 1 -> N end; +opt_type(riak_port) -> fun (_) -> true end; +opt_type(riak_server) -> fun (_) -> true end; +opt_type(riak_start_interval) -> + fun (N) when is_integer(N), N >= 1 -> N end; +opt_type(riak_cacertfile) -> fun iolist_to_binary/1; +opt_type(riak_username) -> fun iolist_to_binary/1; +opt_type(riak_password) -> fun iolist_to_binary/1; +opt_type(_) -> + [modules, riak_pool_size, riak_port, riak_server, + riak_start_interval, riak_cacertfile, riak_username, riak_password]. diff --git a/src/ejabberd_router.erl b/src/ejabberd_router.erl index 76ef71dc..e29d6acf 100644 --- a/src/ejabberd_router.erl +++ b/src/ejabberd_router.erl @@ -5,7 +5,7 @@ %%% Created : 27 Nov 2002 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,6 +25,8 @@ -module(ejabberd_router). +-behaviour(ejabberd_config). + -author('alexey@process-one.net'). -behaviour(gen_server). @@ -34,7 +36,9 @@ route_error/4, register_route/1, register_route/2, + register_route/3, register_routes/1, + host_of_route/1, unregister_route/1, unregister_routes/1, dirty_get_all_routes/0, @@ -43,9 +47,8 @@ -export([start_link/0]). -%% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, - handle_info/2, terminate/2, code_change/3]). + handle_info/2, terminate/2, code_change/3, opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -54,7 +57,7 @@ -type local_hint() :: undefined | integer() | {apply, atom(), atom()}. --record(route, {domain, pid, local_hint}). +-record(route, {domain, server_host, pid, local_hint}). -record(state, {}). @@ -85,7 +88,7 @@ route(From, To, Packet) -> route_error(From, To, ErrPacket, OrigPacket) -> #xmlel{attrs = Attrs} = OrigPacket, - case <<"error">> == xml:get_attr_s(<<"type">>, Attrs) of + case <<"error">> == fxml:get_attr_s(<<"type">>, Attrs) of false -> route(From, To, ErrPacket); true -> ok end. @@ -93,19 +96,29 @@ route_error(From, To, ErrPacket, OrigPacket) -> -spec register_route(binary()) -> term(). register_route(Domain) -> - register_route(Domain, undefined). + ?WARNING_MSG("~s:register_route/1 is deprected, " + "use ~s:register_route/2 instead", + [?MODULE, ?MODULE]), + register_route(Domain, ?MYNAME). --spec register_route(binary(), local_hint()) -> term(). +-spec register_route(binary(), binary()) -> term(). -register_route(Domain, LocalHint) -> - case jlib:nameprep(Domain) of - error -> erlang:error({invalid_domain, Domain}); - LDomain -> +register_route(Domain, ServerHost) -> + register_route(Domain, ServerHost, undefined). + +-spec register_route(binary(), binary(), local_hint()) -> term(). + +register_route(Domain, ServerHost, LocalHint) -> + case {jid:nameprep(Domain), jid:nameprep(ServerHost)} of + {error, _} -> erlang:error({invalid_domain, Domain}); + {_, error} -> erlang:error({invalid_domain, ServerHost}); + {LDomain, LServerHost} -> Pid = self(), case get_component_number(LDomain) of undefined -> F = fun () -> mnesia:write(#route{domain = LDomain, pid = Pid, + server_host = LServerHost, local_hint = LocalHint}) end, mnesia:transaction(F); @@ -114,53 +127,49 @@ register_route(Domain, LocalHint) -> case mnesia:wread({route, LDomain}) of [] -> mnesia:write(#route{domain = LDomain, + server_host = LServerHost, pid = Pid, local_hint = 1}), - lists:foreach(fun (I) -> - mnesia:write(#route{domain - = - LDomain, - pid - = - undefined, - local_hint - = - I}) - end, - lists:seq(2, N)); + lists:foreach( + fun (I) -> + mnesia:write( + #route{domain = LDomain, + pid = undefined, + server_host = LServerHost, + local_hint = I}) + end, + lists:seq(2, N)); Rs -> - lists:any(fun (#route{pid = undefined, - local_hint = I} = - R) -> - mnesia:write(#route{domain = - LDomain, - pid = - Pid, - local_hint - = - I}), - mnesia:delete_object(R), - true; - (_) -> false - end, - Rs) + lists:any( + fun (#route{pid = undefined, + local_hint = I} = R) -> + mnesia:write( + #route{domain = LDomain, + pid = Pid, + server_host = LServerHost, + local_hint = I}), + mnesia:delete_object(R), + true; + (_) -> false + end, + Rs) end end, mnesia:transaction(F) end end. --spec register_routes([binary()]) -> ok. +-spec register_routes([{binary(), binary()}]) -> ok. register_routes(Domains) -> - lists:foreach(fun (Domain) -> register_route(Domain) + lists:foreach(fun ({Domain, ServerHost}) -> register_route(Domain, ServerHost) end, Domains). -spec unregister_route(binary()) -> term(). unregister_route(Domain) -> - case jlib:nameprep(Domain) of + case jid:nameprep(Domain) of error -> erlang:error({invalid_domain, Domain}); LDomain -> Pid = self(), @@ -182,7 +191,9 @@ unregister_route(Domain) -> of [R] -> I = R#route.local_hint, + ServerHost = R#route.server_host, mnesia:write(#route{domain = LDomain, + server_host = ServerHost, pid = undefined, local_hint = I}), mnesia:delete_object(R); @@ -210,6 +221,20 @@ dirty_get_all_routes() -> dirty_get_all_domains() -> lists:usort(mnesia:dirty_all_keys(route)). +-spec host_of_route(binary()) -> binary(). + +host_of_route(Domain) -> + case jid:nameprep(Domain) of + error -> + erlang:error({invalid_domain, Domain}); + LDomain -> + case mnesia:dirty_read(route, LDomain) of + [#route{server_host = ServerHost}|_] -> + ServerHost; + [] -> + erlang:error({unregistered_route, Domain}) + end + end. %%==================================================================== %% gen_server callbacks @@ -225,7 +250,8 @@ dirty_get_all_domains() -> init([]) -> update_tables(), mnesia:create_table(route, - [{ram_copies, [node()]}, {type, bag}, + [{ram_copies, [node()]}, + {type, bag}, {attributes, record_info(fields, route)}]), mnesia:add_table_copy(route, node(), ram_copies), mnesia:subscribe({table, route, simple}), @@ -281,8 +307,11 @@ handle_info({'DOWN', _Ref, _Type, Pid, _Info}, State) -> if is_integer(E#route.local_hint) -> LDomain = E#route.domain, I = E#route.local_hint, + ServerHost = E#route.server_host, mnesia:write(#route{domain = LDomain, + server_host = + ServerHost, pid = undefined, local_hint = @@ -342,17 +371,17 @@ do_route(OrigFrom, OrigTo, OrigPacket) -> end; Rs -> Value = case - ejabberd_config:get_local_option({domain_balancing, - LDstDomain}, fun(D) when is_atom(D) -> D end) + ejabberd_config:get_option({domain_balancing, + LDstDomain}, fun(D) when is_atom(D) -> D end) of - undefined -> now(); - random -> now(); - source -> jlib:jid_tolower(From); - destination -> jlib:jid_tolower(To); + undefined -> p1_time_compat:monotonic_time(); + random -> p1_time_compat:monotonic_time(); + source -> jid:tolower(From); + destination -> jid:tolower(To); bare_source -> - jlib:jid_remove_resource(jlib:jid_tolower(From)); + jid:remove_resource(jid:tolower(From)); bare_destination -> - jlib:jid_remove_resource(jlib:jid_tolower(To)) + jid:remove_resource(jid:tolower(To)) end, case get_component_number(LDstDomain) of undefined -> @@ -392,12 +421,10 @@ get_component_number(LDomain) -> undefined). update_tables() -> - case catch mnesia:table_info(route, attributes) of - [domain, node, pid] -> mnesia:delete_table(route); - [domain, pid] -> mnesia:delete_table(route); - [domain, pid, local_hint] -> ok; - [domain, pid, local_hint|_] -> mnesia:delete_table(route); - {'EXIT', _} -> ok + try + mnesia:transform_table(route, ignore, record_info(fields, route)) + catch exit:{aborted, {no_exists, _}} -> + ok end, case lists:member(local_route, mnesia:system_info(tables)) @@ -406,3 +433,13 @@ update_tables() -> false -> ok end. +opt_type(domain_balancing) -> + fun (random) -> random; + (source) -> source; + (destination) -> destination; + (bare_source) -> bare_source; + (bare_destination) -> bare_destination + end; +opt_type(domain_balancing_component_number) -> + fun (N) when is_integer(N), N > 1 -> N end; +opt_type(_) -> [domain_balancing, domain_balancing_component_number]. diff --git a/src/ejabberd_router_multicast.erl b/src/ejabberd_router_multicast.erl index 9cd7dd3d..fa32c8ed 100644 --- a/src/ejabberd_router_multicast.erl +++ b/src/ejabberd_router_multicast.erl @@ -3,6 +3,24 @@ %%% Author : Badlop <badlop@process-one.net> %%% Purpose : Multicast router %%% Created : 11 Aug 2007 by Badlop <badlop@process-one.net> +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2016 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_router_multicast). @@ -51,7 +69,7 @@ route_multicast(From, Domain, Destinations, Packet) -> end. register_route(Domain) -> - case jlib:nameprep(Domain) of + case jid:nameprep(Domain) of error -> erlang:error({invalid_domain, Domain}); LDomain -> @@ -64,7 +82,7 @@ register_route(Domain) -> end. unregister_route(Domain) -> - case jlib:nameprep(Domain) of + case jid:nameprep(Domain) of error -> erlang:error({invalid_domain, Domain}); LDomain -> @@ -191,9 +209,9 @@ code_change(_OldVsn, State, _Extra) -> do_route(From, Domain, Destinations, Packet) -> ?DEBUG("route_multicast~n\tfrom ~s~n\tdomain ~s~n\tdestinations ~p~n\tpacket ~p~n", - [jlib:jid_to_string(From), + [jid:to_string(From), Domain, - [jlib:jid_to_string(To) || To <- Destinations], + [jid:to_string(To) || To <- Destinations], Packet]), %% Try to find an appropriate multicast service @@ -202,14 +220,18 @@ do_route(From, Domain, Destinations, Packet) -> %% If no multicast service is available in this server, send manually [] -> do_route_normal(From, Destinations, Packet); - %% If available, send the packet using multicast service - [R] -> - case R#route_multicast.pid of - Pid when is_pid(Pid) -> - Pid ! {route_trusted, From, Destinations, Packet}; - _ -> do_route_normal(From, Destinations, Packet) - end + %% If some is available, send the packet using multicast service + Rs when is_list(Rs) -> + Pid = pick_multicast_pid(Rs), + Pid ! {route_trusted, From, Destinations, Packet} end. +pick_multicast_pid(Rs) -> + List = case [R || R <- Rs, node(R#route_multicast.pid) == node()] of + [] -> Rs; + RLocals -> RLocals + end, + (hd(List))#route_multicast.pid. + do_route_normal(From, Destinations, Packet) -> [ejabberd_router:route(From, To, Packet) || To <- Destinations]. diff --git a/src/ejabberd_s2s.erl b/src/ejabberd_s2s.erl index a7b3234f..0eab4633 100644 --- a/src/ejabberd_s2s.erl +++ b/src/ejabberd_s2s.erl @@ -5,7 +5,7 @@ %%% Created : 7 Dec 2002 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,27 +25,32 @@ -module(ejabberd_s2s). +-protocol({xep, 220, '1.1'}). + +-behaviour(ejabberd_config). + -author('alexey@process-one.net'). -behaviour(gen_server). %% API -export([start_link/0, route/3, have_connection/1, - has_key/2, get_connections_pids/1, try_register/1, - remove_connection/3, find_connection/2, + make_key/2, get_connections_pids/1, try_register/1, + remove_connection/2, find_connection/2, dirty_get_connections/0, allow_host/2, incoming_s2s_number/0, outgoing_s2s_number/0, clean_temporarily_blocked_table/0, list_temporarily_blocked_hosts/0, external_host_overloaded/1, is_temporarly_blocked/1, - check_peer_certificate/3]). + check_peer_certificate/3, + get_commands_spec/0]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -%% ejabberd API --export([get_info_s2s_connections/1, transform_options/1]). +-export([get_info_s2s_connections/1, + transform_options/1, opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -71,23 +76,15 @@ %% once a server is temporarly blocked, it stay blocked for 60 seconds -record(s2s, {fromto = {<<"">>, <<"">>} :: {binary(), binary()} | '_', - pid = self() :: pid() | '_' | '$1', - key = <<"">> :: binary() | '_'}). + pid = self() :: pid() | '_' | '$1'}). -record(state, {}). -record(temporarily_blocked, {host = <<"">> :: binary(), - timestamp = now() :: erlang:timestamp()}). + timestamp :: integer()}). -type temporarily_blocked() :: #temporarily_blocked{}. -%%==================================================================== -%% API -%%==================================================================== -%%-------------------------------------------------------------------- -%% Function: start_link() -> {ok,Pid} | ignore | {error,Error} -%% Description: Starts the server -%%-------------------------------------------------------------------- start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). @@ -117,9 +114,9 @@ external_host_overloaded(Host) -> "seconds", [Host, ?S2S_OVERLOAD_BLOCK_PERIOD]), mnesia:transaction(fun () -> + Time = p1_time_compat:monotonic_time(), mnesia:write(#temporarily_blocked{host = Host, - timestamp = - now()}) + timestamp = Time}) end). -spec is_temporarly_blocked(binary()) -> boolean(). @@ -128,7 +125,8 @@ is_temporarly_blocked(Host) -> case mnesia:dirty_read(temporarily_blocked, Host) of [] -> false; [#temporarily_blocked{timestamp = T} = Entry] -> - case timer:now_diff(now(), T) of + Diff = p1_time_compat:monotonic_time() - T, + case p1_time_compat:convert_time_unit(Diff, native, micro_seconds) of N when N > (?S2S_OVERLOAD_BLOCK_PERIOD) * 1000 * 1000 -> mnesia:dirty_delete_object(Entry), false; _ -> true @@ -136,19 +134,15 @@ is_temporarly_blocked(Host) -> end. -spec remove_connection({binary(), binary()}, - pid(), binary()) -> {atomic, ok} | - ok | - {aborted, any()}. + pid()) -> {atomic, ok} | ok | {aborted, any()}. -remove_connection(FromTo, Pid, Key) -> +remove_connection(FromTo, Pid) -> case catch mnesia:dirty_match_object(s2s, - #s2s{fromto = FromTo, pid = Pid, - _ = '_'}) + #s2s{fromto = FromTo, pid = Pid}) of - [#s2s{pid = Pid, key = Key}] -> + [#s2s{pid = Pid}] -> F = fun () -> - mnesia:delete_object(#s2s{fromto = FromTo, pid = Pid, - key = Key}) + mnesia:delete_object(#s2s{fromto = FromTo, pid = Pid}) end, mnesia:transaction(F); _ -> ok @@ -158,25 +152,12 @@ remove_connection(FromTo, Pid, Key) -> have_connection(FromTo) -> case catch mnesia:dirty_read(s2s, FromTo) of - [_] -> + [_] -> true; _ -> false end. --spec has_key({binary(), binary()}, binary()) -> boolean(). - -has_key(FromTo, Key) -> - case mnesia:dirty_select(s2s, - [{#s2s{fromto = FromTo, key = Key, _ = '_'}, - [], - ['$_']}]) of - [] -> - false; - _ -> - true - end. - -spec get_connections_pids({binary(), binary()}) -> [pid()]. get_connections_pids(FromTo) -> @@ -187,10 +168,9 @@ get_connections_pids(FromTo) -> [] end. --spec try_register({binary(), binary()}) -> {key, binary()} | false. +-spec try_register({binary(), binary()}) -> boolean(). try_register(FromTo) -> - Key = randoms:get_string(), MaxS2SConnectionsNumber = max_s2s_connections_number(FromTo), MaxS2SConnectionsNumberPerNode = max_s2s_connections_number_per_node(FromTo), @@ -200,9 +180,8 @@ try_register(FromTo) -> MaxS2SConnectionsNumber, MaxS2SConnectionsNumberPerNode), if NeededConnections > 0 -> - mnesia:write(#s2s{fromto = FromTo, pid = self(), - key = Key}), - {key, Key}; + mnesia:write(#s2s{fromto = FromTo, pid = self()}), + true; true -> false end end, @@ -221,7 +200,7 @@ check_peer_certificate(SockMod, Sock, Peer) -> {ok, Cert} -> case SockMod:get_verify_result(Sock) of 0 -> - case idna:domain_utf8_to_ascii(Peer) of + case ejabberd_idna:domain_utf8_to_ascii(Peer) of false -> {error, <<"Cannot decode remote server name">>}; AsciiPeer -> @@ -235,61 +214,44 @@ check_peer_certificate(SockMod, Sock, Peer) -> end end; VerifyRes -> - {error, p1_tls:get_cert_verify_string(VerifyRes, Cert)} + {error, fast_tls:get_cert_verify_string(VerifyRes, Cert)} end; + {error, _Reason} -> + {error, <<"Cannot get peer certificate">>}; error -> - {error, <<"Cannot get peer certificate">>} + {error, <<"Cannot get peer certificate">>} end. +make_key({From, To}, StreamID) -> + Secret = ejabberd_config:get_option(shared_key, fun(V) -> V end), + p1_sha:to_hexlist( + crypto:hmac(sha256, p1_sha:to_hexlist(crypto:hash(sha256, Secret)), + [To, " ", From, " ", StreamID])). + %%==================================================================== %% gen_server callbacks %%==================================================================== -%%-------------------------------------------------------------------- -%% Function: init(Args) -> {ok, State} | -%% {ok, State, Timeout} | -%% ignore | -%% {stop, Reason} -%% Description: Initiates the server -%%-------------------------------------------------------------------- init([]) -> update_tables(), - mnesia:create_table(s2s, [{ram_copies, [node()]}, {type, bag}, - {attributes, record_info(fields, s2s)}]), + mnesia:create_table(s2s, + [{ram_copies, [node()]}, + {type, bag}, + {attributes, record_info(fields, s2s)}]), mnesia:add_table_copy(s2s, node(), ram_copies), mnesia:subscribe(system), - ejabberd_commands:register_commands(commands()), - mnesia:create_table(temporarily_blocked, [{ram_copies, [node()]}, {attributes, record_info(fields, temporarily_blocked)}]), + ejabberd_commands:register_commands(get_commands_spec()), + mnesia:create_table(temporarily_blocked, + [{ram_copies, [node()]}, + {attributes, record_info(fields, temporarily_blocked)}]), {ok, #state{}}. -%%-------------------------------------------------------------------- -%% 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(_Request, _From, State) -> - Reply = ok, - {reply, Reply, State}. + {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({mnesia_system_event, {mnesia_down, Node}}, State) -> clean_table_from_bad_node(Node), {noreply, State}; @@ -303,27 +265,17 @@ handle_info({route, From, To, Packet}, State) -> {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_commands:unregister_commands(commands()), + ejabberd_commands:unregister_commands(get_commands_spec()), 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 %%-------------------------------------------------------------------- + clean_table_from_bad_node(Node) -> F = fun() -> Es = mnesia:select( @@ -347,8 +299,8 @@ do_route(From, To, Packet) -> #xmlel{name = Name, attrs = Attrs, children = Els} = Packet, NewAttrs = - jlib:replace_from_to_attrs(jlib:jid_to_string(From), - jlib:jid_to_string(To), Attrs), + jlib:replace_from_to_attrs(jid:to_string(From), + jid:to_string(To), Attrs), #jid{lserver = MyServer} = From, ejabberd_hooks:run(s2s_send_packet, MyServer, [From, To, Packet]), @@ -356,7 +308,7 @@ do_route(From, To, Packet) -> #xmlel{name = Name, attrs = NewAttrs, children = Els}), ok; {aborted, _Reason} -> - case xml:get_tag_attr_s(<<"type">>, Packet) of + case fxml:get_tag_attr_s(<<"type">>, Packet) of <<"error">> -> ok; <<"result">> -> ok; _ -> @@ -422,7 +374,7 @@ choose_pid(From, Pids) -> Ps -> Ps end, Pid = - lists:nth(erlang:phash(jlib:jid_remove_resource(From), + lists:nth(erlang:phash(jid:remove_resource(From), length(Pids1)), Pids1), ?DEBUG("Using ejabberd_s2s_out ~p~n", [Pid]), @@ -442,17 +394,15 @@ open_several_connections(N, MyServer, Server, From, new_connection(MyServer, Server, From, FromTo, MaxS2SConnectionsNumber, MaxS2SConnectionsNumberPerNode) -> - Key = randoms:get_string(), {ok, Pid} = ejabberd_s2s_out:start( - MyServer, Server, {new, Key}), + MyServer, Server, new), F = fun() -> L = mnesia:read({s2s, FromTo}), NeededConnections = needed_connections_number(L, MaxS2SConnectionsNumber, MaxS2SConnectionsNumberPerNode), if NeededConnections > 0 -> - mnesia:write(#s2s{fromto = FromTo, pid = Pid, - key = Key}), + mnesia:write(#s2s{fromto = FromTo, pid = Pid}), ?INFO_MSG("New s2s connection started ~p", [Pid]), Pid; true -> choose_connection(From, L) @@ -467,7 +417,7 @@ new_connection(MyServer, Server, From, FromTo, max_s2s_connections_number({From, To}) -> case acl:match_rule(From, max_s2s_connections, - jlib:make_jid(<<"">>, To, <<"">>)) + jid:make(<<"">>, To, <<"">>)) of Max when is_integer(Max) -> Max; _ -> ?DEFAULT_MAX_S2S_CONNECTIONS_NUMBER @@ -475,7 +425,7 @@ max_s2s_connections_number({From, To}) -> max_s2s_connections_number_per_node({From, To}) -> case acl:match_rule(From, max_s2s_connections_per_node, - jlib:make_jid(<<"">>, To, <<"">>)) + jid:make(<<"">>, To, <<"">>)) of Max when is_integer(Max) -> Max; _ -> ?DEFAULT_MAX_S2S_CONNECTIONS_NUMBER_PER_NODE @@ -514,17 +464,19 @@ parent_domains(Domain) -> end, [], lists:reverse(str:tokens(Domain, <<".">>))). -send_element(Pid, El) -> Pid ! {send_element, El}. +send_element(Pid, El) -> + Pid ! {send_element, El}. %%%---------------------------------------------------------------------- %%% ejabberd commands -commands() -> +get_commands_spec() -> [#ejabberd_commands{name = incoming_s2s_number, tags = [stats, s2s], desc = "Number of incoming s2s connections on " "the node", + policy = admin, module = ?MODULE, function = incoming_s2s_number, args = [], result = {s2s_incoming, integer}}, #ejabberd_commands{name = outgoing_s2s_number, @@ -532,6 +484,7 @@ commands() -> desc = "Number of outgoing s2s connections on " "the node", + policy = admin, module = ?MODULE, function = outgoing_s2s_number, args = [], result = {s2s_outgoing, integer}}]. @@ -552,16 +505,17 @@ update_tables() -> end, case catch mnesia:table_info(s2s, attributes) of [fromto, node, key] -> - mnesia:transform_table(s2s, ignore, [fromto, pid, key]), + mnesia:transform_table(s2s, ignore, [fromto, pid]), mnesia:clear_table(s2s); - [fromto, pid, key] -> ok; + [fromto, pid, key] -> + mnesia:transform_table(s2s, ignore, [fromto, pid]), + mnesia:clear_table(s2s); + [fromto, pid] -> ok; {'EXIT', _} -> ok end, case lists:member(local_s2s, mnesia:system_info(tables)) of - true -> - mnesia:delete_table(local_s2s); - false -> - ok + true -> mnesia:delete_table(local_s2s); + false -> ok end. %% Check if host is in blacklist or white list @@ -585,7 +539,7 @@ allow_host1(MyHost, S2SHost) -> s2s_access, fun(A) when is_atom(A) -> A end, all), - JID = jlib:make_jid(<<"">>, S2SHost, <<"">>), + JID = jid:make(<<"">>, S2SHost, <<"">>), case acl:match_rule(MyHost, Rule, JID) of deny -> false; allow -> @@ -655,10 +609,15 @@ get_s2s_state(S2sPid) -> [{s2s_pid, S2sPid} | Infos]. get_cert_domains(Cert) -> - {rdnSequence, Subject} = - (Cert#'Certificate'.tbsCertificate)#'TBSCertificate'.subject, - Extensions = - (Cert#'Certificate'.tbsCertificate)#'TBSCertificate'.extensions, + TBSCert = Cert#'Certificate'.tbsCertificate, + Subject = case TBSCert#'TBSCertificate'.subject of + {rdnSequence, Subj} -> lists:flatten(Subj); + _ -> [] + end, + Extensions = case TBSCert#'TBSCertificate'.extensions of + Exts when is_list(Exts) -> Exts; + _ -> [] + end, lists:flatmap(fun (#'AttributeTypeAndValue'{type = ?'id-at-commonName', value = Val}) -> @@ -669,7 +628,7 @@ get_cert_domains(Cert) -> true -> error end, if D /= error -> - case jlib:string_to_jid(D) of + case jid:from_string(D) of #jid{luser = <<"">>, lserver = LD, lresource = <<"">>} -> [LD]; @@ -681,7 +640,7 @@ get_cert_domains(Cert) -> end; (_) -> [] end, - lists:flatten(Subject)) + Subject) ++ lists:flatmap(fun (#'Extension'{extnID = ?'id-ce-subjectAltName', @@ -705,7 +664,7 @@ get_cert_domains(Cert) -> when is_binary(D) -> case - jlib:string_to_jid((D)) + jid:from_string((D)) of #jid{luser = <<"">>, @@ -714,7 +673,7 @@ get_cert_domains(Cert) -> lresource = <<"">>} -> case - idna:domain_utf8_to_ascii(LD) + ejabberd_idna:domain_utf8_to_ascii(LD) of false -> []; @@ -728,7 +687,7 @@ get_cert_domains(Cert) -> ({dNSName, D}) when is_list(D) -> case - jlib:string_to_jid(list_to_binary(D)) + jid:from_string(list_to_binary(D)) of #jid{luser = <<"">>, lserver = LD, @@ -771,3 +730,11 @@ match_labels([DL | DLabels], [PL | PLabels]) -> end; false -> false end. + +opt_type(route_subdomains) -> + fun (s2s) -> s2s; + (local) -> local + end; +opt_type(s2s_access) -> + fun (A) when is_atom(A) -> A end; +opt_type(_) -> [route_subdomains, s2s_access]. diff --git a/src/ejabberd_s2s_in.erl b/src/ejabberd_s2s_in.erl index 1b40f03c..c8d3cd04 100644 --- a/src/ejabberd_s2s_in.erl +++ b/src/ejabberd_s2s_in.erl @@ -5,7 +5,7 @@ %%% Created : 6 Dec 2002 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,6 +25,8 @@ -module(ejabberd_s2s_in). +-behaviour(ejabberd_config). + -author('alexey@process-one.net'). -behaviour(p1_fsm). @@ -32,11 +34,10 @@ %% External exports -export([start/2, start_link/2, socket_type/0]). -%% gen_fsm callbacks -export([init/1, wait_for_stream/2, wait_for_feature_request/2, stream_established/2, handle_event/3, handle_sync_event/4, code_change/4, - handle_info/3, print_state/1, terminate/3]). + handle_info/3, print_state/1, terminate/3, opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -73,21 +74,6 @@ -endif. -%% Module start with or without supervisor: --ifdef(NO_TRANSIENT_SUPERVISORS). - --define(SUPERVISOR_START, - p1_fsm:start(ejabberd_s2s_in, [SockData, Opts], - ?FSMOPTS ++ fsm_limit_opts(Opts))). - --else. - --define(SUPERVISOR_START, - supervisor:start_child(ejabberd_s2s_in_sup, - [SockData, Opts])). - --endif. - -define(STREAM_HEADER(Version), <<"<?xml version='1.0'?><stream:stream " "xmlns:stream='http://etherx.jabber.org/stream" @@ -99,21 +85,20 @@ -define(STREAM_TRAILER, <<"</stream:stream>">>). -define(INVALID_NAMESPACE_ERR, - xml:element_to_binary(?SERR_INVALID_NAMESPACE)). + fxml:element_to_binary(?SERR_INVALID_NAMESPACE)). -define(HOST_UNKNOWN_ERR, - xml:element_to_binary(?SERR_HOST_UNKNOWN)). + fxml:element_to_binary(?SERR_HOST_UNKNOWN)). -define(INVALID_FROM_ERR, - xml:element_to_binary(?SERR_INVALID_FROM)). + fxml:element_to_binary(?SERR_INVALID_FROM)). -define(INVALID_XML_ERR, - xml:element_to_binary(?SERR_XML_NOT_WELL_FORMED)). + fxml:element_to_binary(?SERR_XML_NOT_WELL_FORMED)). -%%%---------------------------------------------------------------------- -%%% API -%%%---------------------------------------------------------------------- -start(SockData, Opts) -> ?SUPERVISOR_START. +start(SockData, Opts) -> + supervisor:start_child(ejabberd_s2s_in_sup, + [SockData, Opts]). start_link(SockData, Opts) -> p1_fsm:start_link(ejabberd_s2s_in, [SockData, Opts], @@ -125,13 +110,6 @@ socket_type() -> xml_stream. %%% Callback functions from gen_fsm %%%---------------------------------------------------------------------- -%%---------------------------------------------------------------------- -%% Func: init/1 -%% Returns: {ok, StateName, StateData} | -%% {ok, StateName, StateData, Timeout} | -%% ignore | -%% {stop, StopReason} -%%---------------------------------------------------------------------- init([{SockMod, Socket}, Opts]) -> ?DEBUG("started: ~p", [{SockMod, Socket}]), Shaper = case lists:keysearch(shaper, 1, Opts) of @@ -184,9 +162,14 @@ init([{SockMod, Socket}, Opts]) -> undefined -> TLSOpts2; ProtocolOpts -> [{protocol_options, ProtocolOpts} | TLSOpts2] end, + TLSOpts4 = case ejabberd_config:get_option( + s2s_dhfile, fun iolist_to_binary/1) of + undefined -> TLSOpts3; + DHFile -> [{dhfile, DHFile} | TLSOpts3] + end, TLSOpts = case proplists:get_bool(tls_compression, Opts) of - false -> [compression_none | TLSOpts3]; - true -> TLSOpts3 + false -> [compression_none | TLSOpts4]; + true -> TLSOpts4 end, Timer = erlang:start_timer(?S2STIMEOUT, self(), []), {ok, wait_for_stream, @@ -205,10 +188,10 @@ init([{SockMod, Socket}, Opts]) -> wait_for_stream({xmlstreamstart, _Name, Attrs}, StateData) -> - case {xml:get_attr_s(<<"xmlns">>, Attrs), - xml:get_attr_s(<<"xmlns:db">>, Attrs), - xml:get_attr_s(<<"to">>, Attrs), - xml:get_attr_s(<<"version">>, Attrs) == <<"1.0">>} + case {fxml:get_attr_s(<<"xmlns">>, Attrs), + fxml:get_attr_s(<<"xmlns:db">>, Attrs), + fxml:get_attr_s(<<"to">>, Attrs), + fxml:get_attr_s(<<"version">>, Attrs) == <<"1.0">>} of {<<"jabber:server">>, _, Server, true} when StateData#state.tls and @@ -216,7 +199,7 @@ wait_for_stream({xmlstreamstart, _Name, Attrs}, send_text(StateData, ?STREAM_HEADER(<<" version='1.0'">>)), Auth = if StateData#state.tls_enabled -> - case jlib:nameprep(xml:get_attr_s(<<"from">>, Attrs)) of + case jid:nameprep(fxml:get_attr_s(<<"from">>, Attrs)) of From when From /= <<"">>, From /= error -> {Result, Message} = ejabberd_s2s:check_peer_certificate(StateData#state.sockmod, @@ -251,7 +234,7 @@ wait_for_stream({xmlstreamstart, _Name, Attrs}, ?INFO_MSG("Closing s2s connection: ~s <--> ~s (~s)", [StateData#state.server, RemoteServer, CertError]), send_text(StateData, - <<(xml:element_to_binary(?SERRT_POLICY_VIOLATION(<<"en">>, + <<(fxml:element_to_binary(?SERRT_POLICY_VIOLATION(<<"en">>, CertError)))/binary, (?STREAM_TRAILER)/binary>>), {stop, normal, StateData}; @@ -323,7 +306,7 @@ wait_for_feature_request({xmlstreamelement, El}, TLSEnabled = StateData#state.tls_enabled, SockMod = (StateData#state.sockmod):get_sockmod(StateData#state.socket), - case {xml:get_attr_s(<<"xmlns">>, Attrs), Name} of + case {fxml:get_attr_s(<<"xmlns">>, Attrs), Name} of {?NS_TLS, <<"starttls">>} when TLS == true, TLSEnabled == false, SockMod == gen_tcp -> @@ -348,7 +331,7 @@ wait_for_feature_request({xmlstreamelement, El}, end, TLSSocket = (StateData#state.sockmod):starttls(Socket, TLSOpts, - xml:element_to_binary(#xmlel{name + fxml:element_to_binary(#xmlel{name = <<"proceed">>, attrs @@ -362,7 +345,7 @@ wait_for_feature_request({xmlstreamelement, El}, StateData#state{socket = TLSSocket, streamid = new_id(), tls_enabled = true, tls_options = TLSOpts}}; {?NS_SASL, <<"auth">>} when TLSEnabled -> - Mech = xml:get_attr_s(<<"mechanism">>, Attrs), + Mech = fxml:get_attr_s(<<"mechanism">>, Attrs), case Mech of <<"EXTERNAL">> when StateData#state.auth_domain /= <<"">> -> AuthDomain = StateData#state.auth_domain, @@ -377,7 +360,7 @@ wait_for_feature_request({xmlstreamelement, El}, ?INFO_MSG("Accepted s2s EXTERNAL authentication for ~s (TLS=~p)", [AuthDomain, StateData#state.tls_enabled]), change_shaper(StateData, <<"">>, - jlib:make_jid(<<"">>, AuthDomain, <<"">>)), + jid:make(<<"">>, AuthDomain, <<"">>)), {next_state, wait_for_stream, StateData#state{streamid = new_id(), authenticated = true}}; @@ -420,8 +403,8 @@ stream_established({xmlstreamelement, El}, StateData) -> case is_key_packet(El) of {key, To, From, Id, Key} -> ?DEBUG("GET KEY: ~p", [{To, From, Id, Key}]), - LTo = jlib:nameprep(To), - LFrom = jlib:nameprep(From), + LTo = jid:nameprep(To), + LFrom = jid:nameprep(From), case {ejabberd_s2s:allow_host(LTo, LFrom), lists:member(LTo, ejabberd_router:dirty_get_all_domains())} @@ -435,7 +418,7 @@ stream_established({xmlstreamelement, El}, StateData) -> wait_for_verification, StateData#state.connections), change_shaper(StateData, LTo, - jlib:make_jid(<<"">>, LFrom, <<"">>)), + jid:make(<<"">>, LFrom, <<"">>)), {next_state, stream_established, StateData#state{connections = Conns, timer = Timer}}; {_, false} -> @@ -447,11 +430,11 @@ stream_established({xmlstreamelement, El}, StateData) -> end; {verify, To, From, Id, Key} -> ?DEBUG("VERIFY KEY: ~p", [{To, From, Id, Key}]), - LTo = jlib:nameprep(To), - LFrom = jlib:nameprep(From), - Type = case ejabberd_s2s:has_key({LTo, LFrom}, Key) of - true -> <<"valid">>; - _ -> <<"invalid">> + LTo = jid:nameprep(To), + LFrom = jid:nameprep(From), + Type = case ejabberd_s2s:make_key({LTo, LFrom}, Id) of + Key -> <<"valid">>; + _ -> <<"invalid">> end, send_element(StateData, #xmlel{name = <<"db:verify">>, @@ -464,10 +447,10 @@ stream_established({xmlstreamelement, El}, StateData) -> _ -> NewEl = jlib:remove_attr(<<"xmlns">>, El), #xmlel{name = Name, attrs = Attrs} = NewEl, - 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), + From_s = fxml:get_attr_s(<<"from">>, Attrs), + From = jid:from_string(From_s), + To_s = fxml:get_attr_s(<<"to">>, Attrs), + To = jid:from_string(To_s), if (To /= error) and (From /= error) -> LFrom = From#jid.lserver, LTo = To#jid.lserver, @@ -517,8 +500,8 @@ stream_established({valid, From, To}, StateData) -> children = []}), ?INFO_MSG("Accepted s2s dialback authentication for ~s (TLS=~p)", [From, StateData#state.tls_enabled]), - LFrom = jlib:nameprep(From), - LTo = jlib:nameprep(To), + LFrom = jid:nameprep(From), + LTo = jid:nameprep(To), NSD = StateData#state{connections = (?DICT):store({LFrom, LTo}, established, StateData#state.connections)}, @@ -530,8 +513,8 @@ stream_established({invalid, From, To}, StateData) -> [{<<"from">>, To}, {<<"to">>, From}, {<<"type">>, <<"invalid">>}], children = []}), - LFrom = jlib:nameprep(From), - LTo = jlib:nameprep(To), + LFrom = jid:nameprep(From), + LTo = jid:nameprep(To), NSD = StateData#state{connections = (?DICT):erase({LFrom, LTo}, StateData#state.connections)}, @@ -561,20 +544,8 @@ stream_established(closed, StateData) -> % Reply = ok, % {reply, Reply, state_name, StateData}. -%%---------------------------------------------------------------------- -%% Func: handle_event/3 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} -%%---------------------------------------------------------------------- handle_event(_Event, StateName, StateData) -> {next_state, StateName, StateData}. -%%---------------------------------------------------------------------- -%% Func: handle_sync_event/4 -%% Returns: The associated StateData for this connection -%% {reply, Reply, NextStateName, NextStateData} -%% Reply = {state_infos, [{InfoName::atom(), InfoValue::any()] -%%---------------------------------------------------------------------- handle_sync_event(get_state_infos, _From, StateName, StateData) -> @@ -615,12 +586,6 @@ handle_sync_event(_Event, _From, StateName, code_change(_OldVsn, StateName, StateData, _Extra) -> {ok, StateName, StateData}. -%%---------------------------------------------------------------------- -%% Func: handle_info/3 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} -%%---------------------------------------------------------------------- handle_info({send_text, Text}, StateName, StateData) -> send_text(StateData, Text), {next_state, StateName, StateData}; @@ -630,11 +595,6 @@ handle_info({timeout, Timer, _}, _StateName, handle_info(_, StateName, StateData) -> {next_state, StateName, StateData}. -%%---------------------------------------------------------------------- -%% Func: terminate/3 -%% Purpose: Shutdown the fsm -%% Returns: any -%%---------------------------------------------------------------------- terminate(Reason, _StateName, StateData) -> ?DEBUG("terminated: ~p", [Reason]), case Reason of @@ -655,11 +615,6 @@ get_external_hosts(StateData) -> || {{D, _}, established} <- dict:to_list(Connections)] end. -%%---------------------------------------------------------------------- -%% Func: print_state/1 -%% Purpose: Prepare the state to be printed on error log -%% Returns: State to print -%%---------------------------------------------------------------------- print_state(State) -> State. %%%---------------------------------------------------------------------- @@ -671,7 +626,7 @@ send_text(StateData, Text) -> Text). send_element(StateData, El) -> - send_text(StateData, xml:element_to_binary(El)). + send_text(StateData, fxml:element_to_binary(El)). change_shaper(StateData, Host, JID) -> Shaper = acl:match_rule(Host, StateData#state.shaper, @@ -688,15 +643,15 @@ cancel_timer(Timer) -> is_key_packet(#xmlel{name = Name, attrs = Attrs, children = Els}) when Name == <<"db:result">> -> - {key, xml:get_attr_s(<<"to">>, Attrs), - xml:get_attr_s(<<"from">>, Attrs), - xml:get_attr_s(<<"id">>, Attrs), xml:get_cdata(Els)}; + {key, fxml:get_attr_s(<<"to">>, Attrs), + fxml:get_attr_s(<<"from">>, Attrs), + fxml:get_attr_s(<<"id">>, Attrs), fxml:get_cdata(Els)}; is_key_packet(#xmlel{name = Name, attrs = Attrs, children = Els}) when Name == <<"db:verify">> -> - {verify, xml:get_attr_s(<<"to">>, Attrs), - xml:get_attr_s(<<"from">>, Attrs), - xml:get_attr_s(<<"id">>, Attrs), xml:get_cdata(Els)}; + {verify, fxml:get_attr_s(<<"to">>, Attrs), + fxml:get_attr_s(<<"from">>, Attrs), + fxml:get_attr_s(<<"id">>, Attrs), fxml:get_cdata(Els)}; is_key_packet(_) -> false. fsm_limit_opts(Opts) -> @@ -710,3 +665,32 @@ fsm_limit_opts(Opts) -> N -> [{max_queue, N}] end end. + +opt_type(domain_certfile) -> fun iolist_to_binary/1; +opt_type(max_fsm_queue) -> + fun (I) when is_integer(I), I > 0 -> I end; +opt_type(s2s_certfile) -> fun iolist_to_binary/1; +opt_type(s2s_ciphers) -> fun iolist_to_binary/1; +opt_type(s2s_dhfile) -> fun iolist_to_binary/1; +opt_type(s2s_protocol_options) -> + fun (Options) -> + [_ | O] = lists:foldl(fun (X, Acc) -> X ++ Acc end, [], + [["|" | binary_to_list(Opt)] + || Opt <- Options, is_binary(Opt)]), + iolist_to_binary(O) + end; +opt_type(s2s_tls_compression) -> + fun (true) -> true; + (false) -> false + end; +opt_type(s2s_use_starttls) -> + fun (false) -> false; + (true) -> true; + (optional) -> optional; + (required) -> required; + (required_trusted) -> required_trusted + end; +opt_type(_) -> + [domain_certfile, max_fsm_queue, s2s_certfile, + s2s_ciphers, s2s_dhfile, s2s_protocol_options, + s2s_tls_compression, s2s_use_starttls]. diff --git a/src/ejabberd_s2s_out.erl b/src/ejabberd_s2s_out.erl index 6196f136..594fbb2c 100644 --- a/src/ejabberd_s2s_out.erl +++ b/src/ejabberd_s2s_out.erl @@ -5,7 +5,7 @@ %%% Created : 6 Dec 2002 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,6 +25,8 @@ -module(ejabberd_s2s_out). +-behaviour(ejabberd_config). + -author('alexey@process-one.net'). -behaviour(p1_fsm). @@ -35,28 +37,16 @@ start_connection/1, terminate_if_waiting_delay/2, stop_connection/1, - transform_options/1]). - -%% p1_fsm callbacks (same as gen_fsm) --export([init/1, - open_socket/2, - wait_for_stream/2, - wait_for_validation/2, - wait_for_features/2, - wait_for_auth_result/2, - wait_for_starttls_proceed/2, - relay_to_bridge/2, - reopen_socket/2, - wait_before_retry/2, - stream_established/2, - handle_event/3, - handle_sync_event/4, - handle_info/3, - terminate/3, - print_state/1, - code_change/4, - test_get_addr_port/1, - get_addr_port/1]). + transform_options/1]). + +-export([init/1, open_socket/2, wait_for_stream/2, + wait_for_validation/2, wait_for_features/2, + wait_for_auth_result/2, wait_for_starttls_proceed/2, + relay_to_bridge/2, reopen_socket/2, wait_before_retry/2, + stream_established/2, handle_event/3, + handle_sync_event/4, handle_info/3, terminate/3, + print_state/1, code_change/4, test_get_addr_port/1, + get_addr_port/1, opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -66,6 +56,7 @@ -record(state, {socket :: ejabberd_socket:socket_state(), streamid = <<"">> :: binary(), + remote_streamid = <<"">> :: binary(), use_v10 = true :: boolean(), tls = false :: boolean(), tls_required = false :: boolean(), @@ -79,7 +70,7 @@ server = <<"">> :: binary(), queue = queue:new() :: ?TQUEUE, delay_to_retry = undefined_delay :: undefined_delay | non_neg_integer(), - new = false :: false | binary(), + new = false :: boolean(), verify = false :: false | {pid(), binary(), binary()}, bridge :: {atom(), atom()}, timer = make_ref() :: reference()}). @@ -96,15 +87,6 @@ -endif. -%% Module start with or without supervisor: --ifdef(NO_TRANSIENT_SUPERVISORS). --define(SUPERVISOR_START, p1_fsm:start(ejabberd_s2s_out, [From, Host, Type], - fsm_limit_opts() ++ ?FSMOPTS)). --else. --define(SUPERVISOR_START, supervisor:start_child(ejabberd_s2s_out_sup, - [From, Host, Type])). --endif. - -define(FSMTIMEOUT, 30000). %% We do not block on send anymore. @@ -123,13 +105,13 @@ -define(STREAM_TRAILER, <<"</stream:stream>">>). -define(INVALID_NAMESPACE_ERR, - xml:element_to_binary(?SERR_INVALID_NAMESPACE)). + fxml:element_to_binary(?SERR_INVALID_NAMESPACE)). -define(HOST_UNKNOWN_ERR, - xml:element_to_binary(?SERR_HOST_UNKNOWN)). + fxml:element_to_binary(?SERR_HOST_UNKNOWN)). -define(INVALID_XML_ERR, - xml:element_to_binary(?SERR_XML_NOT_WELL_FORMED)). + fxml:element_to_binary(?SERR_XML_NOT_WELL_FORMED)). -define(SOCKET_DEFAULT_RESULT, {error, badarg}). @@ -137,7 +119,8 @@ %%% API %%%---------------------------------------------------------------------- start(From, Host, Type) -> - ?SUPERVISOR_START. + supervisor:start_child(ejabberd_s2s_out_sup, + [From, Host, Type]). start_link(From, Host, Type) -> p1_fsm:start_link(ejabberd_s2s_out, [From, Host, Type], @@ -151,13 +134,6 @@ stop_connection(Pid) -> p1_fsm:send_event(Pid, closed). %%% Callback functions from p1_fsm %%%---------------------------------------------------------------------- -%%---------------------------------------------------------------------- -%% Func: init/1 -%% Returns: {ok, StateName, StateData} | -%% {ok, StateName, StateData, Timeout} | -%% ignore | -%% {stop, StopReason} -%%---------------------------------------------------------------------- init([From, Server, Type]) -> process_flag(trap_exit, true), ?DEBUG("started: ~p", [{From, Server, Type}]), @@ -207,16 +183,21 @@ init([From, Server, Type]) -> undefined -> TLSOpts2; ProtocolOpts -> [{protocol_options, ProtocolOpts} | TLSOpts2] end, + TLSOpts4 = case ejabberd_config:get_option( + s2s_dhfile, fun iolist_to_binary/1) of + undefined -> TLSOpts3; + DHFile -> [{dhfile, DHFile} | TLSOpts3] + end, TLSOpts = case ejabberd_config:get_option( {s2s_tls_compression, From}, fun(true) -> true; (false) -> false end, true) of - false -> [compression_none | TLSOpts3]; - true -> TLSOpts3 + false -> [compression_none | TLSOpts4]; + true -> TLSOpts4 end, {New, Verify} = case Type of - {new, Key} -> {Key, false}; + new -> {true, false}; {verify, Pid, Key, SID} -> start_connection(self()), {false, {Pid, Key, SID}} end, @@ -227,12 +208,6 @@ init([From, Server, Type]) -> tls_options = TLSOpts, queue = queue:new(), myname = From, server = Server, new = New, verify = Verify, timer = Timer}}. -%%---------------------------------------------------------------------- -%% Func: StateName/2 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} -%%---------------------------------------------------------------------- open_socket(init, StateData) -> log_s2s_out(StateData#state.new, StateData#state.myname, StateData#state.server, StateData#state.tls), @@ -240,7 +215,7 @@ open_socket(init, StateData) -> [{StateData#state.myname, StateData#state.server, StateData#state.new, StateData#state.verify}]), AddrList = case - idna:domain_utf8_to_ascii(StateData#state.server) + ejabberd_idna:domain_utf8_to_ascii(StateData#state.server) of false -> []; ASCIIAddr -> get_addr_port(ASCIIAddr) @@ -297,8 +272,6 @@ open_socket(timeout, StateData) -> open_socket(_, StateData) -> {next_state, open_socket, StateData}. -%%---------------------------------------------------------------------- -%% IPv4 open_socket1({_, _, _, _} = Addr, Port) -> open_socket2(inet, Addr, Port); %% IPv6 @@ -338,7 +311,7 @@ open_socket2(Type, Addr, Port) -> wait_for_stream({xmlstreamstart, _Name, Attrs}, StateData) -> - {CertCheckRes, CertCheckMsg, NewStateData} = + {CertCheckRes, CertCheckMsg, StateData0} = if StateData#state.tls_certverify, StateData#state.tls_enabled -> {Res, Msg} = ejabberd_s2s:check_peer_certificate(ejabberd_socket, @@ -350,13 +323,15 @@ wait_for_stream({xmlstreamstart, _Name, Attrs}, true -> {no_verify, <<"Not verified">>, StateData} end, - case {xml:get_attr_s(<<"xmlns">>, Attrs), - xml:get_attr_s(<<"xmlns:db">>, Attrs), - xml:get_attr_s(<<"version">>, Attrs) == <<"1.0">>} + RemoteStreamID = fxml:get_attr_s(<<"id">>, Attrs), + NewStateData = StateData0#state{remote_streamid = RemoteStreamID}, + case {fxml:get_attr_s(<<"xmlns">>, Attrs), + fxml:get_attr_s(<<"xmlns:db">>, Attrs), + fxml:get_attr_s(<<"version">>, Attrs) == <<"1.0">>} of _ when CertCheckRes == error -> send_text(NewStateData, - <<(xml:element_to_binary(?SERRT_POLICY_VIOLATION(<<"en">>, + <<(fxml:element_to_binary(?SERRT_POLICY_VIOLATION(<<"en">>, CertCheckMsg)))/binary, (?STREAM_TRAILER)/binary>>), ?INFO_MSG("Closing s2s connection: ~s -> ~s (~s)", @@ -520,7 +495,7 @@ wait_for_features({xmlstreamelement, El}, StateData) -> STLSReq} = Acc) -> case - xml:get_attr_s(<<"xmlns">>, + fxml:get_attr_s(<<"xmlns">>, Attrs1) of ?NS_SASL -> @@ -533,7 +508,7 @@ wait_for_features({xmlstreamelement, El}, StateData) -> = Els2}) -> case - xml:get_cdata(Els2) + fxml:get_cdata(Els2) of <<"EXTERNAL">> -> true; @@ -558,13 +533,13 @@ wait_for_features({xmlstreamelement, El}, StateData) -> _STLSReq} = Acc) -> case - xml:get_attr_s(<<"xmlns">>, + fxml:get_attr_s(<<"xmlns">>, Attrs1) of ?NS_TLS -> Req = case - xml:get_subtag(El1, + fxml:get_subtag(El1, <<"required">>) of #xmlel{} -> @@ -635,7 +610,7 @@ wait_for_features({xmlstreamelement, El}, StateData) -> end; _ -> send_text(StateData, - <<(xml:element_to_binary(?SERR_BAD_FORMAT))/binary, + <<(fxml:element_to_binary(?SERR_BAD_FORMAT))/binary, (?STREAM_TRAILER)/binary>>), ?INFO_MSG("Closing s2s connection: ~s -> ~s (bad " "format)", @@ -662,7 +637,7 @@ wait_for_auth_result({xmlstreamelement, El}, StateData) -> case El of #xmlel{name = <<"success">>, attrs = Attrs} -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of + case fxml:get_attr_s(<<"xmlns">>, Attrs) of ?NS_SASL -> ?DEBUG("auth: ~p", [{StateData#state.myname, StateData#state.server}]), @@ -678,7 +653,7 @@ wait_for_auth_result({xmlstreamelement, El}, ?FSMTIMEOUT}; _ -> send_text(StateData, - <<(xml:element_to_binary(?SERR_BAD_FORMAT))/binary, + <<(fxml:element_to_binary(?SERR_BAD_FORMAT))/binary, (?STREAM_TRAILER)/binary>>), ?INFO_MSG("Closing s2s connection: ~s -> ~s (bad " "format)", @@ -686,7 +661,7 @@ wait_for_auth_result({xmlstreamelement, El}, {stop, normal, StateData} end; #xmlel{name = <<"failure">>, attrs = Attrs} -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of + case fxml:get_attr_s(<<"xmlns">>, Attrs) of ?NS_SASL -> ?DEBUG("restarted: ~p", [{StateData#state.myname, StateData#state.server}]), @@ -695,7 +670,7 @@ wait_for_auth_result({xmlstreamelement, El}, StateData#state{socket = undefined}, ?FSMTIMEOUT}; _ -> send_text(StateData, - <<(xml:element_to_binary(?SERR_BAD_FORMAT))/binary, + <<(fxml:element_to_binary(?SERR_BAD_FORMAT))/binary, (?STREAM_TRAILER)/binary>>), ?INFO_MSG("Closing s2s connection: ~s -> ~s (bad " "format)", @@ -704,7 +679,7 @@ wait_for_auth_result({xmlstreamelement, El}, end; _ -> send_text(StateData, - <<(xml:element_to_binary(?SERR_BAD_FORMAT))/binary, + <<(fxml:element_to_binary(?SERR_BAD_FORMAT))/binary, (?STREAM_TRAILER)/binary>>), ?INFO_MSG("Closing s2s connection: ~s -> ~s (bad " "format)", @@ -732,7 +707,7 @@ wait_for_starttls_proceed({xmlstreamelement, El}, StateData) -> case El of #xmlel{name = <<"proceed">>, attrs = Attrs} -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of + case fxml:get_attr_s(<<"xmlns">>, Attrs) of ?NS_TLS -> ?DEBUG("starttls: ~p", [{StateData#state.myname, StateData#state.server}]), @@ -762,7 +737,7 @@ wait_for_starttls_proceed({xmlstreamelement, El}, ?FSMTIMEOUT}; _ -> send_text(StateData, - <<(xml:element_to_binary(?SERR_BAD_FORMAT))/binary, + <<(fxml:element_to_binary(?SERR_BAD_FORMAT))/binary, (?STREAM_TRAILER)/binary>>), ?INFO_MSG("Closing s2s connection: ~s -> ~s (bad " "format)", @@ -877,19 +852,7 @@ stream_established(closed, StateData) -> %% Reply = ok, %% {reply, Reply, state_name, StateData}. -%%---------------------------------------------------------------------- -%% Func: handle_event/3 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} -%%---------------------------------------------------------------------- handle_event(_Event, StateName, StateData) -> -%%---------------------------------------------------------------------- -%% Func: handle_sync_event/4 -%% Returns: The associated StateData for this connection -%% {reply, Reply, NextStateName, NextStateData} -%% Reply = {state_infos, [{InfoName::atom(), InfoValue::any()] -%%---------------------------------------------------------------------- {next_state, StateName, StateData, get_timeout_interval(StateName)}. @@ -938,12 +901,6 @@ handle_sync_event(_Event, _From, StateName, code_change(_OldVsn, StateName, StateData, _Extra) -> {ok, StateName, StateData}. -%%---------------------------------------------------------------------- -%% Func: handle_info/3 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} -%%---------------------------------------------------------------------- handle_info({send_text, Text}, StateName, StateData) -> send_text(StateData, Text), cancel_timer(StateData#state.timer), @@ -1000,19 +957,14 @@ handle_info(_, StateName, StateData) -> {next_state, StateName, StateData, get_timeout_interval(StateName)}. -%%---------------------------------------------------------------------- -%% Func: terminate/3 -%% Purpose: Shutdown the fsm -%% Returns: any -%%---------------------------------------------------------------------- terminate(Reason, StateName, StateData) -> ?DEBUG("terminated: ~p", [{Reason, StateName}]), case StateData#state.new of false -> ok; - Key -> + true -> ejabberd_s2s:remove_connection({StateData#state.myname, StateData#state.server}, - self(), Key) + self()) end, bounce_queue(StateData#state.queue, ?ERR_REMOTE_SERVER_NOT_FOUND), @@ -1023,11 +975,6 @@ terminate(Reason, StateName, StateData) -> end, ok. -%%---------------------------------------------------------------------- -%% Func: print_state/1 -%% Purpose: Prepare the state to be printed on error log -%% Returns: State to print -%%---------------------------------------------------------------------- print_state(State) -> State. %%%---------------------------------------------------------------------- @@ -1038,7 +985,7 @@ send_text(StateData, Text) -> ejabberd_socket:send(StateData#state.socket, Text). send_element(StateData, El) -> - send_text(StateData, xml:element_to_binary(El)). + send_text(StateData, fxml:element_to_binary(El)). send_queue(StateData, Q) -> case queue:out(Q) of @@ -1050,14 +997,14 @@ send_queue(StateData, Q) -> %% Bounce a single message (xmlelement) bounce_element(El, Error) -> #xmlel{attrs = Attrs} = El, - case xml:get_attr_s(<<"type">>, Attrs) of + case fxml:get_attr_s(<<"type">>, Attrs) of <<"error">> -> ok; <<"result">> -> ok; _ -> Err = jlib:make_error_reply(El, Error), - From = jlib:string_to_jid(xml:get_tag_attr_s(<<"from">>, + From = jid:from_string(fxml:get_tag_attr_s(<<"from">>, El)), - To = jlib:string_to_jid(xml:get_tag_attr_s(<<"to">>, + To = jid:from_string(fxml:get_tag_attr_s(<<"to">>, El)), ejabberd_router:route(To, From, Err) end. @@ -1085,19 +1032,18 @@ bounce_messages(Error) -> send_db_request(StateData) -> Server = StateData#state.server, New = case StateData#state.new of - false -> - case ejabberd_s2s:try_register({StateData#state.myname, - Server}) - of - {key, Key} -> Key; - false -> false - end; - Key -> Key + false -> + ejabberd_s2s:try_register({StateData#state.myname, Server}); + true -> + true end, NewStateData = StateData#state{new = New}, try case New of - false -> ok; - Key1 -> + false -> ok; + true -> + Key1 = ejabberd_s2s:make_key( + {StateData#state.myname, Server}, + StateData#state.remote_streamid), send_element(StateData, #xmlel{name = <<"db:result">>, attrs = @@ -1124,16 +1070,16 @@ send_db_request(StateData) -> is_verify_res(#xmlel{name = Name, attrs = Attrs}) when Name == <<"db:result">> -> - {result, xml:get_attr_s(<<"to">>, Attrs), - xml:get_attr_s(<<"from">>, Attrs), - xml:get_attr_s(<<"id">>, Attrs), - xml:get_attr_s(<<"type">>, Attrs)}; + {result, fxml:get_attr_s(<<"to">>, Attrs), + fxml:get_attr_s(<<"from">>, Attrs), + fxml:get_attr_s(<<"id">>, Attrs), + fxml:get_attr_s(<<"type">>, Attrs)}; is_verify_res(#xmlel{name = Name, attrs = Attrs}) when Name == <<"db:verify">> -> - {verify, xml:get_attr_s(<<"to">>, Attrs), - xml:get_attr_s(<<"from">>, Attrs), - xml:get_attr_s(<<"id">>, Attrs), - xml:get_attr_s(<<"type">>, Attrs)}; + {verify, fxml:get_attr_s(<<"to">>, Attrs), + fxml:get_attr_s(<<"from">>, Attrs), + fxml:get_attr_s(<<"id">>, Attrs), + fxml:get_attr_s(<<"type">>, Attrs)}; is_verify_res(_) -> false. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -1153,8 +1099,7 @@ get_addr_port(Server) -> ?DEBUG("srv lookup of '~s': ~p~n", [Server, HEnt#hostent.h_addr_list]), AddrList = HEnt#hostent.h_addr_list, - {A1, A2, A3} = now(), - random:seed(A1, A2, A3), + random:seed(p1_time_compat:timestamp()), case catch lists:map(fun ({Priority, Weight, Port, Host}) -> N = case Weight of @@ -1329,14 +1274,10 @@ wait_before_reconnect(StateData) -> cancel_timer(StateData#state.timer), Delay = case StateData#state.delay_to_retry of undefined_delay -> - {_, _, MicroSecs} = now(), MicroSecs rem 14000 + 1000; + {_, _, MicroSecs} = p1_time_compat:timestamp(), MicroSecs rem 14000 + 1000; D1 -> lists:min([D1 * 2, get_max_retry_delay()]) end, Timer = erlang:start_timer(Delay, self(), []), -%% @doc Get the maximum allowed delay for retry to reconnect (in miliseconds). -%% The default value is 5 minutes. -%% The option {s2s_max_retry_delay, Seconds} can be used (in seconds). -%% @spec () -> integer() {next_state, wait_before_retry, StateData#state{timer = Timer, delay_to_retry = Delay, queue = queue:new()}}. @@ -1365,3 +1306,55 @@ fsm_limit_opts() -> undefined -> []; N -> [{max_queue, N}] end. + +opt_type(domain_certfile) -> fun iolist_to_binary/1; +opt_type(max_fsm_queue) -> + fun (I) when is_integer(I), I > 0 -> I end; +opt_type(outgoing_s2s_families) -> + fun (Families) -> + true = lists:all(fun (ipv4) -> true; + (ipv6) -> true + end, + Families), + Families + end; +opt_type(outgoing_s2s_port) -> + fun (I) when is_integer(I), I > 0, I =< 65536 -> I end; +opt_type(outgoing_s2s_timeout) -> + fun (TimeOut) when is_integer(TimeOut), TimeOut > 0 -> + TimeOut; + (infinity) -> infinity + end; +opt_type(s2s_certfile) -> fun iolist_to_binary/1; +opt_type(s2s_ciphers) -> fun iolist_to_binary/1; +opt_type(s2s_dhfile) -> fun iolist_to_binary/1; +opt_type(s2s_dns_retries) -> + fun (I) when is_integer(I), I >= 0 -> I end; +opt_type(s2s_dns_timeout) -> + fun (I) when is_integer(I), I >= 0 -> I end; +opt_type(s2s_max_retry_delay) -> + fun (I) when is_integer(I), I > 0 -> I end; +opt_type(s2s_protocol_options) -> + fun (Options) -> + [_ | O] = lists:foldl(fun (X, Acc) -> X ++ Acc end, [], + [["|" | binary_to_list(Opt)] + || Opt <- Options, is_binary(Opt)]), + iolist_to_binary(O) + end; +opt_type(s2s_tls_compression) -> + fun (true) -> true; + (false) -> false + end; +opt_type(s2s_use_starttls) -> + fun (true) -> true; + (false) -> false; + (optional) -> optional; + (required) -> required; + (required_trusted) -> required_trusted + end; +opt_type(_) -> + [domain_certfile, max_fsm_queue, outgoing_s2s_families, + outgoing_s2s_port, outgoing_s2s_timeout, s2s_certfile, + s2s_ciphers, s2s_dhfile, s2s_dns_retries, s2s_dns_timeout, + s2s_max_retry_delay, s2s_protocol_options, + s2s_tls_compression, s2s_use_starttls]. diff --git a/src/ejabberd_service.erl b/src/ejabberd_service.erl index 1fbc18ff..5caae610 100644 --- a/src/ejabberd_service.erl +++ b/src/ejabberd_service.erl @@ -5,7 +5,7 @@ %%% Created : 6 Dec 2002 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,8 +25,12 @@ -module(ejabberd_service). +-behaviour(ejabberd_config). + -author('alexey@process-one.net'). +-protocol({xep, 114, '1.6'}). + -define(GEN_FSM, p1_fsm). -behaviour(?GEN_FSM). @@ -35,11 +39,10 @@ -export([start/2, start_link/2, send_text/2, send_element/2, socket_type/0, transform_listen_option/2]). -%% gen_fsm callbacks -export([init/1, wait_for_stream/2, wait_for_handshake/2, stream_established/2, handle_event/3, handle_sync_event/4, code_change/4, - handle_info/3, terminate/3, print_state/1]). + handle_info/3, terminate/3, print_state/1, opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -50,8 +53,8 @@ {socket :: ejabberd_socket:socket_state(), sockmod = ejabberd_socket :: ejabberd_socket | ejabberd_frontend_socket, streamid = <<"">> :: binary(), - hosts = [] :: [binary()], - password = <<"">> :: binary(), + host_opts = dict:new() :: ?TDICT, + host = <<"">> :: binary(), access :: atom(), check_from = true :: boolean()}). @@ -88,10 +91,10 @@ "m:error></stream:stream>">>). -define(INVALID_XML_ERR, - xml:element_to_binary(?SERR_XML_NOT_WELL_FORMED)). + fxml:element_to_binary(?SERR_XML_NOT_WELL_FORMED)). -define(INVALID_NS_ERR, - xml:element_to_binary(?SERR_INVALID_NAMESPACE)). + fxml:element_to_binary(?SERR_INVALID_NAMESPACE)). %%%---------------------------------------------------------------------- %%% API @@ -123,18 +126,21 @@ init([{SockMod, Socket}, Opts]) -> {value, {_, A}} -> A; _ -> all end, - %% This should be improved probably - {Hosts, HostOpts} = case lists:keyfind(hosts, 1, Opts) of - {_, HOpts} -> - {[H || {H, _} <- HOpts], - lists:flatten( - [O || {_, O} <- HOpts])}; - _ -> - {[], []} - end, - Password = gen_mod:get_opt(password, HostOpts, - fun iolist_to_binary/1, - p1_sha:sha(crypto:rand_bytes(20))), + HostOpts = case lists:keyfind(hosts, 1, Opts) of + {hosts, HOpts} -> + lists:foldl( + fun({H, Os}, D) -> + P = proplists:get_value( + password, Os, + p1_sha:sha(crypto:rand_bytes(20))), + dict:store(H, P, D) + end, dict:new(), HOpts); + false -> + Pass = proplists:get_value( + password, Opts, + p1_sha:sha(crypto:rand_bytes(20))), + dict:from_list([{global, Pass}]) + end, Shaper = case lists:keysearch(shaper_rule, 1, Opts) of {value, {_, S}} -> S; _ -> none @@ -148,7 +154,7 @@ init([{SockMod, Socket}, Opts]) -> SockMod:change_shaper(Socket, Shaper), {ok, wait_for_stream, #state{socket = Socket, sockmod = SockMod, - streamid = new_id(), hosts = Hosts, password = Password, + streamid = new_id(), host_opts = HostOpts, access = Access, check_from = CheckFrom}}. %%---------------------------------------------------------------------- @@ -160,13 +166,36 @@ init([{SockMod, Socket}, Opts]) -> wait_for_stream({xmlstreamstart, _Name, Attrs}, StateData) -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of + case fxml:get_attr_s(<<"xmlns">>, Attrs) of <<"jabber:component:accept">> -> - To = xml:get_attr_s(<<"to">>, Attrs), - Header = io_lib:format(?STREAM_HEADER, - [StateData#state.streamid, xml:crypt(To)]), - send_text(StateData, Header), - {next_state, wait_for_handshake, StateData}; + To = fxml:get_attr_s(<<"to">>, Attrs), + Host = jid:nameprep(To), + if Host == error -> + Header = io_lib:format(?STREAM_HEADER, + [<<"none">>, ?MYNAME]), + send_text(StateData, + <<(list_to_binary(Header))/binary, + (?INVALID_XML_ERR)/binary, + (?STREAM_TRAILER)/binary>>), + {stop, normal, StateData}; + true -> + Header = io_lib:format(?STREAM_HEADER, + [StateData#state.streamid, fxml:crypt(To)]), + send_text(StateData, Header), + HostOpts = case dict:is_key(Host, StateData#state.host_opts) of + true -> + StateData#state.host_opts; + false -> + case dict:find(global, StateData#state.host_opts) of + {ok, GlobalPass} -> + dict:from_list([{Host, GlobalPass}]); + error -> + StateData#state.host_opts + end + end, + {next_state, wait_for_handshake, + StateData#state{host = Host, host_opts = HostOpts}} + end; _ -> send_text(StateData, ?INVALID_HEADER_ERR), {stop, normal, StateData} @@ -183,23 +212,28 @@ wait_for_stream(closed, StateData) -> wait_for_handshake({xmlstreamelement, El}, StateData) -> #xmlel{name = Name, children = Els} = El, - case {Name, xml:get_cdata(Els)} of + case {Name, fxml:get_cdata(Els)} of {<<"handshake">>, Digest} -> - case p1_sha:sha(<<(StateData#state.streamid)/binary, - (StateData#state.password)/binary>>) - of - Digest -> - send_text(StateData, <<"<handshake/>">>), - lists:foreach(fun (H) -> - ejabberd_router:register_route(H), - ?INFO_MSG("Route registered for service ~p~n", - [H]) - end, - StateData#state.hosts), - {next_state, stream_established, StateData}; - _ -> - send_text(StateData, ?INVALID_HANDSHAKE_ERR), - {stop, normal, StateData} + case dict:find(StateData#state.host, StateData#state.host_opts) of + {ok, Password} -> + case p1_sha:sha(<<(StateData#state.streamid)/binary, + Password/binary>>) of + Digest -> + send_text(StateData, <<"<handshake/>">>), + lists:foreach( + fun (H) -> + ejabberd_router:register_route(H, ?MYNAME), + ?INFO_MSG("Route registered for service ~p~n", + [H]) + end, dict:fetch_keys(StateData#state.host_opts)), + {next_state, stream_established, StateData}; + _ -> + send_text(StateData, ?INVALID_HANDSHAKE_ERR), + {stop, normal, StateData} + end; + _ -> + send_text(StateData, ?INVALID_HANDSHAKE_ERR), + {stop, normal, StateData} end; _ -> {next_state, wait_for_handshake, StateData} end; @@ -216,29 +250,29 @@ wait_for_handshake(closed, StateData) -> stream_established({xmlstreamelement, El}, StateData) -> NewEl = jlib:remove_attr(<<"xmlns">>, El), #xmlel{name = Name, attrs = Attrs} = NewEl, - From = xml:get_attr_s(<<"from">>, Attrs), + From = fxml:get_attr_s(<<"from">>, Attrs), FromJID = case StateData#state.check_from of %% If the admin does not want to check the from field %% when accept packets from any address. %% In this case, the component can send packet of %% behalf of the server users. - false -> jlib:string_to_jid(From); + false -> jid:from_string(From); %% The default is the standard behaviour in XEP-0114 _ -> - FromJID1 = jlib:string_to_jid(From), + FromJID1 = jid:from_string(From), case FromJID1 of #jid{lserver = Server} -> - case lists:member(Server, StateData#state.hosts) of + case dict:is_key(Server, StateData#state.host_opts) of true -> FromJID1; false -> error end; _ -> error end end, - To = xml:get_attr_s(<<"to">>, Attrs), + To = fxml:get_attr_s(<<"to">>, Attrs), ToJID = case To of <<"">> -> error; - _ -> jlib:string_to_jid(To) + _ -> jid:from_string(To) end, if ((Name == <<"iq">>) or (Name == <<"message">>) or (Name == <<"presence">>)) @@ -320,9 +354,9 @@ handle_info({route, From, To, Packet}, StateName, #xmlel{name = Name, attrs = Attrs, children = Els} = Packet, Attrs2 = - jlib:replace_from_to_attrs(jlib:jid_to_string(From), - jlib:jid_to_string(To), Attrs), - Text = xml:element_to_binary(#xmlel{name = Name, + jlib:replace_from_to_attrs(jid:to_string(From), + jid:to_string(To), Attrs), + Text = fxml:element_to_binary(#xmlel{name = Name, attrs = Attrs2, children = Els}), send_text(StateData, Text); deny -> @@ -346,7 +380,7 @@ terminate(Reason, StateName, StateData) -> lists:foreach(fun (H) -> ejabberd_router:unregister_route(H) end, - StateData#state.hosts); + dict:fetch_keys(StateData#state.host_opts)); _ -> ok end, (StateData#state.sockmod):close(StateData#state.socket), @@ -368,7 +402,7 @@ send_text(StateData, Text) -> Text). send_element(StateData, El) -> - send_text(StateData, xml:element_to_binary(El)). + send_text(StateData, fxml:element_to_binary(El)). new_id() -> randoms:get_string(). @@ -402,3 +436,7 @@ fsm_limit_opts(Opts) -> N -> [{max_queue, N}] end end. + +opt_type(max_fsm_queue) -> + fun (I) when is_integer(I), I > 0 -> I end; +opt_type(_) -> [max_fsm_queue]. diff --git a/src/ejabberd_sm.erl b/src/ejabberd_sm.erl index 67845295..218e657f 100644 --- a/src/ejabberd_sm.erl +++ b/src/ejabberd_sm.erl @@ -5,7 +5,7 @@ %%% Created : 24 Nov 2002 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,6 +25,8 @@ -module(ejabberd_sm). +-behaviour(ejabberd_config). + -author('alexey@process-one.net'). -behaviour(gen_server). @@ -33,6 +35,7 @@ -export([start/0, start_link/0, route/3, + process_iq/3, open_session/5, open_session/6, close_session/4, @@ -48,6 +51,7 @@ dirty_get_my_sessions_list/0, get_vh_session_list/1, get_vh_session_number/1, + get_vh_by_backend/1, register_iq_handler/4, register_iq_handler/5, unregister_iq_handler/2, @@ -61,12 +65,12 @@ get_user_ip/3, get_max_user_sessions/2, get_all_pids/0, - is_existing_resource/3 + is_existing_resource/3, + get_commands_spec/0 ]). -%% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, - handle_info/2, terminate/2, code_change/3]). + handle_info/2, terminate/2, code_change/3, opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -94,10 +98,6 @@ %%==================================================================== %% API %%==================================================================== -%%-------------------------------------------------------------------- -%% Function: start_link() -> {ok,Pid} | ignore | {error,Error} -%% Description: Starts the server -%%-------------------------------------------------------------------- -export_type([sid/0]). start() -> @@ -106,8 +106,7 @@ start() -> supervisor:start_child(ejabberd_sup, ChildSpec). start_link() -> - gen_server:start_link({local, ?MODULE}, ?MODULE, [], - []). + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). -spec route(jid(), jid(), xmlel() | broadcast()) -> ok. @@ -124,7 +123,7 @@ route(From, To, Packet) -> open_session(SID, User, Server, Resource, Priority, Info) -> set_session(SID, User, Server, Resource, Priority, Info), check_for_sessions_to_replace(User, Server, Resource), - JID = jlib:make_jid(User, Server, Resource), + JID = jid:make(User, Server, Resource), ejabberd_hooks:run(sm_register_connection_hook, JID#jid.lserver, [SID, JID, Info]). @@ -136,18 +135,21 @@ open_session(SID, User, Server, Resource, Info) -> -spec close_session(sid(), binary(), binary(), binary()) -> ok. close_session(SID, User, Server, Resource) -> - Mod = get_sm_backend(), - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - LResource = jlib:resourceprep(Resource), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + LResource = jid:resourceprep(Resource), + Mod = get_sm_backend(LServer), Info = case Mod:delete_session(LUser, LServer, LResource, SID) of {ok, #session{info = I}} -> I; {error, notfound} -> [] end, - JID = jlib:make_jid(User, Server, Resource), + JID = jid:make(User, Server, Resource), ejabberd_hooks:run(sm_remove_connection_hook, JID#jid.lserver, [SID, JID, Info]). +-spec check_in_subscription(any(), binary(), binary(), + any(), any(), any()) -> any(). + check_in_subscription(Acc, User, Server, _JID, _Type, _Reason) -> case ejabberd_auth:is_user_exists(User, Server) of true -> Acc; @@ -165,21 +167,21 @@ bounce_offline_message(From, To, Packet) -> -spec disconnect_removed_user(binary(), binary()) -> ok. disconnect_removed_user(User, Server) -> - ejabberd_sm:route(jlib:make_jid(<<"">>, <<"">>, <<"">>), - jlib:make_jid(User, Server, <<"">>), + ejabberd_sm:route(jid:make(<<"">>, <<"">>, <<"">>), + jid:make(User, Server, <<"">>), {broadcast, {exit, <<"User removed">>}}). get_user_resources(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - Mod = get_sm_backend(), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + Mod = get_sm_backend(LServer), Ss = Mod:get_sessions(LUser, LServer), [element(3, S#session.usr) || S <- clean_session_list(Ss)]. -spec get_user_present_resources(binary(), binary()) -> [tuple()]. get_user_present_resources(LUser, LServer) -> - Mod = get_sm_backend(), + Mod = get_sm_backend(LServer), Ss = Mod:get_sessions(LUser, LServer), [{S#session.priority, element(3, S#session.usr)} || S <- clean_session_list(Ss), is_integer(S#session.priority)]. @@ -187,10 +189,10 @@ get_user_present_resources(LUser, LServer) -> -spec get_user_ip(binary(), binary(), binary()) -> ip(). get_user_ip(User, Server, Resource) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - LResource = jlib:resourceprep(Resource), - Mod = get_sm_backend(), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + LResource = jid:resourceprep(Resource), + Mod = get_sm_backend(LServer), case Mod:get_sessions(LUser, LServer, LResource) of [] -> undefined; @@ -202,10 +204,10 @@ get_user_ip(User, Server, Resource) -> -spec get_user_info(binary(), binary(), binary()) -> info() | offline. get_user_info(User, Server, Resource) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - LResource = jlib:resourceprep(Resource), - Mod = get_sm_backend(), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + LResource = jid:resourceprep(Resource), + Mod = get_sm_backend(LServer), case Mod:get_sessions(LUser, LServer, LResource) of [] -> offline; @@ -225,7 +227,7 @@ set_presence(SID, User, Server, Resource, Priority, set_session(SID, User, Server, Resource, Priority, Info), ejabberd_hooks:run(set_presence_hook, - jlib:nameprep(Server), + jid:nameprep(Server), [User, Server, Resource, Presence]). -spec unset_presence(sid(), binary(), binary(), @@ -236,7 +238,7 @@ unset_presence(SID, User, Server, Resource, Status, set_session(SID, User, Server, Resource, undefined, Info), ejabberd_hooks:run(unset_presence_hook, - jlib:nameprep(Server), + jid:nameprep(Server), [User, Server, Resource, Status]). -spec close_session_unset_presence(sid(), binary(), binary(), @@ -246,16 +248,16 @@ close_session_unset_presence(SID, User, Server, Resource, Status) -> close_session(SID, User, Server, Resource), ejabberd_hooks:run(unset_presence_hook, - jlib:nameprep(Server), + jid:nameprep(Server), [User, Server, Resource, Status]). -spec get_session_pid(binary(), binary(), binary()) -> none | pid(). get_session_pid(User, Server, Resource) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - LResource = jlib:resourceprep(Resource), - Mod = get_sm_backend(), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + LResource = jid:resourceprep(Resource), + Mod = get_sm_backend(LServer), case Mod:get_sessions(LUser, LServer, LResource) of [#session{sid = {_, Pid}}] -> Pid; _ -> none @@ -264,40 +266,49 @@ get_session_pid(User, Server, Resource) -> -spec dirty_get_sessions_list() -> [ljid()]. dirty_get_sessions_list() -> - Mod = get_sm_backend(), - [S#session.usr || S <- Mod:get_sessions()]. + lists:flatmap( + fun(Mod) -> + [S#session.usr || S <- Mod:get_sessions()] + end, get_sm_backends()). + +-spec dirty_get_my_sessions_list() -> [#session{}]. dirty_get_my_sessions_list() -> - Mod = get_sm_backend(), - [S || S <- Mod:get_sessions(), node(element(2, S#session.sid)) == node()]. + lists:flatmap( + fun(Mod) -> + [S || S <- Mod:get_sessions(), + node(element(2, S#session.sid)) == node()] + end, get_sm_backends()). -spec get_vh_session_list(binary()) -> [ljid()]. get_vh_session_list(Server) -> - LServer = jlib:nameprep(Server), - Mod = get_sm_backend(), + LServer = jid:nameprep(Server), + Mod = get_sm_backend(LServer), [S#session.usr || S <- Mod:get_sessions(LServer)]. -spec get_all_pids() -> [pid()]. get_all_pids() -> - Mod = get_sm_backend(), - [element(2, S#session.sid) || S <- Mod:get_sessions()]. + lists:flatmap( + fun(Mod) -> + [element(2, S#session.sid) || S <- Mod:get_sessions()] + end, get_sm_backends()). + +-spec get_vh_session_number(binary()) -> non_neg_integer(). get_vh_session_number(Server) -> - LServer = jlib:nameprep(Server), - Mod = get_sm_backend(), + LServer = jid:nameprep(Server), + Mod = get_sm_backend(LServer), length(Mod:get_sessions(LServer)). register_iq_handler(Host, XMLNS, Module, Fun) -> - ejabberd_sm ! - {register_iq_handler, Host, XMLNS, Module, Fun}. + ejabberd_sm ! {register_iq_handler, Host, XMLNS, Module, Fun}. -spec register_iq_handler(binary(), binary(), atom(), atom(), list()) -> any(). register_iq_handler(Host, XMLNS, Module, Fun, Opts) -> - ejabberd_sm ! - {register_iq_handler, Host, XMLNS, Module, Fun, Opts}. + ejabberd_sm ! {register_iq_handler, Host, XMLNS, Module, Fun, Opts}. -spec unregister_iq_handler(binary(), binary()) -> any(). @@ -309,16 +320,8 @@ unregister_iq_handler(Host, XMLNS) -> %% gen_server callbacks %%==================================================================== -%%-------------------------------------------------------------------- -%% Function: init(Args) -> {ok, State} | -%% {ok, State, Timeout} | -%% ignore | -%% {stop, Reason} -%% Description: Initiates the server -%%-------------------------------------------------------------------- init([]) -> - Mod = get_sm_backend(), - Mod:init(), + lists:foreach(fun(Mod) -> Mod:init() end, get_sm_backends()), ets:new(sm_iqtable, [named_table]), lists:foreach( fun(Host) -> @@ -329,35 +332,14 @@ init([]) -> ejabberd_hooks:add(remove_user, Host, ejabberd_sm, disconnect_removed_user, 100) end, ?MYHOSTS), - ejabberd_commands:register_commands(commands()), + ejabberd_commands:register_commands(get_commands_spec()), {ok, #state{}}. -%%-------------------------------------------------------------------- -%% 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(_Request, _From, State) -> Reply = ok, {reply, Reply, 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) -> case catch do_route(From, To, Packet) of {'EXIT', Reason} -> @@ -387,34 +369,26 @@ handle_info({unregister_iq_handler, Host, XMLNS}, {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_commands:unregister_commands(commands()), + ejabberd_commands:unregister_commands(get_commands_spec()), 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 %%-------------------------------------------------------------------- +-spec set_session(sid(), binary(), binary(), binary(), + prio(), info()) -> ok. + set_session(SID, User, Server, Resource, Priority, Info) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - LResource = jlib:resourceprep(Resource), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + LResource = jid:resourceprep(Resource), US = {LUser, LServer}, USR = {LUser, LServer, LResource}, - Mod = get_sm_backend(), + Mod = get_sm_backend(LServer), Mod:set_session(#session{sid = SID, usr = USR, us = US, priority = Priority, info = Info}). @@ -425,13 +399,13 @@ do_route(From, To, {broadcast, _} = Packet) -> <<"">> -> lists:foreach(fun(R) -> do_route(From, - jlib:jid_replace_resource(To, R), + jid:replace_resource(To, R), Packet) end, get_user_resources(To#jid.user, To#jid.server)); _ -> - {U, S, R} = jlib:jid_tolower(To), - Mod = get_sm_backend(), + {U, S, R} = jid:tolower(To), + Mod = get_sm_backend(S), case Mod:get_sessions(U, S, R) of [] -> ?DEBUG("packet dropped~n", []); @@ -453,10 +427,10 @@ do_route(From, To, #xmlel{} = Packet) -> <<"">> -> case Name of <<"presence">> -> - {Pass, _Subsc} = case xml:get_attr_s(<<"type">>, Attrs) + {Pass, _Subsc} = case fxml:get_attr_s(<<"type">>, Attrs) of <<"subscribe">> -> - Reason = xml:get_path_s(Packet, + Reason = fxml:get_path_s(Packet, [{elem, <<"status">>}, cdata]), @@ -509,7 +483,7 @@ do_route(From, To, #xmlel{} = Packet) -> PResources = get_user_present_resources(LUser, LServer), lists:foreach(fun ({_, R}) -> do_route(From, - jlib:jid_replace_resource(To, + jid:replace_resource(To, R), Packet) end, @@ -517,7 +491,7 @@ do_route(From, To, #xmlel{} = Packet) -> true -> ok end; <<"message">> -> - case xml:get_attr_s(<<"type">>, Attrs) of + case fxml:get_attr_s(<<"type">>, Attrs) of <<"chat">> -> route_message(From, To, Packet, chat); <<"headline">> -> route_message(From, To, Packet, headline); <<"error">> -> ok; @@ -532,13 +506,15 @@ do_route(From, To, #xmlel{} = Packet) -> _ -> ok end; _ -> - Mod = get_sm_backend(), + Mod = get_sm_backend(LServer), case Mod:get_sessions(LUser, LServer, LResource) of [] -> case Name of <<"message">> -> - case xml:get_attr_s(<<"type">>, Attrs) of + case fxml:get_attr_s(<<"type">>, Attrs) of <<"chat">> -> route_message(From, To, Packet, chat); + <<"normal">> -> route_message(From, To, Packet, normal); + <<"">> -> route_message(From, To, Packet, normal); <<"error">> -> ok; _ -> Err = jlib:make_error_reply(Packet, @@ -546,7 +522,7 @@ do_route(From, To, #xmlel{} = Packet) -> ejabberd_router:route(To, From, Err) end; <<"iq">> -> - case xml:get_attr_s(<<"type">>, Attrs) of + case fxml:get_attr_s(<<"type">>, Attrs) of <<"error">> -> ok; <<"result">> -> ok; _ -> @@ -596,8 +572,8 @@ route_message(From, To, Packet, Type) -> when is_integer(Priority), Priority >= 0 -> lists:foreach(fun ({P, R}) when P == Priority; (P >= 0) and (Type == headline) -> - LResource = jlib:resourceprep(R), - Mod = get_sm_backend(), + LResource = jid:resourceprep(R), + Mod = get_sm_backend(LServer), case Mod:get_sessions(LUser, LServer, LResource) of [] -> @@ -652,9 +628,9 @@ clean_session_list([S1, S2 | Rest], Res) -> %% On new session, check if some existing connections need to be replace check_for_sessions_to_replace(User, Server, Resource) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - LResource = jlib:resourceprep(Resource), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + LResource = jid:resourceprep(Resource), check_existing_resources(LUser, LServer, LResource), check_max_sessions(LUser, LServer). @@ -676,14 +652,14 @@ is_existing_resource(LUser, LServer, LResource) -> [] /= get_resource_sessions(LUser, LServer, LResource). get_resource_sessions(User, Server, Resource) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - LResource = jlib:resourceprep(Resource), - Mod = get_sm_backend(), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + LResource = jid:resourceprep(Resource), + Mod = get_sm_backend(LServer), [S#session.sid || S <- Mod:get_sessions(LUser, LServer, LResource)]. check_max_sessions(LUser, LServer) -> - Mod = get_sm_backend(), + Mod = get_sm_backend(LServer), SIDs = [S#session.sid || S <- Mod:get_sessions(LUser, LServer)], MaxSessions = get_max_user_sessions(LUser, LServer), if length(SIDs) =< MaxSessions -> ok; @@ -696,7 +672,7 @@ check_max_sessions(LUser, LServer) -> %% Defaults to infinity get_max_user_sessions(LUser, Host) -> case acl:match_rule(Host, max_user_sessions, - jlib:make_jid(LUser, Host, <<"">>)) + jid:make(LUser, Host, <<"">>)) of Max when is_integer(Max) -> Max; infinity -> infinity; @@ -735,17 +711,17 @@ process_iq(From, To, Packet) -> -spec force_update_presence({binary(), binary()}) -> any(). force_update_presence({LUser, LServer}) -> - Mod = get_sm_backend(), + Mod = get_sm_backend(LServer), Ss = Mod:get_sessions(LUser, LServer), lists:foreach(fun (#session{sid = {_, Pid}}) -> - Pid ! {force_update_presence, LUser} + Pid ! {force_update_presence, LUser, LServer} end, Ss). --spec get_sm_backend() -> module(). +-spec get_sm_backend(binary()) -> module(). -get_sm_backend() -> - DBType = ejabberd_config:get_option(sm_db_type, +get_sm_backend(Host) -> + DBType = ejabberd_config:get_option({sm_db_type, Host}, fun(mnesia) -> mnesia; (internal) -> mnesia; (odbc) -> odbc; @@ -753,25 +729,41 @@ get_sm_backend() -> end, mnesia), list_to_atom("ejabberd_sm_" ++ atom_to_list(DBType)). +-spec get_sm_backends() -> [module()]. + +get_sm_backends() -> + lists:usort([get_sm_backend(Host) || Host <- ?MYHOSTS]). + +-spec get_vh_by_backend(module()) -> [binary()]. + +get_vh_by_backend(Mod) -> + lists:filter( + fun(Host) -> + get_sm_backend(Host) == Mod + end, ?MYHOSTS). + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% ejabberd commands -commands() -> +get_commands_spec() -> [#ejabberd_commands{name = connected_users, tags = [session], desc = "List all established sessions", + policy = admin, module = ?MODULE, function = connected_users, args = [], result = {connected_users, {list, {sessions, string}}}}, #ejabberd_commands{name = connected_users_number, tags = [session, stats], desc = "Get the number of established sessions", + policy = admin, module = ?MODULE, function = connected_users_number, args = [], result = {num_sessions, integer}}, #ejabberd_commands{name = user_resources, tags = [session], desc = "List user's connected resources", + policy = user, module = ?MODULE, function = user_resources, - args = [{user, binary}, {host, binary}], + args = [], result = {resources, {list, {resource, string}}}}, #ejabberd_commands{name = kick_user, tags = [session], @@ -803,3 +795,11 @@ kick_user(User, Server) -> PID ! kick end, Resources), length(Resources). + +opt_type(sm_db_type) -> + fun (mnesia) -> mnesia; + (internal) -> mnesia; + (odbc) -> odbc; + (redis) -> redis + end; +opt_type(_) -> [sm_db_type]. diff --git a/src/ejabberd_sm_mnesia.erl b/src/ejabberd_sm_mnesia.erl index 7acc1022..b900da31 100644 --- a/src/ejabberd_sm_mnesia.erl +++ b/src/ejabberd_sm_mnesia.erl @@ -1,6 +1,6 @@ %%%------------------------------------------------------------------- %%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> -%%% @copyright (C) 2015, Evgeny Khramtsov +%%% @copyright (C) 2015-2016, Evgeny Khramtsov %%% @doc %%% %%% @end diff --git a/src/ejabberd_sm_odbc.erl b/src/ejabberd_sm_odbc.erl index 946f58ff..698763b5 100644 --- a/src/ejabberd_sm_odbc.erl +++ b/src/ejabberd_sm_odbc.erl @@ -1,6 +1,6 @@ %%%------------------------------------------------------------------- %%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> -%%% @copyright (C) 2015, Evgeny Khramtsov +%%% @copyright (C) 2015-2016, Evgeny Khramtsov %%% @doc %%% %%% @end @@ -43,7 +43,7 @@ init() -> end; (_, Err) -> Err - end, ok, ?MYHOSTS). + end, ok, ejabberd_sm:get_vh_by_backend(?MODULE)). set_session(#session{sid = {Now, Pid}, usr = {U, LServer, R}, priority = Priority, info = Info}) -> @@ -90,7 +90,7 @@ get_sessions() -> lists:flatmap( fun(LServer) -> get_sessions(LServer) - end, ?MYHOSTS). + end, ejabberd_sm:get_vh_by_backend(?MODULE)). get_sessions(LServer) -> case ejabberd_odbc:sql_query( diff --git a/src/ejabberd_sm_redis.erl b/src/ejabberd_sm_redis.erl index 0283f9c3..bf9e0eff 100644 --- a/src/ejabberd_sm_redis.erl +++ b/src/ejabberd_sm_redis.erl @@ -1,6 +1,6 @@ %%%------------------------------------------------------------------- %%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> -%%% @copyright (C) 2015, Evgeny Khramtsov +%%% @copyright (C) 2015-2016, Evgeny Khramtsov %%% @doc %%% %%% @end @@ -8,16 +8,13 @@ %%%------------------------------------------------------------------- -module(ejabberd_sm_redis). +-behaviour(ejabberd_config). + -behaviour(ejabberd_sm). -%% API --export([init/0, - set_session/1, - delete_session/4, - get_sessions/0, - get_sessions/1, - get_sessions/2, - get_sessions/3]). +-export([init/0, set_session/1, delete_session/4, + get_sessions/0, get_sessions/1, get_sessions/2, + get_sessions/3, opt_type/1]). -include("ejabberd.hrl"). -include("ejabberd_sm.hrl"). @@ -110,7 +107,7 @@ get_sessions() -> lists:flatmap( fun(LServer) -> get_sessions(LServer) - end, ?MYHOSTS). + end, ejabberd_sm:get_vh_by_backend(?MODULE)). -spec get_sessions(binary()) -> [#session{}]. get_sessions(LServer) -> @@ -134,7 +131,8 @@ get_sessions(LUser, LServer) -> [] end. --spec get_sessions(binary(), binary(), binary()) -> [#session{}]. +-spec get_sessions(binary(), binary(), binary()) -> + [#session{}]. get_sessions(LUser, LServer, LResource) -> USKey = us_to_key({LUser, LServer}), case eredis:q(?PROCNAME, ["HGETALL", USKey]) of @@ -206,4 +204,18 @@ clean_table() -> ?ERROR_MSG("failed to clean redis table for " "server ~s: ~p", [LServer, Err]) end - end, ?MYHOSTS). + end, ejabberd_sm:get_vh_by_backend(?MODULE)). + +opt_type(redis_connect_timeout) -> + fun (I) when is_integer(I), I > 0 -> I end; +opt_type(redis_db) -> + fun (I) when is_integer(I), I >= 0 -> I end; +opt_type(redis_password) -> fun iolist_to_list/1; +opt_type(redis_port) -> + fun (P) when is_integer(P), P > 0, P < 65536 -> P end; +opt_type(redis_reconnect_timeout) -> + fun (I) when is_integer(I), I > 0 -> I end; +opt_type(redis_server) -> fun iolist_to_list/1; +opt_type(_) -> + [redis_connect_timeout, redis_db, redis_password, + redis_port, redis_reconnect_timeout, redis_server]. diff --git a/src/ejabberd_socket.erl b/src/ejabberd_socket.erl index 29c7774e..887b4a0f 100644 --- a/src/ejabberd_socket.erl +++ b/src/ejabberd_socket.erl @@ -5,7 +5,7 @@ %%% Created : 23 Aug 2006 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -52,10 +52,10 @@ -type sockmod() :: ejabberd_http_bind | ejabberd_http_ws | - gen_tcp | p1_tls | ezlib. + gen_tcp | fast_tls | ezlib. -type receiver() :: pid () | atom(). -type socket() :: pid() | inet:socket() | - p1_tls:tls_socket() | + fast_tls:tls_socket() | ezlib:zlib_socket() | ejabberd_http_bind:bind_socket(). @@ -67,15 +67,12 @@ -export_type([socket_state/0, sockmod/0]). --spec start(atom(), sockmod(), socket(), [{atom(), any()}]) -> any(). %%==================================================================== %% API %%==================================================================== -%%-------------------------------------------------------------------- -%% Function: -%% Description: -%%-------------------------------------------------------------------- +-spec start(atom(), sockmod(), socket(), [{atom(), any()}]) -> any(). + start(Module, SockMod, Socket, Opts) -> case Module:socket_type() of xml_stream -> @@ -148,15 +145,15 @@ connect(Addr, Port, Opts, Timeout) -> end. starttls(SocketData, TLSOpts) -> - {ok, TLSSocket} = p1_tls:tcp_to_tls(SocketData#socket_state.socket, TLSOpts), + {ok, TLSSocket} = fast_tls:tcp_to_tls(SocketData#socket_state.socket, TLSOpts), ejabberd_receiver:starttls(SocketData#socket_state.receiver, TLSSocket), - SocketData#socket_state{socket = TLSSocket, sockmod = p1_tls}. + SocketData#socket_state{socket = TLSSocket, sockmod = fast_tls}. starttls(SocketData, TLSOpts, Data) -> - {ok, TLSSocket} = p1_tls:tcp_to_tls(SocketData#socket_state.socket, TLSOpts), + {ok, TLSSocket} = fast_tls:tcp_to_tls(SocketData#socket_state.socket, TLSOpts), ejabberd_receiver:starttls(SocketData#socket_state.receiver, TLSSocket), send(SocketData, Data), - SocketData#socket_state{socket = TLSSocket, sockmod = p1_tls}. + SocketData#socket_state{socket = TLSSocket, sockmod = fast_tls}. compress(SocketData) -> compress(SocketData, undefined). @@ -219,10 +216,10 @@ get_sockmod(SocketData) -> SocketData#socket_state.sockmod. get_peer_certificate(SocketData) -> - p1_tls:get_peer_certificate(SocketData#socket_state.socket). + fast_tls:get_peer_certificate(SocketData#socket_state.socket). get_verify_result(SocketData) -> - p1_tls:get_verify_result(SocketData#socket_state.socket). + fast_tls:get_verify_result(SocketData#socket_state.socket). close(SocketData) -> ejabberd_receiver:close(SocketData#socket_state.receiver). @@ -241,7 +238,3 @@ peername(#socket_state{sockmod = SockMod, _ -> SockMod:peername(Socket) end. -%%==================================================================== -%% Internal functions -%%==================================================================== -%==================================================================== diff --git a/src/ejabberd_sql_pt.erl b/src/ejabberd_sql_pt.erl new file mode 100644 index 00000000..660eac19 --- /dev/null +++ b/src/ejabberd_sql_pt.erl @@ -0,0 +1,544 @@ +%%%------------------------------------------------------------------- +%%% File : ejabberd_sql_pt.erl +%%% Author : Alexey Shchepin <alexey@process-one.net> +%%% Description : Parse transform for SQL queries +%%% +%%% Created : 20 Jan 2016 by Alexey Shchepin <alexey@process-one.net> +%%%------------------------------------------------------------------- +-module(ejabberd_sql_pt). + +%% API +-export([parse_transform/2]). + +-export([parse/2]). + +-include("ejabberd_sql_pt.hrl"). + +-record(state, {loc, + 'query' = [], + params = [], + param_pos = 0, + args = [], + res = [], + res_vars = [], + res_pos = 0}). + +-define(QUERY_RECORD, "sql_query"). + +-define(ESCAPE_RECORD, "sql_escape"). +-define(ESCAPE_VAR, "__SQLEscape"). + +-define(MOD, sql__module_). + +%%==================================================================== +%% API +%%==================================================================== +%%-------------------------------------------------------------------- +%% Function: +%% Description: +%%-------------------------------------------------------------------- +parse_transform(AST, _Options) -> + %io:format("PT: ~p~nOpts: ~p~n", [AST, Options]), + NewAST = top_transform(AST), + %io:format("NewPT: ~p~n", [NewAST]), + NewAST. + + + +%%==================================================================== +%% Internal functions +%%==================================================================== + + +transform(Form) -> + case erl_syntax:type(Form) of + application -> + case erl_syntax_lib:analyze_application(Form) of + {?SQL_MARK, 1} -> + case erl_syntax:application_arguments(Form) of + [Arg] -> + case erl_syntax:type(Arg) of + string -> + S = erl_syntax:string_value(Arg), + Pos = erl_syntax:get_pos(Arg), + ParseRes = parse(S, Pos), + set_pos(make_sql_query(ParseRes), Pos); + _ -> + throw({error, erl_syntax:get_pos(Form), + "?SQL argument must be " + "a constant string"}) + end; + _ -> + throw({error, erl_syntax:get_pos(Form), + "wrong number of ?SQL args"}) + end; + {?SQL_UPSERT_MARK, 2} -> + case erl_syntax:application_arguments(Form) of + [TableArg, FieldsArg] -> + case {erl_syntax:type(TableArg), + erl_syntax:is_proper_list(FieldsArg)}of + {string, true} -> + Table = erl_syntax:string_value(TableArg), + ParseRes = + parse_upsert( + erl_syntax:list_elements(FieldsArg)), + Pos = erl_syntax:get_pos(Form), + set_pos( + make_sql_upsert(Table, ParseRes, Pos), + Pos); + _ -> + throw({error, erl_syntax:get_pos(Form), + "?SQL_UPSERT arguments must be " + "a constant string and a list"}) + end; + _ -> + throw({error, erl_syntax:get_pos(Form), + "wrong number of ?SQL_UPSERT args"}) + end; + _ -> + Form + end; + attribute -> + case erl_syntax:atom_value(erl_syntax:attribute_name(Form)) of + module -> + case erl_syntax:attribute_arguments(Form) of + [M | _] -> + Module = erl_syntax:atom_value(M), + %io:format("module ~p~n", [Module]), + put(?MOD, Module), + Form; + _ -> + Form + end; + _ -> + Form + end; + _ -> + Form + end. + +top_transform(Forms) when is_list(Forms) -> + lists:map( + fun(Form) -> + try + Form2 = erl_syntax_lib:map( + fun(Node) -> + %io:format("asd ~p~n", [Node]), + transform(Node) + end, Form), + Form3 = erl_syntax:revert(Form2), + Form3 + catch + throw:{error, Line, Error} -> + {error, {Line, erl_parse, Error}} + end + end, Forms). + +parse(S, Loc) -> + parse1(S, [], #state{loc = Loc}). + +parse(S, ParamPos, Loc) -> + parse1(S, [], #state{loc = Loc, param_pos = ParamPos}). + +parse1([], Acc, State) -> + State1 = append_string(lists:reverse(Acc), State), + State1#state{'query' = lists:reverse(State1#state.'query'), + params = lists:reverse(State1#state.params), + args = lists:reverse(State1#state.args), + res = lists:reverse(State1#state.res), + res_vars = lists:reverse(State1#state.res_vars) + }; +parse1([$@, $( | S], Acc, State) -> + State1 = append_string(lists:reverse(Acc), State), + {Name, Type, S1, State2} = parse_name(S, State1), + Var = "__V" ++ integer_to_list(State2#state.res_pos), + EVar = erl_syntax:variable(Var), + Convert = + case Type of + integer -> + erl_syntax:application( + erl_syntax:atom(binary_to_integer), + [EVar]); + string -> + EVar; + boolean -> + erl_syntax:application( + erl_syntax:atom(ejabberd_odbc), + erl_syntax:atom(to_bool), + [EVar]) + end, + State3 = append_string(Name, State2), + State4 = State3#state{res_pos = State3#state.res_pos + 1, + res = [Convert | State3#state.res], + res_vars = [EVar | State3#state.res_vars]}, + parse1(S1, [], State4); +parse1([$%, $( | S], Acc, State) -> + State1 = append_string(lists:reverse(Acc), State), + {Name, Type, S1, State2} = parse_name(S, State1), + Var = State2#state.param_pos, + Convert = + erl_syntax:application( + erl_syntax:record_access( + erl_syntax:variable(?ESCAPE_VAR), + erl_syntax:atom(?ESCAPE_RECORD), + erl_syntax:atom(Type)), + [erl_syntax:variable(Name)]), + State3 = State2, + State4 = + State3#state{'query' = [{var, Var} | State3#state.'query'], + args = [Convert | State3#state.args], + params = [Var | State3#state.params], + param_pos = State3#state.param_pos + 1}, + parse1(S1, [], State4); +parse1([C | S], Acc, State) -> + parse1(S, [C | Acc], State). + +append_string([], State) -> + State; +append_string(S, State) -> + State#state{query = [{str, S} | State#state.query]}. + +parse_name(S, State) -> + parse_name(S, [], 0, State). + +parse_name([], _Acc, _Depth, State) -> + throw({error, State#state.loc, + "expected ')', found end of string"}); +parse_name([$), T | S], Acc, 0, State) -> + Type = + case T of + $d -> integer; + $s -> string; + $b -> boolean; + _ -> + throw({error, State#state.loc, + ["unknown type specifier '", T, "'"]}) + end, + {lists:reverse(Acc), Type, S, State}; +parse_name([$)], _Acc, 0, State) -> + throw({error, State#state.loc, + "expected type specifier, found end of string"}); +parse_name([$( = C | S], Acc, Depth, State) -> + parse_name(S, [C | Acc], Depth + 1, State); +parse_name([$) = C | S], Acc, Depth, State) -> + parse_name(S, [C | Acc], Depth - 1, State); +parse_name([C | S], Acc, Depth, State) -> + parse_name(S, [C | Acc], Depth, State). + + +make_var(V) -> + Var = "__V" ++ integer_to_list(V), + erl_syntax:variable(Var). + + +make_sql_query(State) -> + Hash = erlang:phash2(State#state{loc = undefined}), + SHash = <<"Q", (integer_to_binary(Hash))/binary>>, + Query = pack_query(State#state.'query'), + EQuery = + lists:map( + fun({str, S}) -> + erl_syntax:binary( + [erl_syntax:binary_field( + erl_syntax:string(S))]); + ({var, V}) -> make_var(V) + end, Query), + erl_syntax:record_expr( + erl_syntax:atom(?QUERY_RECORD), + [erl_syntax:record_field( + erl_syntax:atom(hash), + %erl_syntax:abstract(SHash) + erl_syntax:binary( + [erl_syntax:binary_field( + erl_syntax:string(binary_to_list(SHash)))])), + erl_syntax:record_field( + erl_syntax:atom(args), + erl_syntax:fun_expr( + [erl_syntax:clause( + [erl_syntax:variable(?ESCAPE_VAR)], + none, + [erl_syntax:list(State#state.args)] + )])), + erl_syntax:record_field( + erl_syntax:atom(format_query), + erl_syntax:fun_expr( + [erl_syntax:clause( + [erl_syntax:list(lists:map(fun make_var/1, State#state.params))], + none, + [erl_syntax:list(EQuery)] + )])), + erl_syntax:record_field( + erl_syntax:atom(format_res), + erl_syntax:fun_expr( + [erl_syntax:clause( + [erl_syntax:list(State#state.res_vars)], + none, + [erl_syntax:tuple(State#state.res)] + )])), + erl_syntax:record_field( + erl_syntax:atom(loc), + erl_syntax:abstract({get(?MOD), State#state.loc})) + ]). + +pack_query([]) -> + []; +pack_query([{str, S1}, {str, S2} | Rest]) -> + pack_query([{str, S1 ++ S2} | Rest]); +pack_query([X | Rest]) -> + [X | pack_query(Rest)]. + + +parse_upsert(Fields) -> + {Fs, _} = + lists:foldr( + fun(F, {Acc, Param}) -> + case erl_syntax:type(F) of + string -> + V = erl_syntax:string_value(F), + {_, _, State} = Res = + parse_upsert_field( + V, Param, erl_syntax:get_pos(F)), + {[Res | Acc], State#state.param_pos}; + _ -> + throw({error, erl_syntax:get_pos(F), + "?SQL_UPSERT field must be " + "a constant string"}) + end + end, {[], 0}, Fields), + %io:format("asd ~p~n", [{Fields, Fs}]), + Fs. + +parse_upsert_field([$! | S], ParamPos, Loc) -> + {Name, ParseState} = parse_upsert_field1(S, [], ParamPos, Loc), + {Name, true, ParseState}; +parse_upsert_field(S, ParamPos, Loc) -> + {Name, ParseState} = parse_upsert_field1(S, [], ParamPos, Loc), + {Name, false, ParseState}. + +parse_upsert_field1([], _Acc, _ParamPos, Loc) -> + throw({error, Loc, + "?SQL_UPSERT fields must have the " + "following form: \"[!]name=value\""}); +parse_upsert_field1([$= | S], Acc, ParamPos, Loc) -> + {lists:reverse(Acc), parse(S, ParamPos, Loc)}; +parse_upsert_field1([C | S], Acc, ParamPos, Loc) -> + parse_upsert_field1(S, [C | Acc], ParamPos, Loc). + + +make_sql_upsert(Table, ParseRes, Pos) -> + check_upsert(ParseRes, Pos), + erl_syntax:fun_expr( + [erl_syntax:clause( + [erl_syntax:atom(pgsql), erl_syntax:variable("__Version")], + [erl_syntax:infix_expr( + erl_syntax:variable("__Version"), + erl_syntax:operator('>='), + erl_syntax:integer(90100))], + [make_sql_upsert_pgsql901(Table, ParseRes), + erl_syntax:atom(ok)]), + erl_syntax:clause( + [erl_syntax:underscore(), erl_syntax:underscore()], + none, + [make_sql_upsert_generic(Table, ParseRes)]) + ]). + +make_sql_upsert_generic(Table, ParseRes) -> + Update = make_sql_query(make_sql_upsert_update(Table, ParseRes)), + Insert = make_sql_query(make_sql_upsert_insert(Table, ParseRes)), + InsertBranch = + erl_syntax:case_expr( + erl_syntax:application( + erl_syntax:atom(ejabberd_odbc), + erl_syntax:atom(sql_query_t), + [Insert]), + [erl_syntax:clause( + [erl_syntax:abstract({updated, 1})], + none, + [erl_syntax:atom(ok)]), + erl_syntax:clause( + [erl_syntax:variable("__UpdateRes")], + none, + [erl_syntax:variable("__UpdateRes")])]), + erl_syntax:case_expr( + erl_syntax:application( + erl_syntax:atom(ejabberd_odbc), + erl_syntax:atom(sql_query_t), + [Update]), + [erl_syntax:clause( + [erl_syntax:abstract({updated, 1})], + none, + [erl_syntax:atom(ok)]), + erl_syntax:clause( + [erl_syntax:underscore()], + none, + [InsertBranch])]). + +make_sql_upsert_update(Table, ParseRes) -> + WPairs = + lists:flatmap( + fun({_Field, false, _ST}) -> + []; + ({Field, true, ST}) -> + [ST#state{ + 'query' = [{str, Field}, {str, "="}] ++ ST#state.'query' + }] + end, ParseRes), + Where = join_states(WPairs, " AND "), + SPairs = + lists:flatmap( + fun({_Field, true, _ST}) -> + []; + ({Field, false, ST}) -> + [ST#state{ + 'query' = [{str, Field}, {str, "="}] ++ ST#state.'query' + }] + end, ParseRes), + Set = join_states(SPairs, ", "), + State = + concat_states( + [#state{'query' = [{str, "UPDATE "}, {str, Table}, {str, " SET "}]}, + Set, + #state{'query' = [{str, " WHERE "}]}, + Where + ]), + State. + +make_sql_upsert_insert(Table, ParseRes) -> + Vals = + lists:map( + fun({_Field, _, ST}) -> + ST + end, ParseRes), + Fields = + lists:map( + fun({Field, _, _ST}) -> + #state{'query' = [{str, Field}]} + end, ParseRes), + State = + concat_states( + [#state{'query' = [{str, "INSERT INTO "}, {str, Table}, {str, "("}]}, + join_states(Fields, ", "), + #state{'query' = [{str, ") VALUES ("}]}, + join_states(Vals, ", "), + #state{'query' = [{str, ")"}]} + ]), + State. + +make_sql_upsert_pgsql901(Table, ParseRes) -> + Update = make_sql_upsert_update(Table, ParseRes), + Vals = + lists:map( + fun({_Field, _, ST}) -> + ST + end, ParseRes), + Fields = + lists:map( + fun({Field, _, _ST}) -> + #state{'query' = [{str, Field}]} + end, ParseRes), + Insert = + concat_states( + [#state{'query' = [{str, "INSERT INTO "}, {str, Table}, {str, "("}]}, + join_states(Fields, ", "), + #state{'query' = [{str, ") SELECT "}]}, + join_states(Vals, ", "), + #state{'query' = [{str, " WHERE NOT EXISTS (SELECT * FROM upsert)"}]} + ]), + State = + concat_states( + [#state{'query' = [{str, "WITH upsert AS ("}]}, + Update, + #state{'query' = [{str, " RETURNING *) "}]}, + Insert + ]), + Upsert = make_sql_query(State), + erl_syntax:application( + erl_syntax:atom(ejabberd_odbc), + erl_syntax:atom(sql_query_t), + [Upsert]). + + +check_upsert(ParseRes, Pos) -> + Set = + lists:filter( + fun({_Field, Match, _ST}) -> + not Match + end, ParseRes), + case Set of + [] -> + throw({error, Pos, + "No ?SQL_UPSERT fields to set, use INSERT instead"}); + _ -> + ok + end, + ok. + + +concat_states(States) -> + lists:foldr( + fun(ST11, ST2) -> + ST1 = resolve_vars(ST11, ST2), + ST1#state{ + 'query' = ST1#state.'query' ++ ST2#state.'query', + params = ST1#state.params ++ ST2#state.params, + args = ST1#state.args ++ ST2#state.args, + res = ST1#state.res ++ ST2#state.res, + res_vars = ST1#state.res_vars ++ ST2#state.res_vars, + loc = case ST1#state.loc of + undefined -> ST2#state.loc; + _ -> ST1#state.loc + end + } + end, #state{}, States). + +resolve_vars(ST1, ST2) -> + Max = lists:max([0 | ST1#state.params ++ ST2#state.params]), + {Map, _} = + lists:foldl( + fun(Var, {Acc, New}) -> + case lists:member(Var, ST2#state.params) of + true -> + {dict:store(Var, New, Acc), New + 1}; + false -> + {Acc, New} + end + end, {dict:new(), Max + 1}, ST1#state.params), + NewParams = + lists:map( + fun(Var) -> + case dict:find(Var, Map) of + {ok, New} -> + New; + error -> + Var + end + end, ST1#state.params), + NewQuery = + lists:map( + fun({var, Var}) -> + case dict:find(Var, Map) of + {ok, New} -> + {var, New}; + error -> + {var, Var} + end; + (S) -> S + end, ST1#state.'query'), + ST1#state{params = NewParams, 'query' = NewQuery}. + + +join_states([], _Sep) -> + #state{}; +join_states([H | T], Sep) -> + J = [[H] | [[#state{'query' = [{str, Sep}]}, X] || X <- T]], + concat_states(lists:append(J)). + + +set_pos(Tree, Pos) -> + erl_syntax_lib:map( + fun(Node) -> + case erl_syntax:get_pos(Node) of + 0 -> erl_syntax:set_pos(Node, Pos); + _ -> Node + end + end, Tree). diff --git a/src/ejabberd_stun.erl b/src/ejabberd_stun.erl index 11347d60..3c91117d 100644 --- a/src/ejabberd_stun.erl +++ b/src/ejabberd_stun.erl @@ -6,7 +6,7 @@ %%% @end %%% Created : 8 May 2014 by Evgeny Khramtsov <ekhramtsov@process-one.net> %%% -%%% ejabberd, Copyright (C) 2013-2015 ProcessOne +%%% ejabberd, Copyright (C) 2013-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,8 +25,11 @@ %%%------------------------------------------------------------------- -module(ejabberd_stun). -%% API --export([tcp_init/2, udp_init/2, udp_recv/5, start/2, socket_type/0]). +-protocol({rfc, 5766}). +-protocol({xep, 176, '1.0'}). + +-export([tcp_init/2, udp_init/2, udp_recv/5, start/2, + socket_type/0]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -35,11 +38,11 @@ %%% API %%%=================================================================== tcp_init(Socket, Opts) -> - ejabberd:start_app(p1_stun), + ejabberd:start_app(stun), stun:tcp_init(Socket, prepare_turn_opts(Opts)). udp_init(Socket, Opts) -> - ejabberd:start_app(p1_stun), + ejabberd:start_app(stun), stun:udp_init(Socket, prepare_turn_opts(Opts)). udp_recv(Socket, Addr, Port, Packet, Opts) -> diff --git a/src/ejabberd_sup.erl b/src/ejabberd_sup.erl index da25af2c..56dccdcd 100644 --- a/src/ejabberd_sup.erl +++ b/src/ejabberd_sup.erl @@ -5,7 +5,7 @@ %%% Created : 31 Jan 2003 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -97,21 +97,6 @@ init([]) -> infinity, supervisor, [ejabberd_listener]}, - ReceiverSupervisor = - {ejabberd_receiver_sup, - {ejabberd_tmp_sup, start_link, - [ejabberd_receiver_sup, ejabberd_receiver]}, - permanent, - infinity, - supervisor, - [ejabberd_tmp_sup]}, - C2SSupervisor = - {ejabberd_c2s_sup, - {ejabberd_tmp_sup, start_link, [ejabberd_c2s_sup, ejabberd_c2s]}, - permanent, - infinity, - supervisor, - [ejabberd_tmp_sup]}, S2SInSupervisor = {ejabberd_s2s_in_sup, {ejabberd_tmp_sup, start_link, @@ -136,14 +121,6 @@ init([]) -> infinity, supervisor, [ejabberd_tmp_sup]}, - HTTPSupervisor = - {ejabberd_http_sup, - {ejabberd_tmp_sup, start_link, - [ejabberd_http_sup, ejabberd_http]}, - permanent, - infinity, - supervisor, - [ejabberd_tmp_sup]}, FrontendSocketSupervisor = {ejabberd_frontend_socket_sup, {ejabberd_tmp_sup, start_link, @@ -169,12 +146,9 @@ init([]) -> S2S, Local, Captcha, - ReceiverSupervisor, - C2SSupervisor, S2SInSupervisor, S2SOutSupervisor, ServiceSupervisor, - HTTPSupervisor, IQSupervisor, FrontendSocketSupervisor, Listener]}}. diff --git a/src/ejabberd_system_monitor.erl b/src/ejabberd_system_monitor.erl index df8d9d51..3f6c0566 100644 --- a/src/ejabberd_system_monitor.erl +++ b/src/ejabberd_system_monitor.erl @@ -5,7 +5,7 @@ %%% Created : 21 Mar 2007 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,17 +25,18 @@ -module(ejabberd_system_monitor). +-behaviour(ejabberd_config). + -author('alexey@process-one.net'). -behaviour(gen_server). %% API --export([start_link/0, process_command/3, +-export([start_link/0, process_command/3, register_hook/1, process_remote_command/1]). -%% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, - handle_info/2, terminate/2, code_change/3]). + handle_info/2, terminate/2, code_change/3, opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -67,10 +68,10 @@ process_command(From, To, Packet) -> case Name of <<"message">> -> LFrom = - jlib:jid_tolower(jlib:jid_remove_resource(From)), + jid:tolower(jid:remove_resource(From)), case lists:member(LFrom, get_admin_jids()) of true -> - Body = xml:get_path_s(Packet, + Body = fxml:get_path_s(Packet, [{elem, <<"body">>}, cdata]), spawn(fun () -> process_flag(priority, high), @@ -84,6 +85,10 @@ process_command(From, To, Packet) -> _ -> ok end. +register_hook(Host) -> + ejabberd_hooks:add(local_send_to_resource_hook, Host, + ?MODULE, process_command, 50). + %%==================================================================== %% gen_server callbacks %%==================================================================== @@ -99,11 +104,7 @@ init(Opts) -> LH = proplists:get_value(large_heap, Opts), process_flag(priority, high), erlang:system_monitor(self(), [{large_heap, LH}]), - lists:foreach(fun (Host) -> - ejabberd_hooks:add(local_send_to_resource_hook, Host, - ?MODULE, process_command, 50) - end, - ?MYHOSTS), + lists:foreach(fun register_hook/1, ?MYHOSTS), {ok, #state{}}. %%-------------------------------------------------------------------- @@ -184,26 +185,32 @@ process_large_heap(Pid, Info) -> io_lib:format("(~w) The process ~w is consuming too " "much memory:~n~p~n~s", [node(), Pid, Info, DetailedInfo])), - From = jlib:make_jid(<<"">>, Host, <<"watchdog">>), + From = jid:make(<<"">>, Host, <<"watchdog">>), + Hint = [#xmlel{name = <<"no-permanent-store">>, + attrs = [{<<"xmlns">>, ?NS_HINTS}]}], lists:foreach(fun (JID) -> - send_message(From, jlib:make_jid(JID), Body) + send_message(From, jid:make(JID), Body, Hint) end, JIDs). send_message(From, To, Body) -> + send_message(From, To, Body, []). + +send_message(From, To, Body, ExtraEls) -> ejabberd_router:route(From, To, #xmlel{name = <<"message">>, attrs = [{<<"type">>, <<"chat">>}], children = [#xmlel{name = <<"body">>, attrs = [], children = - [{xmlcdata, Body}]}]}). + [{xmlcdata, Body}]} + | ExtraEls]}). get_admin_jids() -> ejabberd_config:get_option( watchdog_admins, fun(JIDs) -> - [jlib:jid_tolower( - jlib:string_to_jid( + [jid:tolower( + jid:from_string( iolist_to_binary(S))) || S <- JIDs] end, []). @@ -244,8 +251,9 @@ s2s_out_info(Pid) -> [<<"Process type: s2s_out">>, case FromTo of [{From, To}] -> - list_to_binary(io_lib:format("\nS2S connection: from ~s to ~s", - [From, To])); + <<"\n", + (io_lib:format("S2S connection: from ~s to ~s", + [From, To]))/binary>>; _ -> <<"">> end, check_send_queue(Pid), <<"\n">>, @@ -309,7 +317,7 @@ help() -> "<node>\n setlh <node> <integer>">>. remote_command(Node, Args, From, To) -> - Message = case rpc:call(Node, ?MODULE, + Message = case ejabberd_cluster:call(Node, ?MODULE, process_remote_command, [Args]) of {badrpc, Reason} -> @@ -331,3 +339,12 @@ process_remote_command([setlh, NewValue]) -> io_lib:format("Result of set large heap: ~p --> ~p", [OldLH, NewLH]); process_remote_command(_) -> throw(unknown_command). + +opt_type(watchdog_admins) -> + fun (JIDs) -> + [jid:tolower(jid:from_string(iolist_to_binary(S))) + || S <- JIDs] + end; +opt_type(watchdog_large_heap) -> + fun (I) when is_integer(I), I > 0 -> I end; +opt_type(_) -> [watchdog_admins, watchdog_large_heap]. diff --git a/src/ejabberd_tmp_sup.erl b/src/ejabberd_tmp_sup.erl index 5852e666..8b9f4fc1 100644 --- a/src/ejabberd_tmp_sup.erl +++ b/src/ejabberd_tmp_sup.erl @@ -5,7 +5,7 @@ %%% Created : 18 Jul 2003 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_update.erl b/src/ejabberd_update.erl index afcb6222..75ccc3de 100644 --- a/src/ejabberd_update.erl +++ b/src/ejabberd_update.erl @@ -5,7 +5,7 @@ %%% Created : 27 Jan 2006 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -67,9 +67,6 @@ update(ModulesToUpdate) -> {error, Reason} end. -%% OTP R14B03 and older provided release_handler_1:eval_script/3 -%% But OTP R14B04 and newer provide release_handler_1:eval_script/5 -%% Dialyzer reports a call to missing function; don't worry. eval_script(Script, Apps, LibDirs) -> release_handler_1:eval_script(Script, Apps, LibDirs, [], []). @@ -138,17 +135,17 @@ build_script(Dir, UpdatedBeams) -> LowLevelScript, [{ejabberd, "", filename:join(Dir, "..")}]), Check1 = case Check of - {ok, []} -> - ?DEBUG("script: ~p~n", [Script]), - ?DEBUG("low level script: ~p~n", [LowLevelScript]), - ?DEBUG("check: ~p~n", [Check]), - ok; - _ -> - ?ERROR_MSG("script: ~p~n", [Script]), - ?ERROR_MSG("low level script: ~p~n", [LowLevelScript]), - ?ERROR_MSG("check: ~p~n", [Check]), - error - end, + {ok, []} -> + ?DEBUG("script: ~p~n", [Script]), + ?DEBUG("low level script: ~p~n", [LowLevelScript]), + ?DEBUG("check: ~p~n", [Check]), + ok; + _ -> + ?ERROR_MSG("script: ~p~n", [Script]), + ?ERROR_MSG("low level script: ~p~n", [LowLevelScript]), + ?ERROR_MSG("check: ~p~n", [Check]), + error + end, {Script, LowLevelScript, Check1}. %% Copied from Erlang/OTP file: lib/sasl/src/systools.hrl diff --git a/src/ejabberd_web.erl b/src/ejabberd_web.erl index 61cd079a..459423aa 100644 --- a/src/ejabberd_web.erl +++ b/src/ejabberd_web.erl @@ -6,7 +6,7 @@ %%% Created : 28 Feb 2004 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/ejabberd_web_admin.erl b/src/ejabberd_web_admin.erl index 7cf15210..f525a4d3 100644 --- a/src/ejabberd_web_admin.erl +++ b/src/ejabberd_web_admin.erl @@ -5,7 +5,7 @@ %%% Created : 9 Apr 2004 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -27,12 +27,13 @@ -module(ejabberd_web_admin). +-behaviour(ejabberd_config). + -author('alexey@process-one.net'). -%% External exports -export([process/2, list_users/4, list_users_in_diapason/4, pretty_print_xml/1, - term_to_id/1]). + term_to_id/1, opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -94,7 +95,7 @@ is_acl_match(Host, Rules, Jid) -> get_jid(Auth, HostHTTP, Method) -> case get_auth_admin(Auth, HostHTTP, [], Method) of {ok, {User, Server}} -> - jlib:make_jid(User, Server, <<"">>); + jid:make(User, Server, <<"">>); {unauthorized, Error} -> ?ERROR_MSG("Unauthorized ~p: ~p", [Auth, Error]), throw({unauthorized, Auth}) @@ -166,15 +167,15 @@ process([<<"doc">>, LocalFile], _Request) -> ?DEBUG("Delivering content.", []), {200, [{<<"Server">>, <<"ejabberd">>}], FileContents}; {error, Error} -> - ?DEBUG("Delivering error: ~p", [Error]), Help = <<" ", FileName/binary, " - Try to specify the path to ejabberd " "documentation with the environment variable " "EJABBERD_DOC_PATH. Check the ejabberd " "Guide for more information.">>, + ?INFO_MSG("Problem '~p' accessing the local Guide file ~s", [Error, Help]), case Error of eacces -> {403, [], <<"Forbidden", Help/binary>>}; - enoent -> {404, [], <<"Not found", Help/binary>>}; + enoent -> {307, [{<<"Location">>, <<"http://docs.ejabberd.im/admin/guide/configuration/">>}], <<"Not found", Help/binary>>}; _Else -> {404, [], <<(iolist_to_binary(atom_to_list(Error)))/binary, Help/binary>>} end @@ -183,7 +184,7 @@ process([<<"server">>, SHost | RPath] = Path, #request{auth = Auth, lang = Lang, host = HostHTTP, method = Method} = Request) -> - Host = jlib:nameprep(SHost), + Host = jid:nameprep(SHost), case lists:member(Host, ?MYHOSTS) of true -> case get_auth_admin(Auth, HostHTTP, Path, Method) of @@ -202,7 +203,7 @@ process([<<"server">>, SHost | RPath] = Path, {unauthorized, Error} -> {BadUser, _BadPass} = Auth, {IPT, _Port} = Request#request.ip, - IPS = jlib:ip_to_list(IPT), + IPS = ejabberd_config:may_hide_data(jlib:ip_to_list(IPT)), ?WARNING_MSG("Access of ~p from ~p failed with error: ~p", [BadUser, IPS, Error]), {401, @@ -234,7 +235,7 @@ process(RPath, {unauthorized, Error} -> {BadUser, _BadPass} = Auth, {IPT, _Port} = Request#request.ip, - IPS = jlib:ip_to_list(IPT), + IPS = ejabberd_config:may_hide_data(jlib:ip_to_list(IPT)), ?WARNING_MSG("Access of ~p from ~p failed with error: ~p", [BadUser, IPS, Error]), {401, @@ -249,7 +250,7 @@ get_auth_admin(Auth, HostHTTP, RPath, Method) -> case Auth of {SJID, Pass} -> {HostOfRule, AccessRule} = get_acl_rule(RPath, Method), - case jlib:string_to_jid(SJID) of + case jid:from_string(SJID) of error -> {unauthorized, <<"badformed-jid">>}; #jid{user = <<"">>, server = User} -> get_auth_account(HostOfRule, AccessRule, User, HostHTTP, @@ -266,7 +267,7 @@ get_auth_account(HostOfRule, AccessRule, User, Server, case ejabberd_auth:check_password(User, <<"">>, Server, Pass) of true -> case is_acl_match(HostOfRule, AccessRule, - jlib:make_jid(User, Server, <<"">>)) + jid:make(User, Server, <<"">>)) of false -> {unauthorized, <<"unprivileged-account">>}; true -> {ok, {User, Server}} @@ -295,7 +296,7 @@ make_xhtml(Els, Host, Node, Lang, JID) -> #xmlel{name = <<"html">>, attrs = [{<<"xmlns">>, <<"http://www.w3.org/1999/xhtml">>}, - {<<"xml:lang">>, Lang}, {<<"lang">>, Lang}], + {<<"xml:lang">>, Lang}, {<<"lang">>, Lang}]++direction(Lang), children = [#xmlel{name = <<"head">>, attrs = [], children = @@ -339,8 +340,15 @@ make_xhtml(Els, Host, Node, Lang, JID) -> [{xmlcdata, <<"">>}])]), ?XAE(<<"div">>, [{<<"id">>, <<"copyrightouter">>}], [?XAE(<<"div">>, [{<<"id">>, <<"copyright">>}], - [?XC(<<"p">>, - <<"ejabberd (c) 2002-2015 ProcessOne">>)])])])]}}. + [?XE(<<"p">>, + [?AC(<<"https://www.ejabberd.im/">>, <<"ejabberd">>), + ?C(<<" (c) 2002-2016 ">>), + ?AC(<<"https://www.process-one.net/">>, <<"ProcessOne, leader in messaging and push solutions">>)] + )])])])]}}. + +direction(ltr) -> [{<<"dir">>, <<"ltr">>}]; +direction(<<"he">>) -> [{<<"dir">>, <<"rtl">>}]; +direction(_) -> []. get_base_path(global, cluster) -> <<"/admin/">>; get_base_path(Host, cluster) -> @@ -368,212 +376,291 @@ additions_js() -> css(Host) -> Base = get_base_path(Host, cluster), - <<"\nhtml,body {\n background: white;\n " - " margin: 0;\n padding: 0;\n height: " - "100%;\n}\n\n#container {\n padding: " - "0;\n margin: 0;\n min-height: 100%;\n " - " height: 100%;\n margin-bottom: -30px;\n}\n\n" - "html>body #container {\n height: auto;\n}\n\n" - "#header h1 {\n width: 100%;\n height: " - "55px;\n padding: 0;\n margin: 0;\n " - " background: transparent url(\"", - Base/binary, - "logo-fill.png\");\n}\n\n#header h1 a " - "{\n position: absolute;\n top: 0;\n " - " left: 0;\n width: 100%;\n height: " - "55px;\n padding: 0;\n margin: 0;\n " - " background: transparent url(\"", - Base/binary, - "logo.png\") no-repeat;\n display: block;\n " - " text-indent: -700em;\n}\n\n#clearcopyright " - "{\n display: block;\n width: 100%;\n " - " height: 30px;\n}\n\n#copyrightouter " - "{\n display: table;\n width: 100%;\n " - " height: 30px;\n}\n\n#copyright {\n " - " display: table-cell;\n vertical-align: " - "bottom;\n width: 100%;\n height: 30px;\n}\n\n" - "#copyright p {\n margin-left: 0;\n " - " margin-right: 0;\n margin-top: 5px;\n " - " margin-bottom: 0;\n padding-left: " - "0;\n padding-right: 0;\n padding-top: " - "1px;\n padding-bottom: 1px;\n width: " - "100%;\n color: #ffffff;\n background-color: " - "#fe8a00;\n font-family: Verdana, Arial, " - "Helvetica, sans-serif; \n font-size: " - "7pt;\n font-weight: bold;\n text-align: " - "center;\n}\n\n#navigation ul {\n position: " - "absolute;\n top: 65px;\n left: 0;\n " - " padding: 0 1px 1px 1px;\n margin: " - "0;\n font-family: Verdana, Arial, Helvetica, " - "sans-serif; \n font-size: 8pt;\n font-weigh" - "t: bold;\n border-top: 1px solid #d47911;\n " - " width: 17em;\n}\n\n#navigation ul li " - "{\n list-style: none;\n margin: 0;\n " - " text-align: left;\n display: inline;\n}\n\n" - "#navigation ul li a {\n margin: 0;\n " - " display: block;\n padding: 3px 6px " - "3px 9px;\n border-left: 1em solid #ffc78c;\n " - " border-right: 1px solid #d47911;\n " - " border-bottom: 1px solid #d47911;\n " - " background: #ffe3c9;\n text-decoration: " - "none;\n}\n\n#navigation ul li a:link " - "{\n color: #844;\n}\n\n#navigation " - "ul li a:visited {\n color: #766;\n}\n\n#navig" - "ation ul li a:hover {\n border-color: " - "#fc8800;\n color: #FFF;\n background: " - "#332;\n}\n\nul li #navhead a, ul li " - "#navheadsub a, ul li #navheadsubsub " - "a {\n text-align: center;\n border-top: " - "1px solid #d47911;\n border-bottom: " - "2px solid #d47911;\n background: #FED6A6;\n}\n\n" - "#navheadsub, #navitemsub {\n border-left: " - "7px solid white;\n margin-left: 2px;\n}\n\n#" - "navheadsubsub, #navitemsubsub {\n border-lef" - "t: 14px solid white;\n margin-left: " - "4px;\n}\n\n#lastactivity li {\n font-weight: " - "bold;\n border: 1px solid #d6760e;\n " - " background-color: #fff2e8;\n padding: " - "2px;\n margin-bottom: -1px;\n}\n\ntd.copy " - "{\n color: #ffffff;\n background-color: " - "#fe8a00;\n font-family: Verdana, Arial, " - "Helvetica, sans-serif; \n font-size: " - "7pt;\n font-weight: bold;\n text-align: " - "center;\n}\n\ninput {\n font-family: " - "Verdana, Arial, Helvetica, sans-serif; " - "\n font-size: 10pt;\n border: 1px " - "solid #d6760e;\n color: #723202;\n " - " background-color: #fff2e8;\n vertical-align" - ": middle;\n margin-bottom: 0px;\n " - "padding: 0.1em;\n}\n\ninput[type=submit] " - "{\n font-family: Verdana, Arial, Helvetica, " - "sans-serif; \n font-size: 8pt;\n font-weigh" - "t: bold;\n color: #ffffff;\n background-col" - "or: #fe8a00;\n border: 1px solid #d6760e;\n}\n\n" - "textarea {\n font-family: Verdana, " - "Arial, Helvetica, sans-serif; \n font-size: " - "10pt;\n border: 1px solid #d6760e;\n " - " color: #723202;\n background-color: " - "#fff2e8;\n}\n\nselect {\n border: 1px " - "solid #d6760e;\n color: #723202;\n " - " background-color: #fff2e8;\n vertical-align" - ": middle;\n margin-bottom: 0px; \n " - " padding: 0.1em;\n}\n\nthead {\n color: " - "#000000;\n background-color: #ffffff;\n " - " font-family: Verdana, Arial, Helvetica, " - "sans-serif; \n font-size: 10pt;\n " - "font-weight: bold;\n}\n\ntr.head {\n " - " color: #ffffff;\n background-color: " - "#3b547a;\n font-family: Verdana, Arial, " - "Helvetica, sans-serif; \n font-size: " - "9pt;\n font-weight: bold;\n text-align: " - "center;\n}\n\ntr.oddraw {\n color: " - "#412c75;\n background-color: #ccd4df;\n " - " font-family: Verdana, Arial, Helvetica, " - "sans-serif; \n font-size: 9pt;\n font-weigh" - "t: normal;\n text-align: center;\n}\n\ntr.ev" - "enraw {\n color: #412c75;\n background-colo" - "r: #dbe0e8;\n font-family: Verdana, " - "Arial, Helvetica, sans-serif; \n font-size: " - "9pt;\n font-weight: normal;\n text-align: " - "center;\n}\n\ntd.leftheader {\n color: " - "#412c75;\n background-color: #ccccc1;\n " - " font-family: Verdana, Arial, Helvetica, " - "sans-serif; \n font-size: 9pt;\n font-weigh" - "t: bold;\n padding-left: 5px;\n padding-top" - ": 2px;\n padding-bottom: 2px;\n margin-top: " - "0px;\n margin-bottom: 0px;\n}\n\ntd.leftcont" - "ent {\n color: #000044;\n background-color: " - "#e6e6df;\n font-family: Verdana, Arial, " - "Helvetica, sans-serif; \n font-size: " - "7pt;\n font-weight: normal;\n padding-left: " - "5px;\n padding-right: 5px;\n padding-top: " - "2px;\n padding-bottom: 2px;\n margin-top: " - "0px;\n margin-bottom: 0px;\n}\n\ntd.rightcon" - "tent {\n color: #000044;\n font-family: " - "Verdana, Arial, Helvetica, sans-serif; " - "\n font-size: 10pt;\n font-weight: " - "normal;\n text-align: justify;\n padding-le" - "ft: 10px;\n padding-right: 10px;\n " - " padding-bottom: 5px;\n}\n\n\nh1 {\n " - " color: #000044;\n font-family: Verdana, " - "Arial, Helvetica, sans-serif; \n font-size: " - "14pt;\n font-weight: bold;\n text-align: " - "center;\n padding-top: 2px;\n padding-botto" - "m: 2px;\n margin-top: 0px;\n margin-bottom: " - "0px;\n}\n\nh2 {\n color: #000044;\n " - " font-family: Verdana, Arial, Helvetica, " - "sans-serif; \n font-size: 12pt;\n " - "font-weight: bold;\n text-align: center;\n " - " padding-top: 2px;\n padding-bottom: " - "2px;\n margin-top: 0px;\n margin-bottom: " - "0px;\n}\n\nh3 {\n color: #000044;\n " - " font-family: Verdana, Arial, Helvetica, " - "sans-serif; \n font-size: 10pt;\n " - "font-weight: bold;\n text-align: left;\n " - " padding-top: 20px;\n padding-bottom: " - "2px;\n margin-top: 0px;\n margin-bottom: " - "0px;\n}\n\n#content a:link {\n color: " - "#990000; \n font-family: Verdana, Arial, " - "Helvetica, sans-serif; \n font-size: " - "10pt;\n font-weight: bold;\n text-decoratio" - "n: underline;\n}\n#content a:visited " - "{\n color: #990000; \n font-family: " - "Verdana, Arial, Helvetica, sans-serif; " - "\n font-size: 10pt;\n font-weight: " - "bold;\n text-decoration: underline;\n}\n#con" - "tent a:hover {\n color: #cc6600; \n " - " font-family: Verdana, Arial, Helvetica, " - "sans-serif; \n font-size: 10pt;\n " - "font-weight: bold;\n text-decoration: " - "underline;\n}\n\n\n#content ul li {\n " - " list-style-type: disc;\n font-size: " - "10pt;\n /*font-size: 7pt;*/\n padding-left: " - "10px;\n}\n\n#content ul.nolistyle>li " - "{\n list-style-type: none;\n}\n\n#content " - "li.big {\n font-size: 10pt;\n}\n\n#content " - "{\n font-family: Verdana, Arial, Helvetica, " - "sans-serif; \n font-size: 10pt;\n " - "padding-left: 17em;\n padding-top: " - "5px;\n}\n\ndiv.guidelink {\n text-align: " - "right;\n padding-right: 1em;\n}\n\ntable.wit" - "htextareas>tbody>tr>td {\n vertical-align: " - "top;\n}\n\np.result {\n border: 1px;\n " - " border-style: dashed;\n border-color: " - "#FE8A02;\n padding: 1em;\n margin-right: " - "1em;\n background: #FFE3C9;\n}\n\n*.alignrig" - "ht {\n font-size: 10pt;\n text-align: " - "right;\n}\n\n">>. + <<"html,body {\n" + " margin: 0;\n" + " padding: 0;\n" + " height: 100%;\n" + " background: #f9f9f9;\n" + " font-family: sans-serif;\n" + "}\n" + "body {\n" + " min-width: 990px;\n" + "}\n" + "a {\n" + " text-decoration: none;\n" + " color: #3eaffa;\n" + "}\n" + "a:hover,\n" + "a:active {\n" + " text-decoration: underline;\n" + "}\n" + "#container {\n" + " position: relative;\n" + " padding: 0;\n" + " margin: 0 auto;\n" + " max-width: 1280px;\n" + " min-height: 100%;\n" + " height: 100%;\n" + " margin-bottom: -30px;\n" + " z-index: 1;\n" + "}\n" + "html>body #container {\n" + " height: auto;\n" + "}\n" + "#header h1 {\n" + " width: 100%;\n" + " height: 50px;\n" + " padding: 0;\n" + " margin: 0;\n" + " background-color: #49cbc1;\n" + "}\n" + "#header h1 a {\n" + " position: absolute;\n" + " top: 0;\n" + " left: 0;\n" + " width: 100%;\n" + " height: 50px;\n" + " padding: 0;\n" + " margin: 0;\n" + " background: url('",Base/binary,"logo.png') 10px center no-repeat transparent;\n" + " background-size: auto 25px;\n" + " display: block;\n" + " text-indent: -9999px;\n" + "}\n" + "#clearcopyright {\n" + " display: block;\n" + " width: 100%;\n" + " height: 30px;\n" + "}\n" + "#copyrightouter {\n" + " position: relative;\n" + " display: table;\n" + " width: 100%;\n" + " height: 30px;\n" + " z-index: 2;\n" + "}\n" + "#copyright {\n" + " display: table-cell;\n" + " vertical-align: bottom;\n" + " width: 100%;\n" + " height: 30px;\n" + "}\n" + "#copyright a {\n" + " font-weight: bold;\n" + " color: #fff;\n" + "}\n" + "#copyright p {\n" + " margin-left: 0;\n" + " margin-right: 0;\n" + " margin-top: 5px;\n" + " margin-bottom: 0;\n" + " padding-left: 0;\n" + " padding-right: 0;\n" + " padding-top: 5px;\n" + " padding-bottom: 5px;\n" + " width: 100%;\n" + " color: #fff;\n" + " background-color: #30353E;\n" + " font-size: 0.75em;\n" + " text-align: center;\n" + "}\n" + "#navigation {\n" + " display: inline-block;\n" + " vertical-align: top;\n" + " width: 30%;\n" + "}\n" + "#navigation ul {\n" + " padding: 0;\n" + " margin: 0;\n" + " width: 90%;\n" + " background: #fff;\n" + "}\n" + "#navigation ul li {\n" + " list-style: none;\n" + " margin: 0;\n" + "\n" + " border-bottom: 1px solid #f9f9f9;\n" + " text-align: left;\n" + "}\n" + "#navigation ul li a {\n" + " margin: 0;\n" + " display: inline-block;\n" + " padding: 10px;\n" + " color: #333;\n" + "}\n" + "ul li #navhead a, ul li #navheadsub a, ul li #navheadsubsub a {\n" + " font-size: 1.5em;\n" + " color: inherit;\n" + "}\n" + "#navitemsub {\n" + " border-left: 0.5em solid #424a55;\n" + "}\n" + "#navitemsubsub {\n" + " border-left: 2em solid #424a55;\n" + "}\n" + "#navheadsub,\n" + "#navheadsubsub {\n" + " padding-left: 0.5em;\n" + "}\n" + "#navhead,\n" + "#navheadsub,\n" + "#navheadsubsub {\n" + " border-top: 3px solid #49cbc1;\n" + " background: #424a55;\n" + " color: #fff;\n" + "}\n" + "#lastactivity li {\n" + " padding: 2px;\n" + " margin-bottom: -1px;\n" + "}\n" + "thead tr td {\n" + " background: #3eaffa;\n" + " color: #fff;\n" + "}\n" + "thead tr td a {\n" + " color: #fff;\n" + "}\n" + "td.copy {\n" + " text-align: center;\n" + "}\n" + "tr.head {\n" + " color: #fff;\n" + " background-color: #3b547a;\n" + " text-align: center;\n" + "}\n" + "tr.oddraw {\n" + " color: #412c75;\n" + " background-color: #ccd4df;\n" + " text-align: center;\n" + "}\n" + "tr.evenraw {\n" + " color: #412c75;\n" + " background-color: #dbe0e8;\n" + " text-align: center;\n" + "}\n" + "td.leftheader {\n" + " color: #412c75;\n" + " background-color: #ccccc1;\n" + " padding-left: 5px;\n" + " padding-top: 2px;\n" + " padding-bottom: 2px;\n" + " margin-top: 0px;\n" + " margin-bottom: 0px;\n" + "}\n" + "td.leftcontent {\n" + " color: #000044;\n" + " background-color: #e6e6df;\n" + " padding-left: 5px;\n" + " padding-right: 5px;\n" + " padding-top: 2px;\n" + " padding-bottom: 2px;\n" + " margin-top: 0px;\n" + " margin-bottom: 0px;\n" + "}\n" + "td.rightcontent {\n" + " color: #000044;\n" + " text-align: justify;\n" + " padding-left: 10px;\n" + " padding-right: 10px;\n" + " padding-bottom: 5px;\n" + "}\n" + "\n" + "h1 {\n" + " color: #000044;\n" + " padding-top: 2px;\n" + " padding-bottom: 2px;\n" + " margin-top: 0px;\n" + " margin-bottom: 0px;\n" + "}\n" + "h2 {\n" + " color: #000044;\n" + " text-align: center;\n" + " padding-top: 2px;\n" + " padding-bottom: 2px;\n" + " margin-top: 0px;\n" + " margin-bottom: 0px;\n" + "}\n" + "h3 {\n" + " color: #000044;\n" + " text-align: left;\n" + " padding-top: 20px;\n" + " padding-bottom: 2px;\n" + " margin-top: 0px;\n" + " margin-bottom: 0px;\n" + "}\n" + "#content ul {\n" + " padding-left: 1.1em;\n" + " margin-top: 1em;\n" + "}\n" + "#content ul li {\n" + " list-style-type: disc;\n" + " padding: 5px;\n" + "}\n" + "#content ul.nolistyle>li {\n" + " list-style-type: none;\n" + "}\n" + "#content {\n" + " display: inline-block;\n" + " vertical-align: top;\n" + " padding-top: 25px;\n" + " width: 70%;\n" + "}\n" + "div.guidelink,\n" + "p[dir=ltr] {\n" + " display: inline-block;\n" + " float: right;\n" + "\n" + " margin: 0;\n" + " margin-right: 1em;\n" + "}\n" + "div.guidelink a,\n" + "p[dir=ltr] a {\n" + " display: inline-block;\n" + " border-radius: 3px;\n" + " padding: 3px;\n" + "\n" + " background: #3eaffa;\n" + "\n" + " text-transform: uppercase;\n" + " font-size: 0.75em;\n" + " color: #fff;\n" + "}\n" + "table {\n" + " margin-top: 1em;\n" + "}\n" + "table tr td {\n" + " padding: 0.5em;\n" + "}\n" + "table tr:nth-child(odd) {\n" + " background: #fff;\n" + "}\n" + "table.withtextareas>tbody>tr>td {\n" + " vertical-align: top;\n" + "}\n" + "textarea {\n" + " margin-bottom: 1em;\n" + "}\n" + "input,\n" + "select {\n" + " font-size: 1em;\n" + "}\n" + "p.result {\n" + " border: 1px;\n" + " border-style: dashed;\n" + " border-color: #FE8A02;\n" + " padding: 1em;\n" + " margin-right: 1em;\n" + " background: #FFE3C9;\n" + "}\n" + "*.alignright {\n" + " text-align: right;\n" + "}">>. favicon() -> - jlib:decode_base64(<<"AAABAAEAEBAQAAEABAAoAQAAFgAAACgAAAAQAAAAIAAAA" - "AEABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJf+cAAI" - "PsAAGC8gAVhecAAIr8ACiR7wBBmOcAUKPsAFun8ABhqeo" - "AgLryAJLB8ACz1PcAv9r7AMvi+gAAAAAAAgICARMhICAk" - "JCQkQkFCQgICN2d2cSMgJCRevdvVQkICAlqYh5MgICQkX" - "rRCQkJCMgI7kiAjICAUFF2swkFBQRQUXazCQUFBAgI7ki" - "AgICAkJF60QkJCQgICOpiHkyAgJCRevdvlQkICAjdndnM" - "gICQkJCRCQkJCAgICARAgICAAAAAAAAAAAAAAAAAAAAAA" - "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - "AAAAAAAAAAAAAAAAAAA">>). + jlib:decode_base64(<<"AAABAAEAEBAAAAEAIAAoBQAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAA1AwMAQwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwMARQUEA+oFAwCOBAQAaAQEAGkEBABpBAQAaQQEAGoFAgBcBAAAOQAAAAoAAAAAAAAAAAAAAAAAAAAAAAAAAAMDAEIHBgX/BwYF/wcGBf8HBgX/BwYF/wcGBf8HBgX/BwYF/wUFA/wEBAHOBQICXgAAAAAAAAAAAAAAAAAAAAADAwBCBwYF/wcGBf8HBgX/BwYF/wcGBf8HBgX/BwYF/wcGBf8HBgX/BwYF/wcGBf8DAwCUAAAABwAAAAAAAAAAAwMAQgcGBf8HBgX/BwYF/wcGBf8FBQPMBAAAaAQAAD8DAwNOAwMDlgUFA/QHBgX/BwYF/wQEAHkAAAAAAAAAAAMDAEIHBgX/BwYF/wcGBf8EBAGeAAAACAAAAAAAAAASAAAABQAAAAAFBQGxBwYF/wcGBf8FBAPvAAAAKAAAAAADAwBCBwYF/wcGBf8EBAHPAAAADQAAACEFBQGuBQQD8AUEAeEFBQGuBQQB9QcGBf8HBgX/BwYF/wQEAH8AAAAAAwMAQgcGBf8HBgX/BgQAbwAAAAADAwOXBQQB3gUFAdgFBQHZBQQB3QUFAdYFBAHhBQUD/gcGBf8EBAK8AAAAAAMDAEIHBgX/BwYF/wQAAD0AAAAAAAAABQAAAAEAAAABAAAAAQAAAAEAAAAFAAAAEQUFArwKBgX/BQMDxQAAAAADAwBCBwYF/wcGBf8DAwBKAAAAAwYDAFAGAwBVBgMAVAYDAFQFAgJZAAAALwAAAAAFBQGuCgYF/wUDA8QAAAAAAAAAKwUEA/QHBgX/AwMDlgAAAAAFAwOIBwYF/wcGBf8HBgX/BQQB5wAAADMAAAAWBQUD5wcGBf8EBAGbAAAAAAAAAAYFBAG9BwYF/wUFA/EDAABAAAAAAAMDA1QDAwOYBQUAhQAAACQAAAAABAQBnQcGBf8HBgX/AwMATQAAAAAAAAAAAwAAQwUFA/oHBgX/BQQB5QYDA1UAAAAAAAAAAAAAAAAAAAAXAwMAlwcGBf8HBgX/BQUBtQAAAAcAAAAAAAAAAAAAAAAEBABzBQUD/gcGBf8HBgX/BQMDyQQEAZwGBAGqBQQB5AcGBf8HBgX/BAQB0QAAACEAAAAAAAAAAAAAAAAAAAAAAAAAAAUFAmQFBAHlBwYF/wcGBf8HBgX/BwYF/wcGBf8FBQP+BQUBsAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwUFA40FBAHrBwYF/wUFA/4FAwPGBgMAUgAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==">>). logo() -> - jlib:decode_base64(<<"iVBORw0KGgoAAAANSUhEUgAAAVcAAAA3CAMAAACPbPnEA" - "AAAAXNSR0IArs4c6QAAAEtQTFRFcTIA1XcE/YsA/40E/p" - "IH/JYc/5kg/54i/KIu/6U6/apE/61H/61P/bFX/7Vh/bd" - "a/rpq/L5s/8J2/cJ8/8qI/86Y/9aj/9mt/+bJ7EGiPwAA" - "AZRJREFUeNrt28lug0AQhGHajrPv+/s/aVwpDlgE0gQ3t" - "qO/DhxihMg33VJ7JmmCVKSJlVJ4bZQ93Jl/zjJv+8tzcM" - "UVV1xxLXIlRfPAZptYrbf5YeW618PWyvG8w/g9ZwquuJ6" - "Y6+bbdY0rrifhSmrmgUulVXbVDq3H39Zy6Cf9+8c7JNM/" - "mXeY8+SMRmuIK6644oprkSupmQdulLhQdup1qJKmrmWmV" - "pb5NN9LUyddu7nnLYkrrrjiimuVK6mZB+6VuFbiXJk8v/" - "bnv0PVa+Yd5tdr/x7vCfqbgPsfV1xxxRXXKldSMw+8KPG" - "gxJWyU7WZE538p0vOr/lOm/q7dPf+bOVKvVXiUcEVV1xx" - "xbXMldTMA29KPCtxp7T6XpvxE6/9nm/l987mnG9l5u/8j" - "O4Ot9uTEq8KrrjiiiuuZa6kZh74UFpli3sO61btMfyHyW" - "Gv/RMs7wB67ne32/BdwRVXXHHFtcyV1MwDn0qrbHHvyPT" - "/Dsarla/R/1GpQydYPhf0bqC/A7jz7YkrrrjiimuVK6nI" - "F5dWoNvcLcs/AAAAAElFTkSuQmCC">>). + jlib:decode_base64(<<"iVBORw0KGgoAAAANSUhEUgAAA64AAADICAYAAADoQ7yoAAAKQWlDQ1BJQ0MgUHJvZmlsZQAASA2dlndUU9kWh8+9N73QEiIgJfQaegkg0jtIFQRRiUmAUAKGhCZ2RAVGFBEpVmRUwAFHhyJjRRQLg4Ji1wnyEFDGwVFEReXdjGsJ7601896a/cdZ39nnt9fZZ+9917oAUPyCBMJ0WAGANKFYFO7rwVwSE8vE9wIYEAEOWAHA4WZmBEf4RALU/L09mZmoSMaz9u4ugGS72yy/UCZz1v9/kSI3QyQGAApF1TY8fiYX5QKUU7PFGTL/BMr0lSkyhjEyFqEJoqwi48SvbPan5iu7yZiXJuShGlnOGbw0noy7UN6aJeGjjAShXJgl4GejfAdlvVRJmgDl9yjT0/icTAAwFJlfzOcmoWyJMkUUGe6J8gIACJTEObxyDov5OWieAHimZ+SKBIlJYqYR15hp5ejIZvrxs1P5YjErlMNN4Yh4TM/0tAyOMBeAr2+WRQElWW2ZaJHtrRzt7VnW5mj5v9nfHn5T/T3IevtV8Sbsz55BjJ5Z32zsrC+9FgD2JFqbHbO+lVUAtG0GQOXhrE/vIADyBQC03pzzHoZsXpLE4gwnC4vs7GxzAZ9rLivoN/ufgm/Kv4Y595nL7vtWO6YXP4EjSRUzZUXlpqemS0TMzAwOl89k/fcQ/+PAOWnNycMsnJ/AF/GF6FVR6JQJhIlou4U8gViQLmQKhH/V4X8YNicHGX6daxRodV8AfYU5ULhJB8hvPQBDIwMkbj96An3rWxAxCsi+vGitka9zjzJ6/uf6Hwtcim7hTEEiU+b2DI9kciWiLBmj34RswQISkAd0oAo0gS4wAixgDRyAM3AD3iAAhIBIEAOWAy5IAmlABLJBPtgACkEx2AF2g2pwANSBetAEToI2cAZcBFfADXALDIBHQAqGwUswAd6BaQiC8BAVokGqkBakD5lC1hAbWgh5Q0FQOBQDxUOJkBCSQPnQJqgYKoOqoUNQPfQjdBq6CF2D+qAH0CA0Bv0BfYQRmALTYQ3YALaA2bA7HAhHwsvgRHgVnAcXwNvhSrgWPg63whfhG/AALIVfwpMIQMgIA9FGWAgb8URCkFgkAREha5EipAKpRZqQDqQbuY1IkXHkAwaHoWGYGBbGGeOHWYzhYlZh1mJKMNWYY5hWTBfmNmYQM4H5gqVi1bGmWCesP3YJNhGbjS3EVmCPYFuwl7ED2GHsOxwOx8AZ4hxwfrgYXDJuNa4Etw/XjLuA68MN4SbxeLwq3hTvgg/Bc/BifCG+Cn8cfx7fjx/GvyeQCVoEa4IPIZYgJGwkVBAaCOcI/YQRwjRRgahPdCKGEHnEXGIpsY7YQbxJHCZOkxRJhiQXUiQpmbSBVElqIl0mPSa9IZPJOmRHchhZQF5PriSfIF8lD5I/UJQoJhRPShxFQtlOOUq5QHlAeUOlUg2obtRYqpi6nVpPvUR9Sn0vR5Mzl/OX48mtk6uRa5Xrl3slT5TXl3eXXy6fJ18hf0r+pvy4AlHBQMFTgaOwVqFG4bTCPYVJRZqilWKIYppiiWKD4jXFUSW8koGStxJPqUDpsNIlpSEaQtOledK4tE20Otpl2jAdRzek+9OT6cX0H+i99AllJWVb5SjlHOUa5bPKUgbCMGD4M1IZpYyTjLuMj/M05rnP48/bNq9pXv+8KZX5Km4qfJUilWaVAZWPqkxVb9UU1Z2qbapP1DBqJmphatlq+9Uuq43Pp893ns+dXzT/5PyH6rC6iXq4+mr1w+o96pMamhq+GhkaVRqXNMY1GZpumsma5ZrnNMe0aFoLtQRa5VrntV4wlZnuzFRmJbOLOaGtru2nLdE+pN2rPa1jqLNYZ6NOs84TXZIuWzdBt1y3U3dCT0svWC9fr1HvoT5Rn62fpL9Hv1t/ysDQINpgi0GbwaihiqG/YZ5ho+FjI6qRq9Eqo1qjO8Y4Y7ZxivE+41smsImdSZJJjclNU9jU3lRgus+0zwxr5mgmNKs1u8eisNxZWaxG1qA5wzzIfKN5m/krCz2LWIudFt0WXyztLFMt6ywfWSlZBVhttOqw+sPaxJprXWN9x4Zq42Ozzqbd5rWtqS3fdr/tfTuaXbDdFrtOu8/2DvYi+yb7MQc9h3iHvQ732HR2KLuEfdUR6+jhuM7xjOMHJ3snsdNJp9+dWc4pzg3OowsMF/AX1C0YctFx4bgccpEuZC6MX3hwodRV25XjWuv6zE3Xjed2xG3E3dg92f24+ysPSw+RR4vHlKeT5xrPC16Il69XkVevt5L3Yu9q76c+Oj6JPo0+E752vqt9L/hh/QL9dvrd89fw5/rX+08EOASsCegKpARGBFYHPgsyCRIFdQTDwQHBu4IfL9JfJFzUFgJC/EN2hTwJNQxdFfpzGC4sNKwm7Hm4VXh+eHcELWJFREPEu0iPyNLIR4uNFksWd0bJR8VF1UdNRXtFl0VLl1gsWbPkRoxajCCmPRYfGxV7JHZyqffS3UuH4+ziCuPuLjNclrPs2nK15anLz66QX8FZcSoeGx8d3xD/iRPCqeVMrvRfuXflBNeTu4f7kufGK+eN8V34ZfyRBJeEsoTRRJfEXYljSa5JFUnjAk9BteB1sl/ygeSplJCUoykzqdGpzWmEtPi000IlYYqwK10zPSe9L8M0ozBDuspp1e5VE6JA0ZFMKHNZZruYjv5M9UiMJJslg1kLs2qy3mdHZZ/KUcwR5vTkmuRuyx3J88n7fjVmNXd1Z752/ob8wTXuaw6thdauXNu5Tnddwbrh9b7rj20gbUjZ8MtGy41lG99uit7UUaBRsL5gaLPv5sZCuUJR4b0tzlsObMVsFWzt3WazrWrblyJe0fViy+KK4k8l3JLr31l9V/ndzPaE7b2l9qX7d+B2CHfc3em681iZYlle2dCu4F2t5czyovK3u1fsvlZhW3FgD2mPZI+0MqiyvUqvakfVp+qk6oEaj5rmvep7t+2d2sfb17/fbX/TAY0DxQc+HhQcvH/I91BrrUFtxWHc4azDz+ui6rq/Z39ff0TtSPGRz0eFR6XHwo911TvU1zeoN5Q2wo2SxrHjccdv/eD1Q3sTq+lQM6O5+AQ4ITnx4sf4H++eDDzZeYp9qukn/Z/2ttBailqh1tzWibakNml7THvf6YDTnR3OHS0/m/989Iz2mZqzymdLz5HOFZybOZ93fvJCxoXxi4kXhzpXdD66tOTSna6wrt7LgZevXvG5cqnbvfv8VZerZ645XTt9nX297Yb9jdYeu56WX+x+aem172296XCz/ZbjrY6+BX3n+l37L972un3ljv+dGwOLBvruLr57/17cPel93v3RB6kPXj/Mejj9aP1j7OOiJwpPKp6qP6391fjXZqm99Oyg12DPs4hnj4a4Qy//lfmvT8MFz6nPK0a0RupHrUfPjPmM3Xqx9MXwy4yX0+OFvyn+tveV0auffnf7vWdiycTwa9HrmT9K3qi+OfrW9m3nZOjk03dp76anit6rvj/2gf2h+2P0x5Hp7E/4T5WfjT93fAn88ngmbWbm3/eE8/syOll+AABAAElEQVR4Ae19CbgdVZVu1c0EGclMBsgNDUHRACKgQpjs0D6FqK0tCCqE9mt8HezXdjO07XvdBAdsIXwP+zUI2D4GWxB8ditBWwFBQoBWEAjYCjTk3oQMZCBzbpKb5Nb717nnhDucc24Ne+3au+rf37fuuadq1xr+tavOXnuvvSsMWIiA5whEUTQMJgytmjEYn/uCINwRhkHkuWlU3zME0BZDFLY7z/yWRl36Og1qvIYIEAEiQATKjID8dor97CuVuRWU0HY0/OGgRXvxp0m5G+cmlhAemqyMANrVNNDdTdpfG87PB8lACovHCMCH9LXH/qPqRIAIEAEikC8C+B0dBVoI2gyqV9pwUM6PyldTSicCCghUGzc+4pbOWxXUIMsSIoAWN2x7FD0ft+VV680vIVTem0xfe+9CGkAEiAARIAI5I7ALg/zJ+kzss+fsMoo3hcAazLKuS9b6e9SWK9cMN6UL+ZQPATSg2T0aVMJ/Ny0uH2L+Wkxf++s7ak4EiAARIAIOIICMs/R99k34GWaf3QEvUoW0CKAFj8WoTcayDdfzRkjrgzJfh5TguRkbHy5/g8GrB42IvvbASVSRCBABIkAE3EUgilrSB6213lal1z/WXSOpGRFohAAWcme/AWo3wvLNjcTwOBGoh0BH9xrHWgPK+LlxQT0ZPOYGAvS1G36gFkSACBABIuAvAq8lX1LVoG8lM69Bi79IUPNSIrAcmzA1aNEpD2+YV0ogaXQqBFanbGX1L5PtnJZy84FUntC/iL7Wx5gSiAARIAJEoLgIbDSSodazB7WC2WrFbS4FtAzrWqWrb7as5utKCthUNEzCA3iB2bYn3PgQ1vBVVp70dVYEeT0RIAJEgAiUHQFzGZK9el+tZceV9nuCQHsUXdmr6Zr7MssTCKhmjggoPYClFXPWNUe/1hNNX9dDhceIABEgAkSACMRDwPxsa63T3/ZYTQPmDdeQ4KerCCitCVx5oasGUy83EFiLdwBPUlPlzc+osSbjxAjQ14kh4wVEgAgQASJABHohsDUIFvU6YOzLmNOx1jUUdoON8SQjImAaAexKNi4IWk2z7ea392J8LtThTa5FQGBXEJylZ8euS8H7Zj3+5JwEAfo6CVqsWyYEMN8hu3oeXaUZ+BwNkoyRkdVP+b8n4WuwvQ/t6PF9G/5fAXoF9HIYhpvwyUIEiIDvCGAjVTwsjtMxQx5D0SFBEG5m4KqDMLmaQSDaZ4ZPHS7DMDDEQgQaI4B0lAmNz2Y+Ix1AFkcQoK8dcQTVyA0BBKhHQPhskASpspSmFqxOxP9Jy5S4F0DuRtR9uQ+9iIB2eVwerEcEiED+CCzFYJaMYCmWMeDNwFURYLJ2GoE9cgOwEIGcEBhziGzxHoZBV04KUKw1BOhra1BTUGwEEDAehsqSVfL+6ufhsS82W1EGCIVO7ckW+r2O74+AHpVPBLLynYUIEAFHEZgWBONtzIbakOEoxFSLCBABIkAEiAARIALFRwCBoCzZrwWp8nmk41ZLYC1LeoQC6P8qPipBrHwikF0nx1mIABFwA4HJQbDXhiYMXG2gTBmpEdBroEwVTu0UXkgEiAARIALOI4BgbwSU/BhINoP7Q1ALyNcigbbQn4G6YJvMxn4X9K8IYmUNLQsRIAIlQMDnh1gJ3FNyE8MwwkLUZToo7P+eDl9yJQJEgAgQASKQDwII6FpAZ4PuggYyKymfZ4OK1N8TW+aC7gS9AVu/W7W5SDbCNBYiQAT6IsCbvC8i/O4UAgcHwY06CkU/0OFLrkSACBABIkAE7CKAwO0Y0HWQKmtBHwTJLKvMuBa9iI2fBonNrwOD60HvKLrRtI8IlBUBBq5l9bwndmPHhrtkX32zZdWWMJzZbpYnuREBIkAEiAARsIsAgrSTQT+G1N+CrgRNtauBU9LE9itALwKT+0HvcUo7KkMEiEBmBBi4ZoaQDFQRCMOuziC4xKyMSNKmWIgAESACRIAIeIkAgrIzQQ9B+V+BPgwKvTRER2nBYh7oP4DRw6CzdMSQKxEgArYRYOBqG3HKS4zAhDC8oy0ItiS+sO4Fa5eE4eHP1D3Fg0SACBABIkAEHEYAQdiHQE9ARdlhV9Z5sjRHQDalegSYPQk6p3lVniUCRMB1BBi4uu4h6ldB4IggmLg+MxYrwGHqmZnZkAERIAJEgAgQAYsIIOiSDZeehcifgE6xKLooot4HQx4Ahs+B/qgoRtEOIlA2BBi4ls3jvtobhvsmY897rHddks6E9diduLUlDIMo3fW8iggQASJABIiAXQQQZE0D3QupD4LeZVd6IaUdD6t+DkzvA00vpIU0iggUGAEGrgV2bhFNGx2GZ8Cus/clM+6SMJx8PIPWZKCxNhEgAkSACOSDAIKqwaDLIf0l0Hn5aFFoqZ+Adb8HxlcI1oW2lMYRgQIhwMC1QM4siymYeH14CP7A3rMxfdroPa/tOH8ZSKregU8WIkAEiAARIALOI4BA6jQo+RxoEWik8wr7q6Bgez3oeWB+ur9mUHMiUB4EOMpUHl8XzlIJYGGUpP0E+NHpOQgT4RxTggvncRpEBIgAESguAvgdwxvgghtAFxXXSictk/e+Pgb8/wWff4X+w0YntaRSRIAIBD07+4SDCHiLAH5ounoQg1ZvPUnFiQARIALlQwBBk8yyPg9i0Jqf+z8N0cvgC86+5ucDSiYCTRFg4NoUHp4kAkSACBABIkAEiIAOAgiSQtCXwP1R0DQdKeSaAIGpqCuvz/lfIPaREwDHqkTABgK8KW2gTBlEgAgQASJABIgAEeiBAAKjSfj6M9DXQIN6nOK/+SIgvvgKSHYfnpyvKpROBIhATwQYuPZEg/8TASJABIgAESACREAZAQREZ0KEpAbznaLKWGdgP1d8BF+dlYEHLyUCRMAgAgxcDYJJVkSACBABIkAEiAARaIYAAqG/w/lfgKY0q8dzTiBwKLR4GD77e5C8zYCFCBCBHBFg4Joj+BRNBIgAESACRIAIlAMBBD7ybtbbYe2XQex/+eN28dU1oDvEh/6oTU2JQPEQ4IOzeD6lRUSACBABIkAEiIBDCCDgGQ51fgSa75BaVCUZArLj84+rvkx2JWsTASJgBAEGrkZgJBMiQASIABEgAkSACPRHAIHOOByV946f0/8sj3iGwIeg7y/g0/Ge6U11iUAhEGDgWgg30ggiQASIABEgAkTANQQQ4EyHTo+D3ueabtQnNQLvxZWPw7eHpebAC4kAEUiFAAPXVLDxIiJABIgAESACRIAINEYAgc0xOPskSD5ZioXA22HOk/DxO4plFq0hAm4jwMDVbf9QOyJABIgAESACRMAzBKoBjcy0clbOM98lUFdm05fA1+9McA2rEgEikAGBQuyOhoeGbFF+MEhe5i0PkqmgCdXv+AhmgdaAdoB2gjaAVoFWVv/fGoZhF/5nIQKlQAD3jAxaCQ1qYvBe3hdN0PH0VAPf74c5XfS3p05toDZ93QAY5cPA/XCI+DlI1rayFBsB8fHP4fNT8PxcUWxTaZ0rCKC9Sdwj1LcfJ7/l+9EWI1d0Na2Hl4ErHDYWQMwBXQD6IOgQUKYSRXtx/eAl+HMv6KeglezEAQVHC9qA7NA4H3TpviA4bnCwd0sQDAHta636cRHu6Z+EYVDKAYnqQ03uiz8AnQyS++WYbqzwX8wCPlITuAYvgJ4GPQZ6FrSW9wdQcLDAZ8Oglrwfcjbo3VWSGYFWUNPS7W+0kmBwO/48BVoK+jXoNdCWIv8Ywj7vCn3tnsvgExk0fxA0zT3t1DTqBOdtoO2gXaARoNGgUSAv+5nQO0mRyZIH4ftT8YzcmORC1m2OADCVvt500NEg+T2TiShJvR8Dkj6OkPRR5LfqG8BfPgtRYLsEpbIJ2HEg6cOJ/Ql+yyu4rMA1vwM9V6Xf43MdcJIfei+LROvOFzhPHnxngv4SdC7IVlkGQdeC/h1OlgcyS94IoC1gOOku3M0yaBGnnA/f3Renos91qveIrLn5OOhiUCtIuVQGC74DIbeBXgXOhRokaI+iBTOC4CYdEOVxMnpQ1oGVqt+PALOPgD4Fkh84zSLPxO+BfgBaAZ8XYlSXvq7bZArp67qWGjqI+1ECtkdAMlhYpCLZai+BpNNbI/m+AbQdzwEJXOsWYCLZcBLEHgaSgKMnzcR36ZwXpTwDQ84CHoIXS0IE0FZk0FUC0wz9mMrA69nwwcMJxedevfp7Lv24izGV9tkhBiblGhtVwekBnP8h6CHQGmCW6fe8I4qm4WZf1Vhm5jMzoWN7Zi5qDDANDifO6Yqi5/HpQOncDCXOA8mNxZIDAsB+9t5ULWET2lAlpSIHrfVEAorhIGmTjtwj0WPQZQ6oEB0RCWZgi1LZBr7pOmy4cBpoYWcUyTMp7yI+nwuSwUVvC30dqxkVwtdajRQIDgFJymgRyh4Y8Qjoi6B3gVQmOcB3NOiPQbeAcBsWosjM61CtdlY0vsBK6fdsw2M+YAX7R4Hmg9pAeRfpS4ouktWauEjgqmxAa2KlbFwAo8WJi5SNz8h+n/yAuwmgDSflIGN3FM3L5rR1uDxdoJCDuY1Fdg/ozEWvoi0bHupXL4aE1saGuH/GpWAGWMoPwq3qXssmQJ6Lkp7sXaGvEzveW19rNE6gF4LuToyiWxfsgDrfAZ0Lkplj6wVyjwZ9AfQsyOdyD5RXCfatO0VBILCp/J7tVffwakhwr98HpQaDztvpxuBzAy/skuNXgiTdP1axFbg6MzMCcGbJ7CrQkXUSl8dCKbdKg06H6LYoqszCzstNjZIIRttoxTT3/dnMnYTL18g6PS8LMJDZ1UXI4+iCAQ9hOLfVcUMkpR/3iMwsyqBD8Wa81fHv7gzPk5lVyJL0m0vVZWYTIM/FF8ThKDL4GPsHL5vYAlxNX/vuxGthQNzlK67ZKv2uBaCpSMP7LOgBkGxiab1A7sugG0EnQPhJoG+DfEy7/ST0/gcQSxUB/B4MAy2QcAiHKr9n+mk6svS4HavL3CgwXQL2xdAGmcDBvcNVU4Gz2nyQMLgOhE5cl8zEnpiVo6nrcw9cAcasrd0ds5cxPKW9PssUblU+Q2RR+P3Y2AlmROcZZk52ggA6dOsRAJkBY0prFG2UH2hvCtrV8P0YyYfC0pFwfECnHqyV2AWDDnsxLiWptwxg66HU6xhSrYHVldVBivt117n0kmzyi7RV/OBVslMmmmRcKF70tffuxL16Doz4G88Mkc78XaD3IEh8F+hbIJk0cKZAn2dAMlgnG819DvSKM8rFU0Rmq2zuyRJPK8u1gMG0fVEkabu7QTdVwiGrOsxAN/J16UPlVoDBiTK7CgUkYPewTYQSmz1dm4jIDciq4NwCVzhyLNIdZaTvZazalwDQ41IZN7o3iipT63M8NsQ51ZcHwfUyV2qujLgJwZP765S7U0kWwe6duEl9Hcnv4bbKPXJTNYCd3+ME/60h0D3rJgGrdCplpLMApZKdsr4awKZaN1MAEPqbQF/3x8TDI+jHTIfad4Iw7u5Fkdmu74OOQVB4MejXrmsNHXeAZAPAY0DzQegWeFGkTdyJNnK4F9oaVhJ2z5IlTWC7alAQSDZOjmX6BVG0YZZtBYDB7GrA+vRw7+McQa97IqIa68y1jWdNnv3AtfsHWzrkm4Z6N8Nag63RZ2Us6fEo2tKGBstOWiOY4h5H8IahVsOzjOKj5f8YV4U86qHtnIfehaSSGLY9D2v6yqwEsLdzkKc3LvD5XDi8C0cLErD2tg+vC5aOyybYiXW67q056qut5nf6WhNde7zhR3mYSRA43p7UTJJ+hKuPQxB4Aci32csAOsu7KWWQ4GiQzMSuBLlexkHB71fbiuu6GtEPtk6sTUqhj99qhKkRJjtfNsImBhNgUJuYe6EYAWtfoyuxzkNRtFU2iGzte1b7u9XAFQbO3tXdOStgh7ynq8aII6WTdmXPo/w/GQJrguC9ldsj2WUxao+41MWUVbSXidW0+XtjGOF5lYpnMcizXdZO4NletrIPHbFA0qfHvtmdQvRQJaQvPgzocEbIfo9KtDcAfV3QZv012HWqB7b9VvRE0PfHoBc90LepirBhH+jbqHQU6O9AkoLqcnkflPu6ywoa0Q2TUtiP4VbwWo+A1cFlf5Iy3Km+ThO/bQuBQQEn5uq1ktGSLdsWRfuxMV0QPhIEHfVqmT5mLXCtNugXdAIR07AY43ddFL0pIxLcpCQFpLgDFqS4LMYlknzcIav2nSnVh916/9Pmk0I6Un7gkE1TpkBGMBqLH9HK2uVNGJL3fKlEUp9X6mNvgMqghftp+6nM63kRfV00X+Pe/RA87PrAtAR0XwKdgEDvyZ4tsgj/w6ZO0Fdhy2zQLxy36XK0GQ/XNsZDFbadKBlD2I8BA5Mul7VXa2kHDFqxqYOk4qvJ0NI9O9+WC7ClRRcW+0tWh3pRD1zhx7Ey9eh+g9bCepx0SqU9l2iGwQyW6NEeY4ZTPS4dZ9c7avsY2sWoyot6Svmw64U2ApkNj7k4E95LS7Nf8LAvc6kMWuBNV9HcEqBAX2NmrAi+hg2yrvUukMvrWh+Gfu9EYPd1EGKK4hbY9ypIniEXgTY4amltvethjuqXTi3MsmLUeTEuftqPjKGDVAYPsD3rQmDQVu4Zqor1siRIvagGrnjAS7C2iYs9K35Ex3zr3eoeLZCATlVbduS+0ynuD9nIa5vZzadUQVNmPgEPvU2SPlvu578yyg6yl7UyfDY66BgFlYrg61uAi6vrWuVn8/MI5M4GvabgP2dZwt7vQrl3gB50VElZ7yqptIUo+J1ulaV/WOejEgzqgDRJMp2M9f3Aa9gbWOqDoL2Es6w6HorDVS1w3d6d635/HCXKU2c0djarpA6XID2uPF5NY2l1hO7xNNcW+5rKMJdkKMwutp20rjcC8myU3IM16AexFBsBf32NBvpR+EZef+NiaYNSpyCAu8lF5WzoBNtlxvWDoL8HddmQmVDGB9GGPpbwGueqw4b5UKrNv6V/kj0fymtpMhdgMGsfskgml3OpT2b8sjAwH7gidWANtsAe6XyuexbYslwrqcO7JWVqWhYuvNZfBJA6v5gjdAP674Uo2nPegLVYoUAISO7BRFnvPKtARtGUugj452u0SxlU+WZdc/I/KGvLZC3rb/JXJV8NgEEX6CvQQpYDrctXm7rSb0RbGlH3jAcHq6nBt3ugah0V18mmiIg3sxX4bz44vOxHenQ2W1282mjgioB1+CaMck1xagtsF2GvjFOtQuNX3+HMRevLrJMM6mBO0aPUmjy9NRTvRt7h+gYoeQJUQNmVrsDLHLQooGv7meSdr2UW7/B+ZuR7IIL4LyJQkx2Dt+SrilvSgccj0OhdoGfc0iyQda7+pZbi9U+yA75fqcF9Pb/3gb5Hkn6vZpN6GrgntdbN+sYCVwRhY9Eh3wliiY/A08BN1gGzFB0BZCLIWggO6iR19AjszL12UdKrWN93BGTQYvtC362g/nEQcN/X+J1+Oyz56zjWWKwjmy5dhADtGxZleiUK2KyFwmeBHnJM8S+gTcl6XC+K9O+xnnUvFukiY9DnMuOa1NqjD4fFLM8zmzQ1gsYuNBK4olG3QqNN/uW7G8MxCyNs2rRzQRYGvNZxBCRoxf3BtRBp/XQoXiWwhcFrWvi8vW7k1VG0WmaUWAqPgPO+vhkuwMsRnCk7ock8BGb/4oxGjioCjHZAtXNB9zqkorQlL9YiF6d/37YlDIc+k6YNLMWGkfgh6sICBwffT5vGIr+vyRy4Vht1m98w5K398JuiaCOD17zdoCT/dQatBpAdg+CVM3AGgPSMxVTsArmGvy+eeS2dum76Gn2cT8GeM9PZpHKVbEB0FgKyn6twLyBTYCW7LV8I+ieHzDsDbeszDunTTxXoJ/sNFOT5G0raeOIiSyDfjbc/JL6QF6ghkClwRaOeCM0K0qjVMI7JeDyC1zfPi1mZ1TxBQFJLpnufXuMK2DIrwwEeV7xhT48prQheH7Mnj5LyQ8AtX6OPIzNjX88Pj36S38SRMxGIPd3vDA80RQCYyaZNf4FKNzataPfktWhjQ+2KjCcNerWi5svxartea/01YTizPbGWUdQyCEsgmU2aGDnVC1IHrjIKgY2l16tqVzrm47Cuq5MbNhXE7yuwezBTS0w7UwZ4Ns41zZX8XEdgyulc6+y6j0zp55SvL4JVspmOC0VSXj+E4Ot3LijjsQ6yVtmVFGuMawcXu4Ylgtax2Hq3IJNSa5eE4eSFaTBeEwSvoQ/H4hgC6QJXrNnjKISWJ4fIhk0yk83iMQKYaV14OHcPVvLg+Id4jyhB6zRbWeu8m5vZOe0jU8rl72s8Y9DNCb5oyqKMfCTVVXYO/nVGPqW/HBhGAOES0E8dAeOL1bbmhjrYPRgvOt1U2fPbDY0yaLHnhjCcekYaBq9H0d3cTDMNcvrXpApcsWbvOY5CaDpn8/ooClL5RlMr8o6HwN4omoP74+p4tVkrHQLreY+kA87zq4ZhM7sOvgPbcy/GUz93X58PPY+Mp6tqrf3gfiECrodVpZSIObDEhGLwCdBTDph9BHS4wAE9Kiqgf7/B/7eDiHuD08LwoCvS4IrX/pw33SGfpLGhyNckDo5kJgkO5c5aqq1CHhvtr6mKIHMVBGT3OTB+XIU5mfZAQIbO2h7tcYD/lgaB3XgHNgf2yuHufHyNGbAQ+H7JEYz/BoHWDx3RpTBqANMOGCMZHCsdMOpL1TaXqyoyy4j+vcevvMECxiC4DBuAt8C/S9OA2RFF0/DaH5d2oE5jRqGvSRS47oii2Q7PJMnLt28DfRh0NAhtL5A11UKywYKQ/D8ahHszOA10FWgZyMEyAxuSrFvooGJUqQkCM/Aj6G6Kzd5G90jf+2MmTJT76AZQO8jR0op1jxuk48HSFIF9cvYBkKTHHQuSqH8EqK/fez4Xl+C8o0UG9jhoUd859HV9XBIf/SiueEfiq8xf8CN0wOU5zKKAALB9E2xl5lVSsfMs8p7gj+WpwIYomocfAGdmfvtjceDZdg3OSf/kJJD8nkl/X/osI8LwYLg0vDkMgwjfkxcMWG0PglXJL7R1xQEMEJxX7K/9ltfiHPmU33a4snJeYhyHf8uhnWpB3vs2DAk5VhZDnxNB2WIFNFbwmAZauAt/3CobZDvyUpbXsCOvni/arzQNansUXamnb2rOco/MAaW+R3BtC2g26G6QYwWJ2dGa4aZ9KfzgzwWOGZtAnT1tqHweaFQWbHD9RNCCzijajE/Hirld2OnrIIBzS+HrOPcDsHjGgcaOn8BoTBx9bdaBTsNBZ4BkbeZ3QT8BPQV6GYT4J9oNQnJe9DsQkpCiH4O+DfocSH5HEk2Y2LANOv0FKO/yrA1b68mQTDH5JXWrVDRaBJ2kzaTuv9Szt9Ex2VDTLQwq2kg/eB7IxG/5le7FOIkQb23ku7rHdYOIJIrvlA7UXJDaww+8W0GONOBNUKWcaXG6bc5s4PoAduGDoxwpnXKPyIPO/D3SPcgzZ6dTgczrz9d9aGU86GkwsxB+z/QD1wg28JU2Lh0JR4p0bJYasZW+7u11AFtYX/e2tP832H62Aw1cgr8T+mtn/wj0kIH9s0D/ByQBfdYYZyt4PAj6W5DMDDlRoMt9oLzLB/IAAynCLg1M3gonTLSNAxq1DPA7UirhpQycD9PAAXyngRyciBgQ/lbBI1bHVlIIjsh9XWtHO/Q9OgxHjEUqwMOgLjFAo4B3O2geeMuUu6Qf51gkLe6/fpyjAhQdA4F3BkFuo6VvqbdNUoFxjwyVe2Sxyj2CHRnBdylycqRhzuxwIpV4+nHlfkVOJX0IG8mEsq5nIQjZTuYL+G4GXRF2/26cX5FqXkwCjjIIP/2FBBcUoCp9bcGJf2pBxkAiLse9lutvCrqQk0F/A0VfAT0C+jzo3aCss1+jweNs0LUgjBlFGPeNPgLKyhfsMpXP4urlmThkv9h624MDrsTowSHZVc/CobI2VX7DBqHdfw60IQu3xNdikH+XE3uTdEgf7qS3Up7DPYltiXEB8F0NuhBVJbX4hhiXOFVl4MAVDxOE/PfnpzU25u4OWGcCaHmAWiuQ1wH6HASOQHdB1ojlVI48N4p2zM5JOMUOgADeaTwHa1tbB6imeLrSmcUOemMkYLV2j0BWOwJYWVtyrEqklAixlodKmplwTXUjivtSr+tJhDMqdw9e3DcEDQDfZA1NjkX2AijNe33pa2VfI3iSoOojOTZoEf0foJvz0gEYzAL9APKxwWzwD6AjFXUZBN7ngH4EWgm5V4CGKspryBqPM/kZ+/OGFeyc+DDst5YeLinC6LtcZ8e0hlIuqQZq8humNiHVUDpOvIr3+hpJ3WkmpOm5SuCOPlxlYu6ZplUNnkSb3wOSnZcPQuNfYpC1KqsBA1c49K4cHXp+GI4DrvY64/XQhvwOdNLm4dzMShhdr5L6sa0lm1lQB9SYAOwCkOMuwrvvqQYuS40ZlJAR7o8XR+MPLssxiJEJ4OV5/wAnRC5L9U0yMisPR8ywptyIIov46rWQfz3+HbE115n3/TJoIe2voIW+fsux6r6WjXoOfkue9f9kFFJmnNJtLpNBXQRMo0DyDP0t6E9AsnmbzYLXZgbyPPkt9DjXpuCaLOD+IP7/Xu17Dp8yA3aeLbkAPMe+y3YJlGT88w5b9taTsxZpyX+Q66ZUe2/DIyf1Lsj1bEp6DD7Ygz7cGbjupMo0SFIGlus3DVzzc+iGduBwEMC8zzIeTcVBn3bpKaJSDh30qUEUtS9oqiBPWkdgYxTNn2pd6gGBGKE7GO/3yy9wOaAJ/sGtIZ2O6fnNvh52udZGTT3tzP9/ean6eJldz28crQcI0KPjkO6Z98t6HLb47yTI2pj3TImSvfR1b2DVfX1Rb3nWv92I+8nqIDWCRFnDKnZLto5sWmg7YO0L8lE4IHuM/Az0tr4nLXz/K8jYZEFOIxFW2uDvsenREfktATw7DEefgbaee5wEBR5q5Ajd4xXTkRY8FANVzvThnsHNPwgdi2W6tmfj3jRw7cjFoV1XheGkmWjQKrnd2eDqvhq6SQd9pv07btxNmFnIex2ICQiLwQPrInAD3W7fmEq8IpOcuc2yNrIZOq1Grl1LPmkncmt0/HMj3QpyPPVL1bXth+9vhoyj7T8XxbKh8mxs+numbb8Cf/q6Lqg6vkaQ1Apx8pq8vMpKCF5oUzhsHgd5PwXdCTrUpuwYsj6AOs9Bx0tj1DVWBc+xDWAm6ZN5lVNh8xHawkfmkhp6oO/ysLZ9cfjLPT89l+BdsmgGy+SctbTgOHhU6mD/IEzQHb8jCK6JfY3lig1/6NvgUNw5x1nW59gwHCRBofMFDa4dIxND1gWBpOxZKpK0vVbWnLA4gMDGIPjvkqBqt0g2wjhJK8lvYnMgg5HmJmkn+Tz4ZlxQzFnXyhqYSfC7c4MVPZsD9HsFvb4R63setPK/PBvXXW5FlLoQ+ro5xGq+/gzk5ply/kXcPzub227uLDrtJ4Lbs6D/Zo6rcU6SOiu7zMprd0YY596AIfwgA9K/bnBa+7C0QWmLakX25UDAdoiagLqM3eu7vBYEj9ZVVfXgxiXVjClnJ+fE/FFYhoSPs+V/10rDwBVzFz+yp2zlh1qycF+0J9OAJKQ5HIrdVRHALDHALSaLCUiHDLBfFkuuCGC2FTtI3GRXh7V44Ek2ghtpJQPZLg++ziDAToE2i8y6hl+2KVFfloxSb8Y+WJZ3Wkxp2FSkDk/GrLvdQT1R9uDr/J91pa/jNTsVX6sGCwPY9RLO3ztAHWOnEQTKppMyCDbDGFNdRp8G+19D77friunF/e96fbP7RbUt4nfZ8trWzQ+41nfpwCthsLa11a5bN90WhhPPsCszvTT0OWRm/Nj0HHSurBu4ikPtTZ8f+KGWf7wsEzG7hAURt9lRXjrmy//RjixKaYQABisuknF/e2X9PWE41ZsHXg2XYd3r1E+qfbfzOUIGd+RGKUCRuctx2MBiKlZueFQw6y6DenioW1wrI3fkRivrw3Q8QV/Hx9WsrxEQyezjUfHlG6/5VXQSu4xz7cMQdsp6VhlwvQXk2wD4MdBZgtcz8ale4I8HIcRygHfArD+AnScf+Gbwn+pbEAxyHIiV9F3GzRuolu3zGFi9267MHddgplUGjLwquA9kQnGmS0rXDVztOdTTH+o6HhyP1+ZstfY+pCmXcta1jhMsHtpvdW3rSoxWTr7QonlGReHB9wwYWhy1q3RqZYTe8yLPx8l4r12wz1dDZK3MKqvLKfbc7idW9HVyvxn19dzk8o1d8Qo4fd8YtwaMEAhJf++fQQsaVPHh8Ego+e+w5UOWlM1z1lWlTaLvstgSdhCzzsm+C14cPLY1CE63h8NqzLSOWmhPnllJ6MO14z23mM90o/QLXO05VCZYJ2Mmwd9OWV8XYmfNK+wEr7LsY/nX+srndzsIdGJ0fpIdUZCytj0MZzg3WpnUfBm1w4KODye9Ln19o53a9GqkvlKWT+xHenA+77VLrXadCw8Lgon20mlk9/U1c+qo4fAh+jqdc4z6+v3pdDBylcy2Ip7QKwj0BoH7naA/1ZNijbN0gH4Em/5EWyL88hhkPKwtpwF/421yA97Ri+jD0trWVVvC8FAn+y7vDoL/2QBzhcOrloXhdO9mWvsCMRwbb+7F63L6Hs/je7/A1Y5Dt8PW3dIp25eH0ZoyJXjF2Pk9mjK6eVfWuvbzn75cSni9e9TaAhByn0zNM33NqI3YQm8xZt8s3BuitnRqd8w2aoBVZgcd7V16cCN8sBcANjGzmGq0x7NBPfq6UdMZ+Hh2XyMAwnYFwakDy1KpsRpcVVMWq0GrPHc/rWJBPkyHQOz3YZsNm67Nx8TgFNg3zKRsrDe5wSS/xrxkMG7QtMbnczyDdHl0nC+3o4H04Q47wY4sfSlDkT2H+OYqfUnNJfQOfOBQ3CUWHDp4emE6ZXXwnRyGF6KDrry2axQkbzinjnge0kQAPyRTre22fRDe8VWswZ3DcG+s0fRPL94b/qnXV2++rL8GI/2SPliYAnva8YN3jR2Dxp/uzxpn+jpbmzDi6/dCh+HZ9Eh99V24N1RnW6HZdaBPpNbQ3QtlFvn/Irg7S1NF+OdR8M/jeXww5L7PmG1IFccA4rnG+DVlNBjvaXVzXwbsen+UvYy5LdKHU1+73tQVhk8ivrl+bRC0G2abiF2vwHVHELwTDVu5bPxwGA6XUcZCF6THvQsdNeWyk5s0KSPclz0een8keUr6Rda1DpW1oYUrndZm34x0ai3jvyLAeuaFloVaEYcfvIV2Bi1kUK/zeCtGZRJCX2eCr3KxEV+/P7seqTmorslGUCeblf11au3cv1BmXn8IO2cpq/ptZf6N2Btrm3j2niJ3i36R1NghD+vLSScBc6CXprsy6VXShzu8kH245dizJM902V6BK9Yh/W1S1ySrL46cuDjZNZ7Wxq6aT2M7UF3tp7VG0VI7zyJdQ7zhvi0IvqKvrKTZzPhjfTn5SJiJ2Tc7KcNGOrWWQZphMaXWsmkQN8baJl3r/8y+dUkl0tdJEatfP7OvjQUH9fVreHQpZvP+q+HZjCcQzJ0MFrdmZOPD5TLfgu1ZIs15lzsgA2Ou1ouxtrk3CP5GX3sJZ1acpi8nvQSMdHw2/dVxr6z04dTXYMfVxnS9OWG4fWsQXGaab1x+bwWuSBMeGgQXxL0weT3ZnmOGxc1Zkmto+opzw3DzRtUNaQZD5bd/0LTe5NcAAaTaTLSSJrzzkqKlCPdFFBkJF8mjXb+s9Gi2QQb2wnZ9TPKTMBKbdOkvoxD79lkaVU+LJX2dFrn+16X3NYIdSRF+T3+eVo6ozbbCLsmG/DeQnQQhK3A1FXIUzsrMq6QPGy94LqMrF/yrccYDMzwZNo0YuNoANdC/xzDuuQPUMnD69RvCcA4mNR0tuN8PtbI51bqr0Ifb4ygKRtSaEIY3rzHCKTmTA4FrB3Yz0c37HnosHBklV9HvK/CO18XIB1+iZ0XHl/R4k3NPBJBK/w796e01SBWdcEdPuYX8Hxv24LVbV+nbdrDiYJxJ7WWkesb5Jjm6ygvDbWfr6zZNdhfOa93iAObR1wMAlPB0Jl+fAmEYs7deZPbuPkWp3wTvqYr8XWQta12vVFRMbaChic6YIMy+cVgbflw0p6O79Zd49QgLv+lN0BrgFAKtE2S6R7fIJF3rIl0ZbnDH4vxcdhk+ELhiPeaFelDI6PLIF/X4u80Zvx5/qDe7NOY4fzYicdtPA2mHx/LFA9XJfn6YhU59di1NcGgNgkXyiNctsruwD+n0m67BwB7GD4tfpoThhnbVwTzBsNI9OcFNNOlrs37J5OvZZnWJze1JzOJhLNR8wQydZGF90jxnLzheA/vfqaTpY+C7U4l3M7aZ22hoZXOuzsvwG+b0RkTA4aPNgDZzbuslZZmkOxy7DOexUdOBwBUOW2DGaX25yCjMjI/0PVqq75hdQgddKe9f5gB3vL1UeOZkLOZJlNdGyGYtEx7OyTz7YrEOHGuGLayTeFteqYAxMZVhrclfjVm5ENWQv2hh/c9uC52UpO6gr5MiFq9+al8fHY+/8Voqz/lqWum3jGvrD0OZPb8TOBifWMNAg6R+/jIHKEy0UaX+fQ0NGYKecEvtm6ufu9T7cJgCDGbe6ar9GnohJeADGnyb8ey+uXGTj8PcdrOK6c91tQdB9H7MCh6cnkchrsQ9o1W2y0zgFVrcyRcIWFkbMaJUa8ClXbUGwS0IXm/STcHe/QmIUukoig3ZyzaZbcW4SHmKzLq+FkXLjlBdM97ycSDq2HORvtZp5al9bSIoSGOS1vPoy1BmRhqFCnSNZFp8CSRYmC4/A8NzTDMdgF+2Noq9OfT69zXNt8tvmNOzrejDtUxQX986SGadoxoqZfjEcshX2qOoHQ+dVlv2VgLXtiCYrtdxHCPGPGTLoHLK2SczgY510IrliZVBcIzxIdxeEElmwsSf9DpUhi9h2LUxim7D8+dSPXM7zwPvz+nxz8JZ4tX912Xh4Ou1GMn8PHR/XE//0a0YMG1xp0NFXzvo62xBQTqDtuCyZ9Jd2vgqzDLKMkbJ7FrWuFZpzmCyJPoqZklNB1MSuNoumdro2iAYr9e/r0FxuKypdrpgHc4U3dkzeb533uE0CErKIc3hM2Ct+FveW/FKXzwMgg/1PsxvfiEw5hBZ51q2WRubPsKv31m68jZjHbjjI5ZKAOD583WwVgxcJ8j94VAA0xPIN/DOu8NKsba1p9XyP9b+P4nZ9kCvUyXdlAiZTJUUPxGZc6GvXfI1ApvRaBBTcmgUv0RAhX1NzBbwlHzNk81yJbeeCADjV9FuMNcTzOx5XPn/yZA5BrK3ppGDB+Bxaa6Lf82qLfgNk7bndNmIe/0wVQ1Xt4dha1l/y5/YBGxl5MxGaREh6Dh+0IYwytBCQLoDHZO1uJMv5sSC4FO6OAz5hi5/d7mjB7BCVobolcr9kUcHNYZJB/1djErFrILZkA2qs0OVN4E45Hf6Wq8hp/L1LD19mnLmjGhTeJw/+WwOGqaedd0RBMrtvMv52VbxFyYftAd1rs+hXbghEvuVIGfwAVvKVAJX/NEdiLBlTanlDHl7qc1XNn5QEIzREyEbtkx9Wo+/45zx0AMCyg+9gzHw7FqR1KK9v3BNK5v64LfnRl15O/Um+RIpTl876Ou3JXKhucovmWNFTjkg8LscZKZuqxh0V8xmEiSG350DHolFIv9mUuKLEl0w4weJqhesMjpY1iZfWrBgORyhnkpQMA85ac7qdzmpVhGUwj2iu7nBWqTaFPtl1QM1g+FBcNtAdbKdXyEbNDlWZBfpqaVMLao5AvNkyuu6N/63mqx8P+lrB32dehYrY1v6fcbreXm+COQRuKZuq0NVsarszfGqqghDzKHpxYZY1WEjmdIhspHLW7D052kZnrVRZMY11N10xoYZlIG1XOcSBR0EMBV4iO60zSAvUm100O3mOjEIHpR553KVFuVg3X00ZXfhFbpq7tRlH5c7fe2grw+P6z2D9SLwesUgP7Kyj0AegWu6toqddJEqdpweRJuwrtOPvTmwJHKrHg477wEOcm+Xt+B1UW8EgWw8p15asMr8cN1OuboNFFBBYH+6BxvRGxCBd6imCYv4KcppsgOa6EKFTsX3RcG+fQtcMLK3DqNu6P29nN+QyrZMz/JBc/R4J+FMXwtajvk6j67PCmyyo/uoS9IsWTcNAi93N+U0l6a+Jm1bjXRnwcI7U1tk80JkzY1UDeD3L7VpjquyMLt/rQ3dWrCjz14bgiiDCPiKgP49srrUKSaVdoF1rpgaUwxghimOtqZt2RM6017J6+IisOd9cWvq1qOvdfEV7ol9nTYYyGLKyiwX89r8EcDAwx5ogcklqyWPthrDQPnZ9qKEWOPKoowARuSstAdJFWYpBAKTKu8sLIQppTOitXQW1zNYN4rbo7i5Vj1rYh1zMJiOpbfRStj4rAwp0/Q1Wo1jvs4jGJBFgSz+IyBv8rJZ8mirNu3TlqU88xz9VNsAH/ijkVqZhGHg6kNroI4FRkASeZ54s8AGxjZNdxOJ2GpYqljZzMHKehBLBlFMQwTo64bQ5Hsij2CAgWu+Pjcl3YvAFTv/TR1ryuK6fKY+Wvdw6Q62ls7iegYjh/6JesdNH2tZFwQOvibCtJnkRwTSI4DcB2y8rVWkHzNnhxZ3n/gi/yqPTS9ygqiyJR72i2ApPgL0taM+ZuDqqGM8UMuLwFUfRy5z0sfYHwnHW1K1pRWCdBdvW7Kk9GJ2LfNldzffXDUBeft690hlPJTLL9AokEb4oG9tg/oSASLgLQIMXL11Xe6KM3CtuKA1d0dQAXcQQJ7wFBvatEDQUL4OxwbU2jIOPi6KgkKlfiOQcWJdYjuyEvTuEUkjDLjLJEBA4/24gMFCBIgAEbCAQB6BK7NrLDjWgggrm9D0sGNkj//5LxFwEgFM8qy1oVgLciBt34A27CqhjMrSmeK8RwrvHxsXBK0uOFJ3V2GZcY1wv7MgcD3MMRQUU8Qds5Tq0NclagNRFEmWC8ZGrZeDrEukQA0EdJeO9te4BW2Wz6j+uPCIQwg8b0mXQs3QWcLMUTGdSwr2AmTlXeCcciN/kOAO3V2Fk/sbowk/S35V3CsqWwtwfwHA5cJUAn0dt91mq+eCr6sW5LUXnEMQZPNlya+2khLZB+OuPt/5NQECellzCZQoeNWjguAEPRNlci5cKfwZuOqhbJnz6JssC6Q4Ywjs4ruUjWFpjhEejpvMcevLSSZeojw6P30Vyf078uQv1VNiUOWHbiD+9PVACJk574KvzViSmkse6cmpleWF/RHAzKcMPszuf0b1yH68P5ZLijJArLdPiSj15ocyqFaYS5G+ayVrjrsKF6LJyEjE+H8thClVI9YEwcGu/MLr77y9U3GUypNWEUUhcq+Oc0lbfb+7ZG1+uujOtIf3xrGMvo6DUvY6Lvg6uxWZOLjys5bJiJJf/AHYP8wyBpW1YJZlFkdcGEbYTWSZnkE79FiTcz8EWv4zCLb2O8oDniGw6SqkCesOKFlG5JAgGKub2jFyQ1yT8Au1fnfcyqnqdbw71WXFuijU9XdysPSfjes/kVyr4l2BuWfNAYtYafj0tZ125YKv7VjaUApThRtC482Jz+egKQPXjKBjA5gxGVk0uTz8YJOTpTmFTQMUs6e2B7U3p7ScGwRbeEf43K7WwJmt1/tsQT3d8eLss+sdN3ds+ENxeU3Frr+6OTrR1XF1KWo95HOe4NpUhP6zcdeCovozrl1PRtE03F+KZUKsdcr0taILqqxd8bW+pU0lYM9BFl8RQJrwOdD9zBz0Zzc9I+joX9yZkUWTy0edi7d6lPu97NhQFSMDioPQu9prDmhB1BMhSNhSO8BPnxCQSdaphfwhRIN0J5jDPbJTNc1kWhBFS12L26zeCGjJn7QqMI4w9WfjuNaivcIqDqw968xSH6AaEa/DR1/3dIvK/874WsW62EyxfwmLjwggaJ0Evb+Vk+7xnmM5KeeDWEw+rNfTUzaZ7tAdg9VT3gjntiA4XLkT+1RNUexJEQTYGWZF7QA/fUFAklcHT8KC/c2+aBxbT4zc4BeiNXb9xBXlN2D42iSXIbBSTKmXJNmjz0yiT9HqDgmCz7poEwYsFJ+N8pjfcKSLdtvSCTfVF/RkyX0erorLn76Oi1S6ei75Op0FRq4ahQCIm7IZgdIeE/hM1rTKPiJWNp+pYxkXUdYBJckhrBl5OEn95HU7PpL8muJcMTwIPqFrzeDnavwrgevQIPhR7QA/fUBg8zLsXTQIQesGH7RNquPvg+AduiM3mw7kysfVDTkgsTZ5icuvf71tX+l/rBxHlkbRqOlBcIiL1mJIQfnZuF1xTYiLiPbQKYoGY4haM7VIhO3vIbHpv/R1U3iynXTM19mMyXz10Zk5kIE1BBC0ToQwCXpOtSa0vyDOuPbHJNERTD4oT/LsuDKRQgWr3BkEX9I1acSPa/wrgSv+/KZ2gJ8uI9DZDu2ODcNxx9cWKbusbVrdELQqb36wH4F/sqI/Wjf1OKSNyqhu6cq0IPi4q0ZjJlh5lHbE5a7arq3XRqwVk5cC6ZUdy5K825q+1vOEa77WszQWZwausWDKvxKCVtl051egOTlrsy1n+d6LR5rDRt3IdRqW/pRzydcDUTR2uurkA4Ydgr0HsqcwyBwEePn6UuVWeQn4YyktS0IEMPteKa/i77NhOKz4GOK1KDBaeRZq0PequMb+wJDrCtmgSa+jLZw3Il12ws2xlSpIRTyE/rerpmBG8FkspQgqD0oVJSdhffOaOWE4VfsZrKJ9FqbIffv2+CwMBrw22X1OXw8IaOoKrvk6tSFmLmTgagZHFS4IVtEHD/4QJP2QU1SEJGeKJYQsmRCQvUqiaNlYtSwf6SUcJgPRCzPp6eHFxwTB/9RVe7VsQnsg/qn0x5AGuWUdpE5Sk7zzN2E48kU19mRcGASQ+3wuHizKJd5Oo72UCMM9a6Noy0zVUaWWmzDrekuRZ9N7YYovG6JoFgbOnEwTrugahh1vwO+6o4md34UsNK3ylLYoap2huo5dsJzxg0SI0teJ4Ipb2Ulfx1Vep957dNgGGASLbgPvt2vxLzBfJFwEI0HyW4QkIOfKy85p5KFCBweB3B836ak+9mr04b5cpj4cHjqDMZutnDk2WPx2oFQC18rOwlG0BEdPP3DG6D+dL8jumUnStoyKJzNvEOgMgvt1lZVNrUZiGW2q8h1cpXiDSsjeDv7Fe71RI7R3BsHPEbg6XfCQVPb7DKQY7ZhdpsE9YKq9dhhtavTKpA2Lvk6K2MD1XfX1wJqr1TgZAeZI7FGBiWjj5V/A8ZcgzEewFAgBBq4GnIlsvh+DjWLgKruzlKsPtyII/pdeJmLN6ZO+XftPPitrXOWf0arOlA758ltEDgsRaITA8ig6b2qjk8aOr2vHAIokzCcueCT1GvVJzCDWBZOvK8ta1x1RNFt/1i0W6E0rwe93Nq1g5ORmGTgsRZFZdsxgK27KJDBukPWtXUkBpa+TIta8vsu+bq656lnE8jqTBAiG5Tnyf1W1J/M8EHglD6FFk4nAdY3uOldBbJz04eQeL37BbtvI1L1a11CZbBraK2P3QOD6VBA8pCt85qUYZWzVlUHu3iKAta3I0bnXgv6p15Bineur29UVlLGr129XF+OAAGzY8oIDagyoAvLH/lPf79MPiaKNcwdUpgAVgOWv9M1ouTGNDPo6DWqNr3HZ1421tnJG1lBqlavAuJBvHNACzHG+q5Vm5x03W0E9rHPdGgTKg8Qy67pccVZXAZeULF8Pgtv1Z1srk017eqp4IHA9F+8DxZSvclnfhpEIprAoo+wje7S9q2VeXr/MuCO1jDDselP9oSfaTb+g6IM8b2J2HbOtfhRrfm95SJZU+AFKOi0xAzdvppU1za2SEpa80NfJMWtwhfO+bqC3pcPv15KDIGcTeP+VFn/ytY4A04QNQo50u0UG2TVgVZmos9OlbaCB9uGOKJqGzKkLtOVg5f41fWX06iQhr+qGvhXMfpftn15PvKOrWR3IzTUE5D2eh6unG4jVa7CcO9u7bzGW9hU7+K1psyMnBylYzI+UHRuz68aMw3uKlHfNE1Xld27Vt4wp7Roj+B04Kq9hF6NXbcF9njojjL420HA88bUBS9OywOvPoilpLx7oOrR/6WcpZ9ENpAXPG0LgJUN8yAYIHBUED6ZaK5YYvRXPJr7EowswifNbfXXFU6t/2FdOr8AVU6H/1LeC+e8ym7Rhnnm+5OgrAhi1sZQyuueGrBhhI6FfSsa9fpmKzdrWWhgZ1Lekr4Q3guDf9NNL+krN9h1rr5/UTxcWHadhScWGWdm0dfNqZFX82ygrqu2/NosY+joLet3X+uLr7Jam5oDuVvDp1FfHu/AiVFsdryprOYwAZ1xNOgdviEAfZJlJlvV5yaaLb55X/5zfR9uj6Er022UHbuWyGnvSzOnX9eoVuM4Mw/Y1ymp0s59wP0YbsWSQpewIIHd8PlJGW+3gcGj294WG4b71QfCAJX0vj6LOE+3IsiNlYxTNnYxXHtmRZlAKUkjh93sMcmzCatDLRUsZ3gu/I6vCgt9lhHZVto0A6esmbXPgU175emBzNGvM12SOWVf0z4OPg/ZoyiFvdQQsDeyr2+GMgEFB8Hk7yoy+F7FOoVKG0WeXV9ldZwe/kX9WT06vwFUq7A0CWdhvoWxfj84ZsrJYyooA3os6sRWLu+3YL+mDw42MPiPN9XI7OouU8OmiPPgewAN8jMfpayOC4C/t+F1+59YVJs1I7nNssWgpbXEldhPuP0Kb1G/0dVLEuuv76Ot0lhq56hg8208ywqkBEwSvshHaZQ1O87D7CEiC11Puq+mXhsiqeSL1WpJEpsrmwms2FWZvHywBGR8ElpaxYaogmPCLenD3C1xbg+AfZcxav0jS2OrdRZtZ0MetIBJwA6BzKC3TUjlI0qaMlIlh+MprRjjFYSIPvu148K1BvOxxgb9PCYJNYo2vZQrWR6+1sjmXIDQJa+DW3e0rVgf0xnb5du/zIz56QHaGf+jrFOB56usUlpq8ZL5JZvV4IXiV91AXd+18PaOLc+wJ+I8z5qb9id2Fu6wN6CBMDtY9Z9oE6/zw5g/M/Oy1s9xHrBt0CV5pF9Wzs1/git1r9mB9iqWUOHHoG9hglDsN13NOYY9FUYvdG0Di44lG03uR3H+2Pf/Io2LkTm9nXqv+LkK+DPx+oT2/T8J+AFu9DV7XRNFwbG+6294P3QqshwnbTfmHvo6PpO++jm+p8Zqyg7yNJf+SLfKIce3JUBuBR7UFlJX/hCC4rd/iSTUwKgPRz6ux12aMPtybmHiQiM1OkfnwCXc1ktU/cEVNbGFmMbVkMvoHq7s489rIRQU7jlH5dUGw394NIPh1NBy5SYvuhDB8GAM8Fot0//dh5jVqtSg0s6hqh9ayvzOr3ZDB8DBcvdzKxg41FUajY7vOux88GWTBQMVOu4MVEz5cQ83EJ30dD8Ui+DqepSq15Bb5MxXOPZhiQAerwCprzP+9x2H+6z4CHGzQ8hH2K9kVBNdose/PV4JX/14JWuvDjbOyGVMNta7LMNvaVfvW97Nu4CrvdG23lhInKkkYs2m/9+mQYgpLQwSkg7MNMzDyUiR7RWZbZ96pIW+k1VlXsaCSaNsGHOdp2GOaJ/ScaD94MW1Ff34YeDmn/1HNI/KD9+ZmDO55kWkNv88CGptsTCO9hXob1rCPfPGt72b+o6+b41gkXze3VPXsVcBxqKoEMEfwin568BHQ/9OWRf5GEJAJwaeNcCKTughMDoKv2nlLRE28bOuyDhN1fiz9wnNpGnbRtTwAXZlt/VYNsXqfdQNXqYhOx5/Uu0DvmAw8TpR0yFY9GeScFwLwzDf9CgAAHYZJREFU62zI3iTzhnbL4PMb5cln1cP+rOsBjWVX7kUHvjn4D/SbC7XW2w1e7ABxCmZd260O7Ild45CZsgsbtrr9fIR+86FsDq9vmHS6oGS60NeNES2arxtbqn4Gb5YILlGXAgHVmddP4l+VwVwbNpRIxuPwl50tZ0oEai9Tu98SYXHWVaTL1E0l1pEBXmcLnu8yQbLK/mj51ksG6rM3DFxlc4qV1l77UfPdgRmlBbUj/PQfAUylXwkrctjSfQ1+qMffp4kgbiDVXSGb6I5X5cgsXGR/LKCJUnJqexTdig9Lu8gOoIzSaazRPleJdRO2lWGANvjcvecj1sBsi6LHoLylXcJ7wiRrW83PttYk0Nc1JKqfBfZ1H0ttfv0i7msrfUQEQ/thmATKXwc1TMezaTxl1UXgkbpHedAoAjOC4Mv21rrWVK/c6njt3X7pG7tVsAkTZhAXQ6n77SsmffaZdwwkt2HgKhe+bnUjkl6q3hRFO6WDZneJVC8V+CUrAvDf8C1R1IZGdl1WXumun6geVB4ehs9gh+H2dPplvUpm4QLEC5VZrqzMMl8PPabJIlykUF+amZnjDOaE4fb1VtfH9AKk9nx0YtACLj8ROYj7oYzKrGcvy+t+mTSn7mFDB+nrt4Asuq/fstT6f62QeJEtqQheI9CXIO8M0HJbciknEQIPJqrNyukQwHu737SU8dBfwZbrqhMQTsQ6eL7Pxm951/BcBuYFnf2x+uxNA1f5wX4jCG7oD7aNI8NbIQX94M5bAaaVkchmVkGHsaArQW2gnkVmve4GzW52fdnOAQ+ZFdo5Jgha87F9Fd7nOPQZG7JHBMHJNuQ0kXE7dp+VdtjapI7aKcgdhln1uyFglRNPXzVLezPG+phrELzmVCrPRxm0WJjXxnaQPRaz67Jx1NP5pYS/do+p9zM3cyR9XR5fN2sHyue+hntqtLKMXuwRvC7FgeNA3+51gl/yRuBF+Mb4mv28jXJV/swwvGNFbspVJiBkzH8RKJdYB3JH7erOmHohv9/y9iVheHisPnvTwFX8OCUIrrI/jd6zBQ2R2RtZ2yVB44D69rzSxP+QOXZPd+cMb3aozBy29uErs14XgF6IIulHljuAhf1zcQNEwOOmPjhZ/CrLQlacZkugpNXnN8BTs3K0tMO2KNrzPOBvrR3V/IScUaBFkLEbN6bcA+UqmLXAoMWxORt9dRBEGDeoPB+t/OhB1kSQpBJtwuy6dHpzKvLLdOSnrQinr8vjaysNqq6QQ3H0K3XPKB5EgLQDJP2sD4KWKYpKwhoZ+pV3z8os9Jmgd4Nk35VrQE+Bil7uKrqBrtmHTuvMnHW6HPIl1sFgdIRJT/0COTIhJ5MO2xCwnq4vsZEE2SJr5h81Opvq+EYEIzDOlSKjEshKUyzI8YaM2aDF6YzeLmv8SlOA0XDQlXLHuVE2ymyv3YI2s84N46ta7JQZ2Hkgo4M9wg90IrbFkwDZo/Jqm1aDWIU1vQ4BIdkfraZtBU/x+1wM4rXh05GyVzVFuB6G9HVerjfra1gxJi9LBpC7D+ePr9f2bByDbOn7fAyU1/P9Zcj+Y9CgZvbi/CyQPHfxU1S4Im3AyBsDO7B8Rxmd1mZ+8u3ccqd+y/c9Bt+Zz+Tsvscd68NtnKvSVl7L70HW4L7b04YTElAPM2UweMnDUDp+Bop/72tKgiMAktm289zqyIrb1qgFKAPhs6N7sEOUcK3IA1DulVSjeLhOfC3XCx9Pi17gKu1itXOoVIaRpGMnA3CpZmJxnfh9HshBv6+QGd9cCn2NFmG1mPc11Hc1cBVknwSFuTTuqlCRD5IAcilIOziUjJGHQPNBQ5PYjfqngF4FFan8PAkGzeoycG2GTv1z7j3fK01b4pITQaniHVwnk0tzQCkn43ClWkn+fI//cETnB+/g3Ks71Vm/IcU42o46N4N+DFqBtJc9+BywwA/SkT8G9NcghVRHebfgEd4t+wMuPTu68kOCjMhA0pgkXefjSMQ9t2cFHHOkSLrB5hFhOLUjL4VWYcRumtObE0ka9eAl+PMA6CVQO0iKPBBxi1f2aj8Sn+8EzQXlmAoK6cbKa9h59ki1VCDcM3Kfy3ICV8sWKLYUJL5/DoRXlAbynOwEyT0ua+uOAMl6bcf9vh4qTm4ZaMt8VFIp9LUKrA2Y6vgaPhwDgXJPuFr+HP2YW1xQDljJc/NCkKTlv82QTsjMrKT83ovPH8DWtWn5Qj/pm8gu9vKbVYTyaeDxPROGSOB6MPaeMMGrAY+Z0LW9wTkvD6/FUhg0KHnwOFoO9OEehYK/Acm9I323nr/lU/Bd+utnofbpg/GPm2UN1JqW+Lc8fuAK9p2I+If480Lkdqj8W5Agsx0kReJuScGQB1wryEJpuwHB6xUWBGUSgYf/HDD4GijHPPdMJuDivSfZ2pCpmabtAHNGswo8lwMCuoGrGIQ5zrn4gSj0a4BycFwdkbum29iQqY7gA4fo6wNQKP+j42s8ol0PXLG5Z3AyggLpwzhTgJsMZMr+Ee8FvQd0JChuWY6KT1bpAdiGF1eYKdBrAjhJJ/5wMxxz4yJ91UOBjZHBdwau6fz4JrIJxwWBDKqwqCFQCcDHoa1vTioiUeAqzDEasQijEZcnFVTu+k+MDsM5teDZKSjwwD9xd667gpqCY/01YTh5oSlumfhgJh+9jp357c6WSfuCXqwfuApwfD5qN59N52u/mzmuBfR1XKTS1tPztQeBq4D2OxAGY80EMWm90Ow64Dge548CoZ9fIfkuEwTS35EZbemUyucrsAN7GOoV6PI+cJeMEncnmAY2/w7gdMnA1eLVYOAaD6d6tVYgrRajIOfWO8djJhDYd1oYDlmahlPiwFWEYAHz5plBcEgageW8ZiVGF2fMc8127P57N4IrhRRp25bKq28OO9621Gby8CPaivO5rbdtpls5z9kJXAXb17EfwPTCpFi71Fo23Yag9XMuaURfa3lD19eeBK4C7u0IZP5UC+Wi8YVf/zds+oLHduEtlOETpvRn4JoNyTcQ6+BVaIx1ssFY5+r1V2Gi6fo6J2IdShW4Bm6vd41luN1KMiX+K3dmXbHxAoY+NxXjhlwRhGFrunas3AiQTjgHQ7+PK4sh+1gI2AtcRR3+4MVySoJKK7BGuXVmggusVaWvTUOt72uPAlcB9zMIZv7FNMpF5Ae/ToJdkpIs+3L4Vh6Hn083qTQD14xoYjMkLB7dLSkELKYQWIV3rx8ma+ZTl5ZUV4bhPjhSHhAssRCQzJVpkkLjRMFTvSBBq2QhtWLZtZtlCF7ujtXy57upHbXSRADLKcY7vLuDpukKvFeAZ6tsHOVkoa9NusVtX5u0NAGvbyMgOzVB/dJWReAnj13ZpNPH8lUflS60ztjoFbHOOJl6YjGBwNolWYNW0SJd4IoL8YDYsBfrL0yYUg4eo850wU68a3Qhpi0KkPogS2h2YwfhwOlnyrAwvA+7LFzmgu+pg0UEwrALGQ1DGLxmxVwQbMUYUCC7kLpZ6GtDfvHA14YsTchGtkuQ11gck/C6slZ/wEPDn0af+kEP9S68yvDLZkw9YfUPSzYEJGidekY2Ht1Xpw5c5fKhYfgMooazTShSfB47JuZtI374xmKa/Oq89cguX4LWHbm+9iaJDVD05p1BcFWSa1i3AAggMwXB6yDZ1pwlDQISyEyWoNXpwamKZfR1Ggf3uMYjX/fQ2uK/8rqtn+E3nB3ogUF/fuAqztX4mnMaUaEDCCB4XY0vTi5VOaCk0/+swz4/ZoJWMTNT4CoM0Kt4GC8D/LD8z+I2AquC4FG3NYyjnXRwRqPZ5feu1jha9q0zMgyv58xrX1RK8B2zcdPwnMWL1tpLYK1BE9dtQdA6yIugtWY1fV1DIuGnh75OaKGh6oeBjwSv3r0b3pD9cdmo7l4cV4kE9V5E3fsT1GfVHBBA8NoOsZPcH0XNAZymIrfilaCHGt2cNnPgKvoeFIaL4Ux5txeLqwjgFS1Yi3Wcq+rF02ttOzqz8rJiL58dMvPKNa/xPF2oWmEYTQ3DmeuCwMcUthxcsR67hB86Fvd5Vw7Cs4mkrxPi57GvE1pqqPo7wOdhBK9InmJpgMD+BsddPXwtgiJ3l0K4iloOesFPGzZg4y/ZXYUlDgK7LwnDQ66IUzNJHSOBqwjEFNhSfBydRHi56k57Kk97ka54AvL0PS7rsRPZ1JnozHr9gJc1rxzk8bgZZlAdb5WXUUeud26K4Q688mby8U2reHCSvo7jpGL4Oo6lhuucAH5LEbzOMMy3KOwmeGTIK9D1Po/0Lb2qGITuwEuLByGAbS89GM0BODYMD76jeZV0Z40FriIeoxFyE47bnU6Xgl+1Cpm6+RVspDU3P+mZJZ+PzuyFmbk4wqA6yDOJ94kjDrGoBp6RN0McN7Wrjznu81Gfq3/Kv6P0dVOfFcrXTS3VOXkU2D6B4FVmYFl6I/C23l+d/nY5nhP+ZZY4DakF5eCzSciiwqty7rEgzTMRlflovP4zlBR4lWI0cBUNoezmgzEBi7Q4rFFi6UZA1mXOlH3+cyuYprw4N+GpBVdCu+loU4UbkYRNG+Q+4ahd6sbh7YXw/TNrkW60ic/Iqg8r9/nMgt7n9HWvO7W4vu5lpp0vWD4fLEHweoodcd5IeY8nmv4YzzzV5SPohw/xBAsv1RwThjKhwj1+DnivA5swjUOzDmUHVbViPHCtaIodFpEqNZajETW/7bos7xRXBEky0+NR2b4kCA7G5iyV3dw80juBqrhPZNQOw61XJbiKVQuAgKQbjcczEhvb3VAAczKYsHlZ9T5vz8DE6Uvp65p7iu/rmqUWP5G1GPwSwetfWpTprCjgIH3aTzqr4FuKdeBfdZ9hG9xVlaGit+TyP8MIoI+6GCwncd1r8OEwHGF0E6ZGrtIJXKvSOBohQEhzbv1WFZLcPvCU/EFuwpMLxg0w+gwvN2dJbmuA6Px6XMYHXwrs4l8Sbo1f115NbGwnGxccXdLOBdJFxx1flvucvi6Pr+09QSqSZFbtRgRtPwSNsSzbNXEy+zXLNaXq6PNVBDz6WXgYHN+om9mDN/2xwJcbZJ5xRxDcVj40trbDZuw9WgngrZivGriKBVVjRpc3Le4gpLrmv6HQTGzl7f77JHdjllU2qbZ3A1i5y2IIgc2VBx+qlm32FfaGLfj1OzYGTBmqtNyY4WLVS+H7V5ARIc/iksy+7sAsa+WHrnBLAAZqKPT1QAjxfAYEPoZrf4Pg9V0ZeHh7KeyWzapu9sCAl6CjtWc9NuX8jg4m66V/j9VOLDUERoWh7NEwUzVPtibMjU8MPh8yE+2gw6Y66oGrGAOjtktaHP69zKZx+cradw3sRhkuLy52ogxzFv/KbS47kGGWNUT2ZHkL7JfZ19EYuZPOfZGLjExin6rwehnYQQtQfvfe6h86DSZehwAsZPZ1En4B2p3WNbVylXnl07ABE2ZZ7f7QpVZZ40L6WgNV8uxG4A/w8R8I4r4MOqgsoMDWj8LWX4CmeGAzlo6Fnbb0xFP3n3Rkbb9Hh6/fXOHbdtmZCFYUON7pEt9L/y2XwWcB12rBA2YUOuWPj/T+naKNYJMgbNRMabyNauR2PIpCLNbvmpSbAn0Fy+tYB2PEJp/G31cb177jXpmFIOZXw4PgENd0S6/Pvgfg8wvh836Dku0wWOf9DiuxYcAMK2sv0uPS+0pAcSIAenpU78M+f7sEPr/DZwO0dKevtZBtzBeYS0pt0TeQfA02LsB992BjJPw+Az8iWaUye/nnnlhyN/zxKdu6vhpFbRjRaDUsF1t0cMa1GaZon5jwDm4CXdqsnj/nOjGhMvTsvP1uZca1p1Ng8HZMpx+PY8f267n2rOjn/1dhbSZMdDBoFTwx0o9pbwdexVGZeUHAKgM2DFobNXVg8woWDkimwkkIYD3vZO2XNHAsAxkyD3bVvfURpJ3fCIv0x2VwZIgC3/QaxbkSGD1THbU9zV/fC/bBJZIKDnvuiGN3GevQ12X0uhWbZfb15+g8fx/kw0xkIlBg0ym44GmQL0HrKuj6PxIZaajyiCA42RCrKpu22/Dc2mCWZ/G4AaN9oM/BMrjA5/WvErAGWPY4TLKl6Hc8fOZswx+/y97F0B8TY36UdVF0dz54dz0PubP9QMk9LQW7rigSDH0qi6Bs7IlDzLq2mTVu71z3PJlcI2DSui+KHjOLjRa3PW3gPCe5lbxCEAB29LVyUwDGY0BlKjth7A2gQ5WhVWcPGw4D5dSHgeR0ZS8uO1UdnCYCVkTRwnSq971qNd6uyJIGASA5GDTfo5jnVugrkycs9RAAOLP2+Ncpl4enl05dbzxAABJ1yy45eiUodvBSr33w2FsICJaCaQVZ/ONgkQDrxCgKki9FQDr7G1G02YxNu+a/hVox/gMuw0AL3PO99MsqHSMvn4cutg76Ws8rwLZsgavcn1Lk0fFN0FQ9dHU4Q+dpoG+AOkC+lat0UEnGdU3mwU9pPkvZl0sGe93aAFLaswSGjpXKwPM8KGU9I7cuUD4cBFjSKV9Y6QY55s5udTqlUz0fJLnrXhfMvCrN3lW61TK6Jy9IZ1FEABhPBF3ZaSzYA7d0RYLVOSAjDzuMSGaYXaw8PQo/sw+sxfdynxkK9MEpWRG5vM8V7+8aa+BMX9fAMPAJPMsauNbu8N34RzrNhlNIDTinDwvoiEHQ6Hsg/Mx5WX4CrZMP4vbBwdTXralnq9Fj9Ciz0BRe6nzQNoDrbNCt+cU9lWD1PPrXgLcBYmu+zoT0qNKUJBVYRiC8SQeOCz9sWgDKWHphxNG4uOAbrgcnShqKPAAlmGgDKZaKzyXjQOQZCVb7wgG+KZYRVNL2vR9U6ovFQN+B1XDQXJD4RCuQFb7S2ZUBisI9CwfC2JXzgj2Ivs7gEOBX9sAVEBwov8d/XwQ5M9AMXY4HfQUkuvlcXofy4zM0VZVLoZP0Z5OURSqKkGk/BOCUsSAJIhcrBrJt4C/LuKT/5l1/yZlRoH7e63MA4EoK2tmgy0Cn9zlt8usyMPsRCLufBr/DQmTsjVLsAmyHwcKvYSuVywdowbJB0ArQr0BPgZ6U78Co1K+wAQZOFvhV7m/ZdXE66GjQu0GzQMeAZFfNQ6qEj15F/Cy0FfQ66Deg34F+D1oF2gqfd+HTSoEd0qG6ECT3/TtBffWWe/Z7oFugV92Nn3CuVAWYya08GfR20LuqFNfvK1H/JZDg+muQ7E5q1eeQxxITAfo6JlA9qgGzMuwq3MPiWP/KM11eKfNz0GOg5/A83Y9P1QJf4CUTwQmgE0GyeeT7QDobzIOxxSK7050JDJ+wKDO+KAw4Y7Hqf8dWmTfVf2+SqD/4Bvz5e9hQ+H5wfODs1mzyfJ8BDx3SoM9e68PV+y3fAn96vU7Zm8C1b1OBMyXYkofb8SDpkEmn/DBQ7YGHzm3lxhMHSql9iiPl3aprQL8FvQp6GbQNzix9AFbFdSjwkB+sGnUBG2uBCuSyEAEiQASIABFQQQC/cwxcB0Z2G6osBUkQ+yyoDbQSfYG9+ExcqphPxIWyQZT022qB6tvwv0rWDvjmWa4AVhL4OV/gG4l/ZJB7J0h2wN0F3aUDzUIEnEPA28DVOSSpEBEgAkSACBABIuA8AgxcU7tIBrBXgdpBK0CYsKvMEMggtwQ6QkNAEqD2JRkQL0v5JgK/L5TFWNpJBGwiwMDVJtqURQSIABEgAkSACOSKAAPXXOEvuvB7YOCnELh6nY5ZdCfRPn8RKGJ6hr/eoOZEgAgQASJABIgAESACPiLwIJS+mEGrj66jzr4gwMDVF09RTyJABIgAESACRIAIEAEXEZCN7D6GoDXVGmAXDaJORMBFBBi4uugV6kQEiAARIAJEgAgQASLgAwKywec5CFplcyMWIkAEFBFg4KoILlkTASJABIgAESACRIAIFBaBlbDsjxC0biyshTSMCDiEAANXh5xBVYgAESACRIAIEAEiQAS8QOA/oeWpCFoleGUhAkTAAgIMXC2ATBFEgAgQASJABIgAESAChUHgSVhyGoJWeT0QCxEgApYQYOBqCWiKIQJEgAgQASJABIgAEfAegQdgwVwErZu9t4QGEAHPEGDg6pnDqC4RIAJEgAgQASJABIhALgjcDqkfRdC6KxfpFEoESo4AA9eSNwCaTwSIABEgAkSACBABIjAgAv+AgPVPQfsHrMkKRIAIqCAwWIUrmRIBIkAEiAARIAJEgAgQAf8RkNnVv0DA+h3/TaEFRMBvBBi4+u0/ak8EiAARIAJEgAgQASKgg8BLYHsegtYXddiTKxEgAkkQYKpwErRYlwgQASJABIgAESACRKAMCHwXRp7IoLUMrqaNviDAGVdfPEU9iQARIAJEgAgQASJABLQR6ICAzyNglY2YWIgAEXAIAQauDjmDqhABIkAEiAARIAJEgAjkhsDvIPkTCFrlk4UIEAHHEGCqsGMOoTpEgAgQASJABIgAESACVhHYC2nfAJ3EoNUq7hRGBBIhwBnXRHCxMhEgAkSACBABIkAEiECBEHgUtlyGgPX3BbKJphCBQiLAGddCupVGEQEiQASIABEgAkSACDRBYC3OXYiA9f0MWpugxFNEwCEEGLg65AyqQgSIABEgAkSACBABIqCKwH5w/ybobQhY71GVROZEgAgYRYCpwkbhJDMiQASIABEgAkSACBABRxH4KfT6WwSsLziqH9UiAkSgCQIMXJuAw1NEgAgQASJABIgAESACXiPQBe3/FXQtAtbnvLaEyhOBkiPAwLXkDYDmEwEiQASIABEgAkSggAjsg013g76OgPWlAtpHk4hA6RBg4Fo6l9NgIkAEiAARIAJEgAgUFoE9sOx20HUIWNsKayUNIwIlRICBawmdTpOJABEgAkSACBABIlAwBCQN+C7Q3QhY1xfMNppDBIgAEGDgymZABIgAESACRIAIEAEi4CMCq6D090DfRbD6nz4aQJ2JABGIjwAD1/hYsSYRIAJEgAgQASJABIhAvgjsgHjZbElmVx9FwCqbL7EQASJQAgQYuJbAyTSRCBABIkAEiAARIAKeItAJvf8D9AjoUfkfwaocYyECRKBkCDBwLZnDaS4RIAJEgAgQASJABBxGQHYDfgYkQaoEq08gUN2FTxYiQARKjgAD15I3AJpPBIgAESACRIAIEIEcEOiAzP8CvdyHXkKgKunALESACBCBXggwcO0FB78QASJABIgAESACREANgf3gLAHbzuqn/F+kNZqSwru9B0kA2vP7NnxfCZJg9XUEqBE+WYgAESACsRBg4BoLJlYiAkSACBABIkAEiEAsBPai1jLQr0GvguRdou1CCNS24JOFCBABIkAEUiDAwDUFaLyECBABIkAEiAARIAJVBGTG9CnQ/aDHQc8iQN2DTxYiQASIABEwiAADV4NgkhURIAJEgAgQASJQGgSWwNI7QYsRqG4ojdU0lAgQASJABIgAESACRIAIEAEiQAR0EYiiaAwobdmGC/8RdIyuluROBIgAESACRIAIEAEiQASIABEgAqVFAEFnmsB1J677Bmh8aYGj4USACBABIkAEiAARIAJEgAgQASJgB4EUgesduGayHe0ohQgQASJABIgAESACRIAIEAEiQARKj0CCwPUl1D2j9IARACJABIgAESACRIAIEAEiQASIABGwi0DMwPWfUW+4Xc0ojQgQASJABIgAESACRIAIEAEiQASIABAYIHDdhfMXEigiQASIABEgAkSACBABIkAEiAARIAK5IdAkcF2Hc+/NTTEKJgJEgAgQASJABIgAESACRIAIEAEiIAg0CFxX4fgsIkQEiAARIAJEgAgQASJABIgAESACRCB3BOoErqtx7IjcFaMCRIAIEAEiQASIABEgAkSACBABIkAEBIE+gesWfJ9NZIgAESACRIAIEAEiQASIABEgAkSACDiDQI/AdT/+/4AzilERIkAEiAARIAJEgAgQASJABIgAESACgkCPwPXviQgRIAJEgAgQASJABIgAESACRIAIEAHnEKgGrk/hc5BzylEhIkAEiAARIAJEgAgQASJABIgAESACCFhHgo4lEkSACBABIkAEiAARIAJEgAgQASJABIgAESACRIAIEAFjCPx/2P3JeG4VmJoAAAAASUVORK5CYII=">>). logo_fill() -> jlib:decode_base64(<<"iVBORw0KGgoAAAANSUhEUgAAAAYAAAA3BAMAAADdxCZzA" @@ -589,7 +676,7 @@ logo_fill() -> process_admin(global, #request{path = [], auth = {_, _, AJID}, lang = Lang}) -> - make_xhtml((?H1GL((?T(<<"Administration">>)), <<"toc">>, + make_xhtml((?H1GL((?T(<<"Administration">>)), <<"">>, <<"Contents">>)) ++ [?XE(<<"ul">>, @@ -658,7 +745,7 @@ process_admin(Host, [{{acl, '$1', '$2'}}]}])), {NumLines, ACLsP} = term_to_paragraph(ACLs, 80), make_xhtml((?H1GL((?T(<<"Access Control Lists">>)), - <<"ACLDefinition">>, <<"ACL Definition">>)) + <<"acldefinition">>, <<"ACL Definition">>)) ++ case Res of ok -> [?XREST(<<"Submitted">>)]; @@ -667,7 +754,7 @@ process_admin(Host, end ++ [?XAE(<<"form">>, - [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], + [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}]++direction(ltr), [?TEXTAREA(<<"acls">>, (iolist_to_binary(integer_to_list(lists:max([16, NumLines])))), @@ -694,7 +781,7 @@ process_admin(Host, [{{acl, {'$1', Host}, '$2'}, [], [{{acl, '$1', '$2'}}]}])), make_xhtml((?H1GL((?T(<<"Access Control Lists">>)), - <<"ACLDefinition">>, <<"ACL Definition">>)) + <<"acldefinition">>, <<"ACL Definition">>)) ++ case Res of ok -> [?XREST(<<"Submitted">>)]; @@ -702,9 +789,9 @@ process_admin(Host, nothing -> [] end ++ - [?XE(<<"p">>, [?ACT(<<"../acls-raw/">>, <<"Raw">>)])] ++ + [?XAE(<<"p">>, direction(ltr), [?ACT(<<"../acls-raw/">>, <<"Raw">>)])] ++ [?XAE(<<"form">>, - [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], + [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}]++direction(ltr), [acls_to_xhtml(ACLs), ?BR, ?INPUTT(<<"submit">>, <<"delete">>, <<"Delete Selected">>), @@ -760,7 +847,7 @@ process_admin(Host, [{{access, '$1', '$2'}}]}]), {NumLines, AccessP} = term_to_paragraph(lists:keysort(2,Access), 80), make_xhtml((?H1GL((?T(<<"Access Rules">>)), - <<"AccessRights">>, <<"Access Rights">>)) + <<"accessrights">>, <<"Access Rights">>)) ++ case Res of ok -> [?XREST(<<"Submitted">>)]; @@ -769,7 +856,7 @@ process_admin(Host, end ++ [?XAE(<<"form">>, - [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], + [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}]++direction(ltr), [?TEXTAREA(<<"access">>, (iolist_to_binary(integer_to_list(lists:max([16, NumLines])))), @@ -793,7 +880,7 @@ process_admin(Host, [{{access, {'$1', Host}, '$2'}, [], [{{access, '$1', '$2'}}]}]), make_xhtml((?H1GL((?T(<<"Access Rules">>)), - <<"AccessRights">>, <<"Access Rights">>)) + <<"accessrights">>, <<"Access Rights">>)) ++ case Res of ok -> [?XREST(<<"Submitted">>)]; @@ -801,10 +888,10 @@ process_admin(Host, nothing -> [] end ++ - [?XE(<<"p">>, [?ACT(<<"../access-raw/">>, <<"Raw">>)])] + [?XAE(<<"p">>, direction(ltr), [?ACT(<<"../access-raw/">>, <<"Raw">>)])] ++ [?XAE(<<"form">>, - [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], + [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}]++direction(ltr), [access_rules_to_xhtml(AccessRules, Lang), ?BR, ?INPUTT(<<"submit">>, <<"delete">>, <<"Delete Selected">>)])], @@ -852,7 +939,7 @@ process_admin(global, lang = Lang}) -> Res = list_vhosts(Lang, AJID), make_xhtml((?H1GL((?T(<<"Virtual Hosts">>)), - <<"virtualhost">>, <<"Virtual Hosting">>)) + <<"virtualhosting">>, <<"Virtual Hosting">>)) ++ Res, global, Lang, AJID); process_admin(Host, @@ -1049,8 +1136,9 @@ term_to_string(T) -> %% @spec (T::any(), Cols::integer()) -> {NumLines::integer(), Paragraph::string()} term_to_paragraph(T, Cols) -> - Paragraph = list_to_binary(erl_prettypr:format(erl_syntax:abstract(T), - [{paper, Cols}])), + P1 = erl_syntax:abstract(T), + P2 = erl_prettypr:format(P1, [{paper, Cols}]), + Paragraph = list_to_binary(P2), FieldList = ejabberd_regexp:split(Paragraph, <<"\n">>), NumLines = length(FieldList), {NumLines, Paragraph}. @@ -1114,7 +1202,7 @@ string_to_spec(<<"server_regexp">>, Val) -> {server_regexp, Val}; string_to_spec(<<"node_regexp">>, Val) -> #jid{luser = U, lserver = S, resource = <<"">>} = - jlib:string_to_jid(Val), + jid:from_string(Val), {node_regexp, U, S}; string_to_spec(<<"user_glob">>, Val) -> string_to_spec2(user_glob, Val); @@ -1122,7 +1210,7 @@ string_to_spec(<<"server_glob">>, Val) -> {server_glob, Val}; string_to_spec(<<"node_glob">>, Val) -> #jid{luser = U, lserver = S, resource = <<"">>} = - jlib:string_to_jid(Val), + jid:from_string(Val), {node_glob, U, S}; string_to_spec(<<"all">>, _) -> all; string_to_spec(<<"raw">>, Val) -> @@ -1132,7 +1220,7 @@ string_to_spec(<<"raw">>, Val) -> string_to_spec2(ACLName, Val) -> #jid{luser = U, lserver = S, resource = <<"">>} = - jlib:string_to_jid(Val), + jid:from_string(Val), case U of <<"">> -> {ACLName, S}; _ -> {ACLName, {U, S}} @@ -1348,7 +1436,7 @@ list_users_parse_query(Query, Host) -> lists:keysearch(<<"newusername">>, 1, Query), {value, {_, Password}} = lists:keysearch(<<"newuserpassword">>, 1, Query), - case jlib:string_to_jid(<<Username/binary, "@", + case jid:from_string(<<Username/binary, "@", Host/binary>>) of error -> error; @@ -1432,8 +1520,7 @@ get_offlinemsg_length(ModOffline, User, Server) -> case ModOffline of none -> <<"disabled">>; _ -> - pretty_string_int(ModOffline:get_queue_length(User, - Server)) + pretty_string_int(ModOffline:count_offline_messages(User,Server)) end. get_offlinemsg_module(Server) -> @@ -1449,10 +1536,10 @@ get_lastactivity_menuitem_list(Server) -> end. us_to_list({User, Server}) -> - jlib:jid_to_string({User, Server, <<"">>}). + jid:to_string({User, Server, <<"">>}). su_to_list({Server, User}) -> - jlib:jid_to_string({User, Server, <<"">>}). + jid:to_string({User, Server, <<"">>}). %%%================================== %%%% get_stats @@ -1464,10 +1551,8 @@ get_stats(global, Lang) -> + Total end, 0, ?MYHOSTS), - S2SConns = ejabberd_s2s:dirty_get_connections(), - S2SConnections = length(S2SConns), - S2SServers = length(lists:usort([element(2, C) - || C <- S2SConns])), + OutS2SNumber = ejabberd_s2s:outgoing_s2s_number(), + InS2SNumber = ejabberd_s2s:incoming_s2s_number(), [?XAE(<<"table">>, [], [?XE(<<"tbody">>, [?XE(<<"tr">>, @@ -1478,10 +1563,10 @@ get_stats(global, Lang) -> ?XC(<<"td">>, (pretty_string_int(OnlineUsers)))]), ?XE(<<"tr">>, [?XCT(<<"td">>, <<"Outgoing s2s Connections:">>), - ?XC(<<"td">>, (pretty_string_int(S2SConnections)))]), + ?XC(<<"td">>, (pretty_string_int(OutS2SNumber)))]), ?XE(<<"tr">>, - [?XCT(<<"td">>, <<"Outgoing s2s Servers:">>), - ?XC(<<"td">>, (pretty_string_int(S2SServers)))])])])]; + [?XCT(<<"td">>, <<"Incoming s2s Connections:">>), + ?XC(<<"td">>, (pretty_string_int(InS2SNumber)))])])])]; get_stats(Host, Lang) -> OnlineUsers = length(ejabberd_sm:get_vh_session_list(Host)), @@ -1509,8 +1594,8 @@ list_online_users(Host, _Lang) -> SUsers). user_info(User, Server, Query, Lang) -> - LServer = jlib:nameprep(Server), - US = {jlib:nodeprep(User), LServer}, + LServer = jid:nameprep(Server), + US = {jid:nodeprep(User), LServer}, Res = user_parse_query(User, Server, Query), Resources = ejabberd_sm:get_user_resources(User, Server), @@ -1549,18 +1634,24 @@ user_info(User, Server, Query, Lang) -> c2s_compressed_tls -> <<"tls+zlib">>; http_bind -> - <<"http-bind">> + <<"http-bind">>; + websocket -> + <<"websocket">>; + _ -> + <<"unknown">> end, - <<" (", ConnS/binary, + <<ConnS/binary, "://", (jlib:ip_to_list(IP))/binary, ":", (jlib:integer_to_binary(Port))/binary, "#", - (jlib:atom_to_binary(Node))/binary, - ")">> + (jlib:atom_to_binary(Node))/binary>> end, - ?LI([?C((<<R/binary, FIP/binary>>))]) + case direction(Lang) of + [{_, <<"rtl">>}] -> ?LI([?C((<<FIP/binary, " - ", R/binary>>))]); + _ -> ?LI([?C((<<R/binary, " - ", FIP/binary>>))]) + end end, lists:sort(Resources))))] end, @@ -1640,8 +1731,7 @@ user_parse_query1(Action, User, Server, Query) -> end. list_last_activity(Host, Lang, Integral, Period) -> - {MegaSecs, Secs, _MicroSecs} = now(), - TimeStamp = MegaSecs * 1000000 + Secs, + TimeStamp = p1_time_compat:system_time(seconds), case Period of <<"all">> -> TS = 0, Days = infinity; <<"year">> -> TS = TimeStamp - 366 * 86400, Days = 366; @@ -1780,7 +1870,7 @@ get_node(Host, Node, [], _Query, Lang) -> <<"Modules">>)])] ++ MenuItems2))]; get_node(global, Node, [<<"db">>], Query, Lang) -> - case rpc:call(Node, mnesia, system_info, [tables]) of + case ejabberd_cluster:call(Node, mnesia, system_info, [tables]) of {badrpc, _Reason} -> [?XCT(<<"h1">>, <<"RPC Call Error">>)]; Tables -> @@ -1792,7 +1882,7 @@ get_node(global, Node, [<<"db">>], Query, Lang) -> Rows = lists:map(fun (Table) -> STable = iolist_to_binary(atom_to_list(Table)), - TInfo = case rpc:call(Node, mnesia, + TInfo = case ejabberd_cluster:call(Node, mnesia, table_info, [Table, all]) of @@ -1871,9 +1961,7 @@ get_node(global, Node, [<<"backup">>], Query, Lang) -> [?XRES(<<(?T(<<"Error">>))/binary, ": ", (list_to_binary(io_lib:format("~p", [Error])))/binary>>)] end, - (?H1GL(list_to_binary(io_lib:format(?T(<<"Backup of ~p">>), [Node])), - <<"list-eja-commands">>, - <<"List of ejabberd Commands">>)) + [?XC(<<"h1">>, list_to_binary(io_lib:format(?T(<<"Backup of ~p">>), [Node])))] ++ ResS ++ [?XCT(<<"p">>, @@ -2014,7 +2102,7 @@ get_node(global, Node, [<<"backup">>], Query, Lang) -> [?INPUTT(<<"submit">>, <<"import_dir">>, <<"OK">>)])])])])])]; get_node(global, Node, [<<"ports">>], Query, Lang) -> - Ports = rpc:call(Node, ejabberd_config, + Ports = ejabberd_cluster:call(Node, ejabberd_config, get_local_option, [listen, {ejabberd_listener, validate_cfg}, []]), @@ -2028,14 +2116,14 @@ get_node(global, Node, [<<"ports">>], Query, Lang) -> {error, iolist_to_binary(io_lib:format("~p", [Reason]))}; _ -> nothing end, - NewPorts = lists:sort(rpc:call(Node, ejabberd_config, + NewPorts = lists:sort(ejabberd_cluster:call(Node, ejabberd_config, get_local_option, [listen, {ejabberd_listener, validate_cfg}, []])), H1String = <<(?T(<<"Listened Ports at ">>))/binary, (iolist_to_binary(atom_to_list(Node)))/binary>>, - (?H1GL(H1String, <<"listened">>, <<"Listening Ports">>)) + (?H1GL(H1String, <<"listeningports">>, <<"Listening Ports">>)) ++ case Res of ok -> [?XREST(<<"Submitted">>)]; @@ -2051,7 +2139,7 @@ get_node(global, Node, [<<"ports">>], Query, Lang) -> [node_ports_to_xhtml(NewPorts, Lang)])]; get_node(Host, Node, [<<"modules">>], Query, Lang) when is_binary(Host) -> - Modules = rpc:call(Node, gen_mod, + Modules = ejabberd_cluster:call(Node, gen_mod, loaded_modules_with_opts, [Host]), Res = case catch node_modules_parse_query(Host, Node, Modules, Query) @@ -2060,10 +2148,10 @@ get_node(Host, Node, [<<"modules">>], Query, Lang) {'EXIT', Reason} -> ?INFO_MSG("~p~n", [Reason]), error; _ -> nothing end, - NewModules = lists:sort(rpc:call(Node, gen_mod, + NewModules = lists:sort(ejabberd_cluster:call(Node, gen_mod, loaded_modules_with_opts, [Host])), H1String = list_to_binary(io_lib:format(?T(<<"Modules at ~p">>), [Node])), - (?H1GL(H1String, <<"modoverview">>, + (?H1GL(H1String, <<"modulesoverview">>, <<"Modules Overview">>)) ++ case Res of @@ -2076,21 +2164,21 @@ get_node(Host, Node, [<<"modules">>], Query, Lang) [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], [node_modules_to_xhtml(NewModules, Lang)])]; get_node(global, Node, [<<"stats">>], _Query, Lang) -> - UpTime = rpc:call(Node, erlang, statistics, + UpTime = ejabberd_cluster:call(Node, erlang, statistics, [wall_clock]), UpTimeS = list_to_binary(io_lib:format("~.3f", [element(1, UpTime) / 1000])), - CPUTime = rpc:call(Node, erlang, statistics, [runtime]), + CPUTime = ejabberd_cluster:call(Node, erlang, statistics, [runtime]), CPUTimeS = list_to_binary(io_lib:format("~.3f", [element(1, CPUTime) / 1000])), OnlineUsers = mnesia:table_info(session, size), - TransactionsCommitted = rpc:call(Node, mnesia, + TransactionsCommitted = ejabberd_cluster:call(Node, mnesia, system_info, [transaction_commits]), - TransactionsAborted = rpc:call(Node, mnesia, + TransactionsAborted = ejabberd_cluster:call(Node, mnesia, system_info, [transaction_failures]), - TransactionsRestarted = rpc:call(Node, mnesia, + TransactionsRestarted = ejabberd_cluster:call(Node, mnesia, system_info, [transaction_restarts]), - TransactionsLogged = rpc:call(Node, mnesia, system_info, + TransactionsLogged = ejabberd_cluster:call(Node, mnesia, system_info, [transaction_log_writes]), [?XC(<<"h1">>, list_to_binary(io_lib:format(?T(<<"Statistics of ~p">>), [Node]))), @@ -2125,12 +2213,12 @@ get_node(global, Node, [<<"stats">>], _Query, Lang) -> ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], (pretty_string_int(TransactionsLogged)))])])])]; get_node(global, Node, [<<"update">>], Query, Lang) -> - rpc:call(Node, code, purge, [ejabberd_update]), + ejabberd_cluster:call(Node, code, purge, [ejabberd_update]), Res = node_update_parse_query(Node, Query), - rpc:call(Node, code, load_file, [ejabberd_update]), + ejabberd_cluster:call(Node, code, load_file, [ejabberd_update]), {ok, _Dir, UpdatedBeams, Script, LowLevelScript, Check} = - rpc:call(Node, ejabberd_update, update_info, []), + ejabberd_cluster:call(Node, ejabberd_update, update_info, []), Mods = case UpdatedBeams of [] -> ?CT(<<"None">>); _ -> @@ -2199,14 +2287,14 @@ get_node(Host, Node, NPath, Query, Lang) -> node_parse_query(Node, Query) -> case lists:keysearch(<<"restart">>, 1, Query) of {value, _} -> - case rpc:call(Node, init, restart, []) of + case ejabberd_cluster:call(Node, init, restart, []) of {badrpc, _Reason} -> error; _ -> ok end; _ -> case lists:keysearch(<<"stop">>, 1, Query) of {value, _} -> - case rpc:call(Node, init, stop, []) of + case ejabberd_cluster:call(Node, init, stop, []) of {badrpc, _Reason} -> error; _ -> ok end; @@ -2290,35 +2378,35 @@ node_backup_parse_query(Node, Query) -> {value, {_, Path}} -> Res = case Action of <<"store">> -> - rpc:call(Node, mnesia, backup, + ejabberd_cluster:call(Node, mnesia, backup, [binary_to_list(Path)]); <<"restore">> -> - rpc:call(Node, ejabberd_admin, + ejabberd_cluster:call(Node, ejabberd_admin, restore, [Path]); <<"fallback">> -> - rpc:call(Node, mnesia, + ejabberd_cluster:call(Node, mnesia, install_fallback, [binary_to_list(Path)]); <<"dump">> -> - rpc:call(Node, ejabberd_admin, + ejabberd_cluster:call(Node, ejabberd_admin, dump_to_textfile, [Path]); <<"load">> -> - rpc:call(Node, mnesia, + ejabberd_cluster:call(Node, mnesia, load_textfile, [binary_to_list(Path)]); <<"import_piefxis_file">> -> - rpc:call(Node, ejabberd_piefxis, + ejabberd_cluster:call(Node, ejabberd_piefxis, import_file, [Path]); <<"export_piefxis_dir">> -> - rpc:call(Node, ejabberd_piefxis, + ejabberd_cluster:call(Node, ejabberd_piefxis, export_server, [Path]); <<"export_piefxis_host_dir">> -> {value, {_, Host}} = lists:keysearch(<<Action/binary, "host">>, 1, Query), - rpc:call(Node, ejabberd_piefxis, + ejabberd_cluster:call(Node, ejabberd_piefxis, export_host, [Path, Host]); <<"export_sql_file">> -> @@ -2326,13 +2414,13 @@ node_backup_parse_query(Node, Query) -> lists:keysearch(<<Action/binary, "host">>, 1, Query), - rpc:call(Node, ejd2odbc, + ejabberd_cluster:call(Node, ejd2odbc, export, [Host, Path]); <<"import_file">> -> - rpc:call(Node, ejabberd_admin, + ejabberd_cluster:call(Node, ejabberd_admin, import_file, [Path]); <<"import_dir">> -> - rpc:call(Node, ejabberd_admin, + ejabberd_cluster:call(Node, ejabberd_admin, import_dir, [Path]) end, case Res of @@ -2380,18 +2468,18 @@ node_ports_to_xhtml(Ports, Lang) -> [?INPUTS(<<"text">>, <<"module", SSPort/binary>>, SModule, <<"15">>)]), - ?XE(<<"td">>, + ?XAE(<<"td">>, direction(ltr), [?TEXTAREA(<<"opts", SSPort/binary>>, (iolist_to_binary(integer_to_list(NumLines))), <<"35">>, SOptsClean)]), ?XE(<<"td">>, [?INPUTT(<<"submit">>, <<"add", SSPort/binary>>, - <<"Update">>)]), + <<"Restart">>)]), ?XE(<<"td">>, [?INPUTT(<<"submit">>, <<"delete", SSPort/binary>>, - <<"Delete">>)])]) + <<"Stop">>)])]) end, Ports) ++ @@ -2406,12 +2494,12 @@ node_ports_to_xhtml(Ports, Lang) -> ?XE(<<"td">>, [?INPUTS(<<"text">>, <<"modulenew">>, <<"">>, <<"15">>)]), - ?XE(<<"td">>, + ?XAE(<<"td">>, direction(ltr), [?TEXTAREA(<<"optsnew">>, <<"2">>, <<"35">>, <<"[]">>)]), ?XAE(<<"td">>, [{<<"colspan">>, <<"2">>}], [?INPUTT(<<"submit">>, <<"addnew">>, - <<"Add New">>)])])]))]). + <<"Start">>)])])]))]). make_netprot_html(NetProt) -> ?XAE(<<"select">>, [{<<"name">>, <<"netprotnew">>}], @@ -2457,10 +2545,10 @@ node_ports_parse_query(Node, Ports, Query) -> {ok, Tokens, _} = erl_scan:string(binary_to_list(SOpts) ++ "."), {ok, Opts} = erl_parse:parse_term(Tokens), - rpc:call(Node, ejabberd_listener, + ejabberd_cluster:call(Node, ejabberd_listener, delete_listener, [PortIpNetp2, Module1]), - R = rpc:call(Node, ejabberd_listener, + R = ejabberd_cluster:call(Node, ejabberd_listener, add_listener, [PortIpNetp2, Module, Opts]), throw({is_added, R}); @@ -2470,7 +2558,7 @@ node_ports_parse_query(Node, Ports, Query) -> 1, Query) of {value, _} -> - rpc:call(Node, ejabberd_listener, + ejabberd_cluster:call(Node, ejabberd_listener, delete_listener, [PortIpNetp, Module1]), throw(submitted); @@ -2503,7 +2591,7 @@ node_ports_parse_query(Node, Ports, Query) -> {Port2, _SPort, IP2, _SIP, _SSPort, NetProt2, OptsClean} = get_port_data({Port2, STIP2, NetProt2}, Opts), - R = rpc:call(Node, ejabberd_listener, add_listener, + R = ejabberd_cluster:call(Node, ejabberd_listener, add_listener, [{Port2, IP2, NetProt2}, Module, OptsClean]), throw({is_added, R}); _ -> ok @@ -2523,7 +2611,7 @@ node_modules_to_xhtml(Modules, Lang) -> 40), ?XE(<<"tr">>, [?XC(<<"td">>, SModule), - ?XE(<<"td">>, + ?XAE(<<"td">>, direction(ltr), [?TEXTAREA(<<"opts", SModule/binary>>, (iolist_to_binary(integer_to_list(NumLines))), <<"40">>, SOpts)]), @@ -2542,7 +2630,7 @@ node_modules_to_xhtml(Modules, Lang) -> [?XE(<<"tr">>, [?XE(<<"td">>, [?INPUT(<<"text">>, <<"modulenew">>, <<"">>)]), - ?XE(<<"td">>, + ?XAE(<<"td">>, direction(ltr), [?TEXTAREA(<<"optsnew">>, <<"2">>, <<"40">>, <<"[]">>)]), ?XAE(<<"td">>, [{<<"colspan">>, <<"2">>}], @@ -2562,9 +2650,9 @@ node_modules_parse_query(Host, Node, Modules, Query) -> {ok, Tokens, _} = erl_scan:string(binary_to_list(<<SOpts/binary, ".">>)), {ok, Opts} = erl_parse:parse_term(Tokens), - rpc:call(Node, gen_mod, stop_module, + ejabberd_cluster:call(Node, gen_mod, stop_module, [Host, Module]), - rpc:call(Node, gen_mod, start_module, + ejabberd_cluster:call(Node, gen_mod, start_module, [Host, Module, Opts]), throw(submitted); _ -> @@ -2572,7 +2660,7 @@ node_modules_parse_query(Host, Node, Modules, Query) -> 1, Query) of {value, _} -> - rpc:call(Node, gen_mod, stop_module, + ejabberd_cluster:call(Node, gen_mod, stop_module, [Host, Module]), throw(submitted); _ -> ok @@ -2588,7 +2676,7 @@ node_modules_parse_query(Host, Node, Modules, Query) -> Module = jlib:binary_to_atom(SModule), {ok, Tokens, _} = erl_scan:string(binary_to_list(<<SOpts/binary, ".">>)), {ok, Opts} = erl_parse:parse_term(Tokens), - rpc:call(Node, gen_mod, start_module, + ejabberd_cluster:call(Node, gen_mod, start_module, [Host, Module, Opts]), throw(submitted); _ -> ok @@ -2601,7 +2689,7 @@ node_update_parse_query(Node, Query) -> proplists:get_all_values(<<"selected">>, Query), ModulesToUpdate = [jlib:binary_to_atom(M) || M <- ModulesToUpdateStrings], - case rpc:call(Node, ejabberd_update, update, + case ejabberd_cluster:call(Node, ejabberd_update, update, [ModulesToUpdate]) of {ok, _} -> ok; @@ -2643,14 +2731,14 @@ pretty_print_xml(#xmlel{name = Name, attrs = Attrs, [{Attr, Val} | RestAttrs] -> AttrPrefix = [Prefix, str:copies(<<" ">>, byte_size(Name) + 2)], - [$\s, Attr, $=, $', xml:crypt(Val) | [$', + [$\s, Attr, $=, $', fxml:crypt(Val) | [$', lists:map(fun ({Attr1, Val1}) -> [$\n, AttrPrefix, Attr1, $=, $', - xml:crypt(Val1), + fxml:crypt(Val1), $'] end, RestAttrs)]] @@ -2662,7 +2750,7 @@ pretty_print_xml(#xmlel{name = Name, attrs = Attrs, end, Els), if OnlyCData -> - [$>, xml:get_cdata(Els), $<, $/, Name, $>, $\n]; + [$>, fxml:get_cdata(Els), $<, $/, Name, $>, $\n]; true -> [$>, $\n, lists:map(fun (E) -> @@ -2809,7 +2897,7 @@ make_server_menu(HostMenu, NodeMenu, Lang, JID) -> Fixed2 = [Tuple || Tuple <- Fixed, is_allowed_path(BasePath, Tuple, JID)], - {Base, <<"ejabberd">>, Fixed2}. + {Base, <<"">>, Fixed2}. get_menu_items_hook({hostnode, Host, Node}, Lang) -> ejabberd_hooks:run_fold(webadmin_menu_hostnode, Host, @@ -2876,4 +2964,8 @@ make_menu_item(item, 3, URI, Name, Lang) -> %%%================================== + +opt_type(access) -> fun (V) -> V end; +opt_type(_) -> [access]. + %%% vim: set foldmethod=marker foldmarker=%%%%,%%%=: diff --git a/src/ejabberd_websocket.erl b/src/ejabberd_websocket.erl index 9d5f32c3..0cdd9bac 100644 --- a/src/ejabberd_websocket.erl +++ b/src/ejabberd_websocket.erl @@ -33,11 +33,13 @@ %%% NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE %%% POSSIBILITY OF SUCH DAMAGE. %%% ========================================================================================================== -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%%---------------------------------------------------------------------- -module(ejabberd_websocket). +-protocol({rfc, 6455}). + -author('ecestari@process-one.net'). -export([check/2, socket_handoff/8]). @@ -86,7 +88,8 @@ check(_Path, Headers) -> end. socket_handoff(LocalPath, #request{method = 'GET', ip = IP, q = Q, path = Path, - headers = Headers, host = Host, port = Port}, + headers = Headers, host = Host, port = Port, + opts = HOpts}, Socket, SockMod, Buf, _Opts, HandlerModule, InfoMsgFun) -> case check(LocalPath, Headers) of true -> @@ -99,7 +102,8 @@ socket_handoff(LocalPath, #request{method = 'GET', ip = IP, q = Q, path = Path, path = Path, headers = Headers, local_path = LocalPath, - buf = Buf}, + buf = Buf, + http_opts = HOpts}, connect(WS, HandlerModule); _ -> @@ -369,10 +373,10 @@ process_frame(#frame_info{unprocessed = process_frame(FrameInfo#frame_info{unprocessed = <<>>}, <<UnprocessedPre/binary, Data/binary>>). -handle_data(tcp, FrameInfo, Data, Socket, WsHandleLoopPid, p1_tls) -> - case p1_tls:recv_data(Socket, Data) of +handle_data(tcp, FrameInfo, Data, Socket, WsHandleLoopPid, fast_tls) -> + case fast_tls:recv_data(Socket, Data) of {ok, NewData} -> - handle_data_int(FrameInfo, NewData, Socket, WsHandleLoopPid, p1_tls); + handle_data_int(FrameInfo, NewData, Socket, WsHandleLoopPid, fast_tls); {error, Error} -> {error, Error} end; diff --git a/src/ejabberd_xmlrpc.erl b/src/ejabberd_xmlrpc.erl index 904604fc..c7e72d66 100644 --- a/src/ejabberd_xmlrpc.erl +++ b/src/ejabberd_xmlrpc.erl @@ -2,8 +2,25 @@ %%% File : ejabberd_xmlrpc.erl %%% Author : Badlop <badlop@process-one.net> %%% Purpose : XML-RPC server that frontends ejabberd commands -%%% Created : 21 Aug 2007 by Badlop <badlop@ono.com> -%%% Id : $Id: ejabberd_xmlrpc.erl 595 2008-05-20 11:39:31Z badlop $ +%%% Created : 21 Aug 2007 by Badlop <badlop@process-one.net> +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2016 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. +%%% %%%---------------------------------------------------------------------- %%% TODO: Implement a command in ejabberdctl 'help COMMAND LANGUAGE' that shows @@ -181,41 +198,43 @@ socket_type() -> raw. process(_, #request{method = 'POST', data = Data, opts = Opts}) -> AccessCommandsOpts = gen_mod:get_opt(access_commands, Opts, fun(L) when is_list(L) -> L end, - []), - AccessCommands = lists:flatmap( - fun({Ac, AcOpts}) -> - Commands = gen_mod:get_opt( - commands, AcOpts, - fun(A) when is_atom(A) -> - A; - (L) when is_list(L) -> - true = lists:all( - fun is_atom/1, - L), - L - end, all), - CommOpts = gen_mod:get_opt( - options, AcOpts, - fun(L) when is_list(L) -> L end, - []), - [{Ac, Commands, CommOpts}]; - (Wrong) -> - ?WARNING_MSG("wrong options format for ~p: ~p", - [?MODULE, Wrong]), - [] - end, AccessCommandsOpts), - GetAuth = case [ACom || {Ac, _, _} = ACom <- AccessCommands, Ac /= all] of - [] -> false; - _ -> true - end, + undefined), + AccessCommands = + case AccessCommandsOpts of + undefined -> undefined; + _ -> + lists:flatmap( + fun({Ac, AcOpts}) -> + Commands = gen_mod:get_opt( + commands, AcOpts, + fun(A) when is_atom(A) -> + A; + (L) when is_list(L) -> + true = lists:all( + fun is_atom/1, + L), + L + end, all), + CommOpts = gen_mod:get_opt( + options, AcOpts, + fun(L) when is_list(L) -> L end, + []), + [{Ac, Commands, CommOpts}]; + (Wrong) -> + ?WARNING_MSG("wrong options format for ~p: ~p", + [?MODULE, Wrong]), + [] + end, AccessCommandsOpts) + end, + GetAuth = true, State = #state{access_commands = AccessCommands, get_auth = GetAuth}, - case xml_stream:parse_element(Data) of + case fxml_stream:parse_element(Data) of {error, _} -> {400, [], #xmlel{name = <<"h1">>, attrs = [], children = [{xmlcdata, <<"Malformed XML">>}]}}; El -> - case p1_xmlrpc:decode(El) of + case fxmlrpc:decode(El) of {error, _} = Err -> ?ERROR_MSG("XML-RPC request ~s failed with reason: ~p", [Data, Err]), @@ -225,7 +244,7 @@ process(_, #request{method = 'POST', data = Data, opts = Opts}) -> {ok, RPC} -> ?DEBUG("got XML-RPC request: ~p", [RPC]), {false, Result} = handler(State, RPC), - XML = xml:element_to_binary(p1_xmlrpc:encode(Result)), + XML = fxml:element_to_binary(fxmlrpc:encode(Result)), {200, [{<<"Content-Type">>, <<"text/xml">>}], <<"<?xml version=\"1.0\"?>", XML/binary>>} end @@ -240,17 +259,22 @@ process(_, _) -> %% ----------------------------- get_auth(AuthList) -> - [User, Server, Password] = try get_attrs([user, server, - password], - AuthList) - of - [U, S, P] -> [U, S, P] - catch - exit:{attribute_not_found, Attr, _} -> - throw({error, missing_auth_arguments, - Attr}) - end, - {User, Server, Password}. + Admin = + case lists:keysearch(admin, 1, AuthList) of + {value, {admin, true}} -> true; + _ -> false + end, + try get_attrs([user, server, token], AuthList) of + [U, S, T] -> {U, S, {oauth, T}, Admin} + catch + exit:{attribute_not_found, _Attr, _} -> + try get_attrs([user, server, password], AuthList) of + [U, S, P] -> {U, S, P, Admin} + catch + exit:{attribute_not_found, Attr, _} -> + throw({error, missing_auth_arguments, Attr}) + end + end. %% ----------------------------- %% Handlers @@ -280,9 +304,9 @@ handler(#state{get_auth = true, auth = noauth} = State, {call, Method, [{struct, AuthList} | Arguments] = AllArgs}) -> try get_auth(AuthList) of - Auth -> - handler(State#state{get_auth = false, auth = Auth}, - {call, Method, Arguments}) + Auth -> + handler(State#state{get_auth = false, auth = Auth}, + {call, Method, Arguments}) catch {error, missing_auth_arguments, _Attr} -> handler(State#state{get_auth = false, auth = noauth}, @@ -311,7 +335,7 @@ handler(State, {call, Command, []}) -> handler(State, {call, Command, [{struct, []}]}); handler(State, {call, Command, [{struct, AttrL}]} = Payload) -> - case ejabberd_commands:get_command_format(Command) of + case ejabberd_commands:get_command_format(Command, State#state.auth) of {error, command_unknown} -> build_fault_response(-112, "Unknown call: ~p", [Payload]); @@ -440,7 +464,7 @@ format_arg({array, Elements}, {list, ElementsDef}) [format_arg(Element, ElementsDef) || Element <- Elements]; format_arg(Arg, integer) when is_integer(Arg) -> Arg; -format_arg(Arg, binary) when is_list(Arg) -> list_to_binary(Arg); +format_arg(Arg, binary) when is_list(Arg) -> process_unicode_codepoints(Arg); format_arg(Arg, binary) when is_binary(Arg) -> Arg; format_arg(Arg, string) when is_list(Arg) -> Arg; format_arg(Arg, string) when is_binary(Arg) -> binary_to_list(Arg); @@ -448,7 +472,12 @@ format_arg(undefined, binary) -> <<>>; format_arg(undefined, string) -> ""; format_arg(Arg, Format) -> ?ERROR_MSG("don't know how to format Arg ~p for format ~p", [Arg, Format]), - throw({error_formatting_argument, Arg, Format}). + error. + +process_unicode_codepoints(Str) -> + iolist_to_binary(lists:map(fun(X) when X > 255 -> unicode:characters_to_binary([X]); + (Y) -> Y + end, Str)). %% ----------------------------- %% Result @@ -462,10 +491,16 @@ format_result(Atom, {Name, atom}) -> [{Name, iolist_to_binary(atom_to_list(Atom))}]}; format_result(Int, {Name, integer}) -> {struct, [{Name, Int}]}; -format_result(String, {Name, string}) when is_list(String) -> +format_result([A|_]=String, {Name, string}) when is_list(String) and is_integer(A) -> {struct, [{Name, lists:flatten(String)}]}; format_result(Binary, {Name, string}) when is_binary(Binary) -> {struct, [{Name, binary_to_list(Binary)}]}; +format_result(Atom, {Name, string}) when is_atom(Atom) -> + {struct, [{Name, atom_to_list(Atom)}]}; +format_result(Integer, {Name, string}) when is_integer(Integer) -> + {struct, [{Name, integer_to_list(Integer)}]}; +format_result(Other, {Name, string}) -> + {struct, [{Name, io_lib:format("~p", [Other])}]}; format_result(String, {Name, binary}) when is_list(String) -> {struct, [{Name, lists:flatten(String)}]}; format_result(Binary, {Name, binary}) when is_binary(Binary) -> diff --git a/src/ejd2odbc.erl b/src/ejd2odbc.erl index 90781481..1df88470 100644 --- a/src/ejd2odbc.erl +++ b/src/ejd2odbc.erl @@ -5,7 +5,7 @@ %%% Created : 22 Aug 2005 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -29,7 +29,8 @@ -include("logger.hrl"). --export([export/2, export/3, import_file/2, import/2, import/3]). +-export([export/2, export/3, import_file/2, import/2, + import/3, delete/1]). -define(MAX_RECORDS_PER_TRANSACTION, 100). @@ -54,14 +55,14 @@ modules() -> mod_offline, mod_privacy, mod_private, - mod_pubsub, + %% mod_pubsub, mod_roster, mod_shared_roster, mod_vcard, mod_vcard_xupdate]. export(Server, Output) -> - LServer = jlib:nameprep(iolist_to_binary(Server)), + LServer = jid:nameprep(iolist_to_binary(Server)), Modules = modules(), IO = prepare_output(Output), lists:foreach( @@ -71,7 +72,7 @@ export(Server, Output) -> close_output(Output, IO). export(Server, Output, Module) -> - LServer = jlib:nameprep(iolist_to_binary(Server)), + LServer = jid:nameprep(iolist_to_binary(Server)), IO = prepare_output(Output), lists:foreach( fun({Table, ConvertFun}) -> @@ -79,6 +80,20 @@ export(Server, Output, Module) -> end, Module:export(Server)), close_output(Output, IO). +delete(Server) -> + Modules = modules(), + lists:foreach( + fun(Module) -> + delete(Server, Module) + end, Modules). + +delete(Server, Module) -> + LServer = jid:nameprep(iolist_to_binary(Server)), + lists:foreach( + fun({Table, ConvertFun}) -> + delete(LServer, Table, ConvertFun) + end, Module:export(Server)). + import_file(Server, FileName) when is_binary(FileName) -> import(Server, binary_to_list(FileName)); import_file(Server, FileName) -> @@ -86,7 +101,7 @@ import_file(Server, FileName) -> {file, FileName}, {mode, read_only}]) of {ok, Fd} -> - LServer = jlib:nameprep(Server), + LServer = jid:nameprep(Server), Mods = [{Mod, gen_mod:db_type(LServer, Mod)} || Mod <- modules(), gen_mod:is_loaded(LServer, Mod)], AuthMods = case lists:member(ejabberd_auth_internal, @@ -105,7 +120,7 @@ import(Server, Output) -> import(Server, Output, [{fast, true}]). import(Server, Output, Opts) -> - LServer = jlib:nameprep(iolist_to_binary(Server)), + LServer = jid:nameprep(iolist_to_binary(Server)), Modules = modules(), IO = prepare_output(Output, disk_log), lists:foreach( @@ -115,7 +130,7 @@ import(Server, Output, Opts) -> close_output(Output, IO). import(Server, Output, Opts, Module) -> - LServer = jlib:nameprep(iolist_to_binary(Server)), + LServer = jid:nameprep(iolist_to_binary(Server)), IO = prepare_output(Output, disk_log), lists:foreach( fun({SelectQuery, ConvertFun}) -> @@ -159,6 +174,25 @@ output(_LServer, Table, Fd, SQLs) -> file:write(Fd, ["-- \n-- Mnesia table: ", atom_to_list(Table), "\n--\n", SQLs]). +delete(LServer, Table, ConvertFun) -> + F = fun () -> + mnesia:write_lock_table(Table), + {_N, SQLs} = + mnesia:foldl( + fun(R, {N, SQLs} = Acc) -> + case ConvertFun(LServer, R) of + [] -> + Acc; + _SQL -> + mnesia:delete_object(R), + Acc + end + end, + {0, []}, Table), + delete(LServer, Table, SQLs) + end, + mnesia:transaction(F). + import(LServer, SelectQuery, IO, ConvertFun, Opts) -> F = case proplists:get_bool(fast, Opts) of true -> diff --git a/src/eldap.erl b/src/eldap.erl index a9edebdf..f48b2d84 100644 --- a/src/eldap.erl +++ b/src/eldap.erl @@ -80,7 +80,6 @@ -export([get_status/1]). -%% gen_fsm callbacks -export([init/1, connecting/2, connecting/3, wait_bind_response/3, active/3, active_bind/3, handle_event/3, handle_sync_event/4, handle_info/3, @@ -630,27 +629,10 @@ init([Hosts, Port, Rootdn, Passwd, Opts]) -> id = 0, dict = dict:new(), req_q = queue:new()}, 0}. -%%---------------------------------------------------------------------- -%% Func: StateName/2 -%% Called when gen_fsm:send_event/2,3 is invoked (async) -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} -%%---------------------------------------------------------------------- connecting(timeout, S) -> {ok, NextState, NewS} = connect_bind(S), {next_state, NextState, NewS}. -%%---------------------------------------------------------------------- -%% Func: StateName/3 -%% Called when gen_fsm:sync_send_event/2,3 is invoked. -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {reply, Reply, NextStateName, NextStateData} | -%% {reply, Reply, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} | -%% {stop, Reason, Reply, NewStateData} -%%---------------------------------------------------------------------- connecting(Event, From, S) -> Q = queue:in({Event, From}, S#eldap.req_q), {next_state, connecting, S#eldap{req_q = Q}}. @@ -679,34 +661,15 @@ handle_event(close, _StateName, S) -> handle_event(_Event, StateName, S) -> {next_state, StateName, S}. -%%---------------------------------------------------------------------- -%% Func: handle_sync_event/4 -%% Called when gen_fsm:sync_send_all_state_event/2,3 is invoked -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {reply, Reply, NextStateName, NextStateData} | -%% {reply, Reply, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} | -%% {stop, Reason, Reply, NewStateData} -%%---------------------------------------------------------------------- handle_sync_event(_Event, _From, StateName, S) -> {reply, {StateName, S}, StateName, S}. -%%---------------------------------------------------------------------- -%% Func: handle_info/3 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} -%% {stop, Reason, NewStateData} -%%---------------------------------------------------------------------- - %% %% Packets arriving in various states %% handle_info({Tag, _Socket, Data}, connecting, S) when Tag == tcp; Tag == ssl -> - ?DEBUG("tcp packet received when disconnected!~n~p", - [Data]), + ?DEBUG("tcp packet received when disconnected!~n~p", [Data]), {next_state, connecting, S}; handle_info({Tag, _Socket, Data}, wait_bind_response, S) when Tag == tcp; Tag == ssl -> @@ -725,8 +688,7 @@ handle_info({Tag, _Socket, Data}, wait_bind_response, S) {next_state, connecting, close_and_retry(S)} end; handle_info({Tag, _Socket, Data}, StateName, S) - when (StateName == active orelse - StateName == active_bind) + when (StateName == active orelse StateName == active_bind) andalso (Tag == tcp orelse Tag == ssl) -> case catch recvd_packet(Data, S) of {response, Response, RequestType} -> @@ -767,8 +729,7 @@ handle_info({timeout, Timer, {cmd_timeout, Id}}, handle_info({timeout, retry_connect}, connecting, S) -> {ok, NextState, NewS} = connect_bind(S), {next_state, NextState, NewS}; -handle_info({timeout, _Timer, bind_timeout}, - wait_bind_response, S) -> +handle_info({timeout, _Timer, bind_timeout}, wait_bind_response, S) -> {next_state, connecting, close_and_retry(S)}; %% %% Make sure we don't fill the message queue with rubbish @@ -825,17 +786,15 @@ send_command(Command, From, S) -> {Name, Request} = gen_req(Command), Message = #'LDAPMessage'{messageID = Id, protocolOp = {Name, Request}}, - ?DEBUG("~p~n", [{Name, Request}]), - {ok, Bytes} = 'ELDAPv3':encode('LDAPMessage', - Message), + ?DEBUG("~p~n", [{Name, ejabberd_config:may_hide_data(Request)}]), + {ok, Bytes} = 'ELDAPv3':encode('LDAPMessage', Message), case (S#eldap.sockmod):send(S#eldap.fd, Bytes) of ok -> - Timer = erlang:start_timer(?CMD_TIMEOUT, self(), - {cmd_timeout, Id}), - New_dict = dict:store(Id, - [{Timer, Command, From, Name}], S#eldap.dict), - {ok, S#eldap{id = Id, dict = New_dict}}; - Error -> Error + Timer = erlang:start_timer(?CMD_TIMEOUT, self(), {cmd_timeout, Id}), + New_dict = dict:store(Id, [{Timer, Command, From, Name}], S#eldap.dict), + {ok, S#eldap{id = Id, dict = New_dict}}; + Error -> + Error end. gen_req({search, A}) -> @@ -1148,9 +1107,8 @@ bind_request(Socket, S) -> authentication = {simple, S#eldap.passwd}}, Message = #'LDAPMessage'{messageID = Id, protocolOp = {bindRequest, Req}}, - ?DEBUG("Bind Request Message:~p~n", [Message]), - {ok, Bytes} = 'ELDAPv3':encode('LDAPMessage', - Message), + ?DEBUG("Bind Request Message:~p~n", [ejabberd_config:may_hide_data(Message)]), + {ok, Bytes} = 'ELDAPv3':encode('LDAPMessage', Message), case (S#eldap.sockmod):send(Socket, Bytes) of ok -> {ok, S#eldap{id = Id}}; Error -> Error @@ -1163,24 +1121,6 @@ next_host(Host, Hosts) -> % Find next in turn next_host(Host, Hosts, Hosts). -%%% -------------------------------------------------------------------- -%%% Verify the input data -%%% -------------------------------------------------------------------- -%%% -------------------------------------------------------------------- -%%% Get and Validate the initial configuration -%%% -------------------------------------------------------------------- -%% get_atom(Key, List) -> -%% case lists:keysearch(Key, 1, List) of -%% {value, {Key, Value}} when is_atom(Value) -> -%% Value; -%% {value, {Key, _Value}} -> -%% throw({error, "Bad Value in Config for " ++ atom_to_list(Key)}); -%% false -> -%% throw({error, "No Entry in Config for " ++ atom_to_list(Key)}) -%% end. -%%% -------------------------------------------------------------------- -%%% Other Stuff -%%% -------------------------------------------------------------------- next_host(Host, [Host], Hosts) -> hd(Hosts); % Wrap back to first next_host(Host, [Host | Tail], _Hosts) -> diff --git a/src/eldap_filter.erl b/src/eldap_filter.erl index e8ce625c..d46e410a 100644 --- a/src/eldap_filter.erl +++ b/src/eldap_filter.erl @@ -6,7 +6,7 @@ %%% Author: Evgeniy Khramtsov <ekhramtsov@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/eldap_pool.erl b/src/eldap_pool.erl index 51761c07..8e0096f8 100644 --- a/src/eldap_pool.erl +++ b/src/eldap_pool.erl @@ -5,7 +5,7 @@ %%% Created : 12 Nov 2006 by Evgeniy Khramtsov <xram@jabber.ru> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/eldap_utils.erl b/src/eldap_utils.erl index eb660162..9b9e5fbc 100644 --- a/src/eldap_utils.erl +++ b/src/eldap_utils.erl @@ -5,7 +5,7 @@ %%% Created : 12 Oct 2006 by Mickael Remond <mremond@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -24,20 +24,14 @@ %%%---------------------------------------------------------------------- -module(eldap_utils). + +-behaviour(ejabberd_config). -author('mremond@process-one.net'). --export([generate_subfilter/1, - find_ldap_attrs/2, - get_ldap_attr/2, - get_user_part/2, - make_filter/2, - get_state/2, - case_insensitive_match/2, - get_opt/3, - get_opt/4, - get_config/2, - decode_octet_string/3, - uids_domain_subst/2]). +-export([generate_subfilter/1, find_ldap_attrs/2, + get_ldap_attr/2, get_user_part/2, make_filter/2, + get_state/2, case_insensitive_match/2, get_config/2, + decode_octet_string/3, uids_domain_subst/2, opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -171,64 +165,48 @@ uids_domain_subst(Host, UIDs) -> end, UIDs). --spec get_opt({atom(), binary()}, list(), fun()) -> any(). - -get_opt({Key, Host}, Opts, F) -> - get_opt({Key, Host}, Opts, F, undefined). - --spec get_opt({atom(), binary()}, list(), fun(), any()) -> any(). - -get_opt({Key, Host}, Opts, F, Default) -> - case gen_mod:get_opt(Key, Opts, F, undefined) of - undefined -> - ejabberd_config:get_option( - {Key, Host}, F, Default); - Val -> - Val - end. - -spec get_config(binary(), list()) -> eldap_config(). get_config(Host, Opts) -> - Servers = get_opt({ldap_servers, Host}, Opts, + Servers = gen_mod:get_opt({ldap_servers, Host}, Opts, fun(L) -> [iolist_to_binary(H) || H <- L] end, [<<"localhost">>]), - Backups = get_opt({ldap_backups, Host}, Opts, + Backups = gen_mod:get_opt({ldap_backups, Host}, Opts, fun(L) -> [iolist_to_binary(H) || H <- L] end, []), - Encrypt = get_opt({ldap_encrypt, Host}, Opts, + Encrypt = gen_mod:get_opt({ldap_encrypt, Host}, Opts, fun(tls) -> tls; (starttls) -> starttls; (none) -> none end, none), - TLSVerify = get_opt({ldap_tls_verify, Host}, Opts, + TLSVerify = gen_mod:get_opt({ldap_tls_verify, Host}, Opts, fun(hard) -> hard; (soft) -> soft; (false) -> false end, false), - TLSCAFile = get_opt({ldap_tls_cacertfile, Host}, Opts, + TLSCAFile = gen_mod:get_opt({ldap_tls_cacertfile, Host}, Opts, fun iolist_to_binary/1), - TLSDepth = get_opt({ldap_tls_depth, Host}, Opts, + TLSDepth = gen_mod:get_opt({ldap_tls_depth, Host}, Opts, fun(I) when is_integer(I), I>=0 -> I end), - Port = get_opt({ldap_port, Host}, Opts, + Port = gen_mod:get_opt({ldap_port, Host}, Opts, fun(I) when is_integer(I), I>0 -> I end, case Encrypt of tls -> ?LDAPS_PORT; starttls -> ?LDAP_PORT; _ -> ?LDAP_PORT end), - RootDN = get_opt({ldap_rootdn, Host}, Opts, + RootDN = gen_mod:get_opt({ldap_rootdn, Host}, Opts, fun iolist_to_binary/1, <<"">>), - Password = get_opt({ldap_password, Host}, Opts, + Password = gen_mod:get_opt({ldap_password, Host}, Opts, fun iolist_to_binary/1, <<"">>), - Base = get_opt({ldap_base, Host}, Opts, + Base = gen_mod:get_opt({ldap_base, Host}, Opts, fun iolist_to_binary/1, <<"">>), - OldDerefAliases = get_opt({deref_aliases, Host}, Opts, + OldDerefAliases = gen_mod:get_opt({deref_aliases, Host}, Opts, fun(never) -> never; (searching) -> searching; (finding) -> finding; @@ -236,7 +214,7 @@ get_config(Host, Opts) -> end, unspecified), DerefAliases = if OldDerefAliases == unspecified -> - get_opt({ldap_deref_aliases, Host}, Opts, + gen_mod:get_opt({ldap_deref_aliases, Host}, Opts, fun(never) -> never; (searching) -> searching; (finding) -> finding; @@ -367,3 +345,43 @@ collect_parts_bit([{?N_BIT_STRING,<<Unused,Bits/binary>>}|Rest],Acc,Uacc) -> collect_parts_bit(Rest,[Bits|Acc],Unused+Uacc); collect_parts_bit([],Acc,Uacc) -> list_to_binary([Uacc|lists:reverse(Acc)]). + +opt_type(deref_aliases) -> + fun (never) -> never; + (searching) -> searching; + (finding) -> finding; + (always) -> always + end; +opt_type(ldap_backups) -> + fun (L) -> [iolist_to_binary(H) || H <- L] end; +opt_type(ldap_base) -> fun iolist_to_binary/1; +opt_type(ldap_deref_aliases) -> + fun (never) -> never; + (searching) -> searching; + (finding) -> finding; + (always) -> always + end; +opt_type(ldap_encrypt) -> + fun (tls) -> tls; + (starttls) -> starttls; + (none) -> none + end; +opt_type(ldap_password) -> fun iolist_to_binary/1; +opt_type(ldap_port) -> + fun (I) when is_integer(I), I > 0 -> I end; +opt_type(ldap_rootdn) -> fun iolist_to_binary/1; +opt_type(ldap_servers) -> + fun (L) -> [iolist_to_binary(H) || H <- L] end; +opt_type(ldap_tls_cacertfile) -> fun iolist_to_binary/1; +opt_type(ldap_tls_depth) -> + fun (I) when is_integer(I), I >= 0 -> I end; +opt_type(ldap_tls_verify) -> + fun (hard) -> hard; + (soft) -> soft; + (false) -> false + end; +opt_type(_) -> + [deref_aliases, ldap_backups, ldap_base, + ldap_deref_aliases, ldap_encrypt, ldap_password, + ldap_port, ldap_rootdn, ldap_servers, + ldap_tls_cacertfile, ldap_tls_depth, ldap_tls_verify]. diff --git a/src/elixir_logger_backend.erl b/src/elixir_logger_backend.erl new file mode 100644 index 00000000..c055853f --- /dev/null +++ b/src/elixir_logger_backend.erl @@ -0,0 +1,122 @@ +%%%------------------------------------------------------------------- +%%% @author Mickael Remond <mremond@process-one.net> +%%% @doc +%%% This module bridges lager logs to Elixir Logger. +%%% @end +%%% Created : 9 March 2016 by Mickael Remond <mremond@process-one.net> +%%% +%%% ejabberd, Copyright (C) 2002-2016 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(elixir_logger_backend). + +-behaviour(gen_event). + +-export([init/1, handle_call/2, handle_event/2, handle_info/2, terminate/2, + code_change/3]). + +-record(state, {level = debug}). + +init(Opts) -> + Level = proplists:get_value(level, Opts, debug), + State = #state{level = Level}, + {ok, State}. + +%% @private +handle_event({log, LagerMsg}, State) -> + #{mode := Mode, truncate := Truncate, level := MinLevel, utc_log := UTCLog} = 'Elixir.Logger.Config':'__data__'(), + MsgLevel = severity_to_level(lager_msg:severity(LagerMsg)), + case {lager_util:is_loggable(LagerMsg, lager_util:level_to_num(State#state.level), ?MODULE), + 'Elixir.Logger':compare_levels(MsgLevel, MinLevel)} of + {_, lt}-> + {ok, State}; + {true, _} -> + Metadata = normalize_pid(lager_msg:metadata(LagerMsg)), + Message = 'Elixir.Logger.Utils':truncate(lager_msg:message(LagerMsg), Truncate), + Timestamp = timestamp(lager_msg:timestamp(LagerMsg), UTCLog), + GroupLeader = case proplists:get_value(pid, Metadata, self()) of + Pid when is_pid(Pid) -> + erlang:process_info(self(), group_leader); + _ -> {group_leader, self()} + end, + notify(Mode, {MsgLevel, GroupLeader, {'Elixir.Logger', Message, Timestamp, Metadata}}), + {ok, State}; + _ -> + {ok, State} + end; +handle_event(_Msg, State) -> + {ok, State}. + +%% @private +%% TODO Handle loglevels +handle_call(get_loglevel, State) -> + {ok, lager_util:config_to_mask(State#state.level), State}; +handle_call({set_loglevel, Config}, State) -> + {ok, ok, State#state{level = Config}}. + +%% @private +handle_info(_Msg, State) -> + {ok, State}. + +%% @private +terminate(_Reason, _State) -> + ok. + +%% @private +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +notify(sync, Msg) -> + gen_event:sync_notify('Elixir.Logger', Msg); +notify(async, Msg) -> + gen_event:notify('Elixir.Logger', Msg). + +normalize_pid(Metadata) -> + case proplists:get_value(pid, Metadata) of + Pid when is_pid(Pid) -> Metadata; + Pid when is_list(Pid) -> + M1 = proplists:delete(pid, Metadata), + case catch erlang:list_to_pid(Pid) of + {'EXIT', _} -> + M1; + PidAsPid -> + [{pid, PidAsPid}|M1] + end; + _ -> + proplists:delete(pid, Metadata) + end. + +%% Return timestamp with milliseconds +timestamp(Time, UTCLog) -> + {_, _, Micro} = p1_time_compat:timestamp(), + {Date, {Hours, Minutes, Seconds}} = + case UTCLog of + true -> calendar:now_to_universal_time(Time); + false -> calendar:now_to_local_time(Time) + end, + {Date, {Hours, Minutes, Seconds, Micro div 1000}}. + + +severity_to_level(debug) -> debug; +severity_to_level(info) -> info; +severity_to_level(notice) -> info; +severity_to_level(warning) -> warn; +severity_to_level(error) -> error; +severity_to_level(critical) -> error; +severity_to_level(alert) -> error; +severity_to_level(emergency) -> error. diff --git a/src/ext_mod.erl b/src/ext_mod.erl index f83fe2c6..91526e71 100644 --- a/src/ext_mod.erl +++ b/src/ext_mod.erl @@ -5,7 +5,7 @@ %%% Created : 19 Feb 2015 by Christophe Romain <christophe.romain@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2006-2015 ProcessOne +%%% ejabberd, Copyright (C) 2006-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -24,37 +24,34 @@ %%%---------------------------------------------------------------------- -module(ext_mod). + +-behaviour(ejabberd_config). -author("Christophe Romain <christophe.romain@process-one.net>"). -%% Packaging service -export([start/0, stop/0, update/0, check/1, - available_command/0, available/0, available/1, - installed_command/0, installed/0, installed/1, - install/1, uninstall/1, - upgrade/0, upgrade/1, - add_sources/2, del_sources/1]). + available_command/0, available/0, available/1, + installed_command/0, installed/0, installed/1, + install/1, uninstall/1, upgrade/0, upgrade/1, + add_sources/2, del_sources/1, modules_dir/0, + config_dir/0, opt_type/1, get_commands_spec/0]). -include("ejabberd_commands.hrl"). +-include("logger.hrl"). -define(REPOS, "https://github.com/processone/ejabberd-contrib"). %% -- ejabberd init and commands start() -> - case is_contrib_allowed() of - true -> - [code:add_patha(module_ebin_dir(Module)) - || {Module, _} <- installed()], - application:start(inets), - ejabberd_commands:register_commands(commands()); - false -> - ok - end. + [code:add_patha(module_ebin_dir(Module)) + || {Module, _} <- installed()], + application:start(inets), + ejabberd_commands:register_commands(get_commands_spec()). stop() -> - ejabberd_commands:unregister_commands(commands()). + ejabberd_commands:unregister_commands(get_commands_spec()). -commands() -> +get_commands_spec() -> [#ejabberd_commands{name = modules_update_specs, tags = [admin,modules], desc = "", @@ -115,10 +112,19 @@ commands() -> update() -> add_sources(?REPOS), - lists:foreach(fun({Package, Spec}) -> + Res = lists:foldl(fun({Package, Spec}, Acc) -> Path = proplists:get_value(url, Spec, ""), - add_sources(Package, Path) - end, modules_spec(sources_dir(), "*")). + Update = add_sources(Package, Path), + ?INFO_MSG("Update package ~s: ~p", [Package, Update]), + case Update of + ok -> Acc; + Error -> [Error|Acc] + end + end, [], modules_spec(sources_dir(), "*")), + case Res of + [] -> ok; + [Error|_] -> Error + end. available() -> Jungle = modules_spec(sources_dir(), "*/*"), @@ -163,6 +169,7 @@ install(Package) when is_binary(Package) -> case compile_and_install(Module, Attrs) of ok -> code:add_patha(module_ebin_dir(Module)), + ejabberd_config:reload_file(), ok; Error -> delete_path(module_lib_dir(Module)), @@ -181,7 +188,8 @@ uninstall(Package) when is_binary(Package) -> code:purge(Module), code:delete(Module), code:del_path(module_ebin_dir(Module)), - delete_path(module_lib_dir(Module)); + delete_path(module_lib_dir(Module)), + ejabberd_config:reload_file(); false -> {error, not_installed} end. @@ -352,6 +360,10 @@ modules_dir() -> sources_dir() -> filename:join(modules_dir(), "sources"). +config_dir() -> + DefaultDir = filename:join(modules_dir(), "conf"), + getenv("CONTRIB_MODULES_CONF_DIR", DefaultDir). + module_lib_dir(Package) -> filename:join(modules_dir(), Package). @@ -444,9 +456,14 @@ compile_and_install(Module, Spec) -> true -> {ok, Dir} = file:get_cwd(), file:set_cwd(SrcDir), - Result = case compile(Module, Spec, LibDir) of - ok -> install(Module, Spec, LibDir); - Error -> Error + Result = case compile_deps(Module, Spec, LibDir) of + ok -> + case compile(Module, Spec, LibDir) of + ok -> install(Module, Spec, LibDir); + Error -> Error + end; + Error -> + Error end, file:set_cwd(Dir), Result; @@ -458,17 +475,46 @@ compile_and_install(Module, Spec) -> end end. +compile_deps(_Module, _Spec, DestDir) -> + case filelib:is_dir("deps") of + true -> ok; + false -> fetch_rebar_deps() + end, + Ebin = filename:join(DestDir, "ebin"), + filelib:ensure_dir(filename:join(Ebin, ".")), + Result = lists:foldl(fun(Dep, Acc) -> + Inc = filename:join(Dep, "include"), + Src = filename:join(Dep, "src"), + Options = [{outdir, Ebin}, {i, Inc}], + [file:copy(App, Ebin) || App <- filelib:wildcard(Src++"/*.app")], + Acc++[case compile:file(File, Options) of + {ok, _} -> ok; + {ok, _, _} -> ok; + {ok, _, _, _} -> ok; + error -> {error, {compilation_failed, File}}; + Error -> Error + end + || File <- filelib:wildcard(Src++"/*.erl")] + end, [], filelib:wildcard("deps/*")), + case lists:dropwhile( + fun(ok) -> true; + (_) -> false + end, Result) of + [] -> ok; + [Error|_] -> Error + end. + compile(_Module, _Spec, DestDir) -> Ebin = filename:join(DestDir, "ebin"), filelib:ensure_dir(filename:join(Ebin, ".")), EjabBin = filename:dirname(code:which(ejabberd)), EjabInc = filename:join(filename:dirname(EjabBin), "include"), - XmlHrl = filename:join(EjabInc, "xml.hrl"), - Logger = [{d, 'LAGER'} || code:is_loaded(lager)=/=false], + XmlHrl = filename:join(EjabInc, "fxml.hrl"), ExtLib = [{d, 'NO_EXT_LIB'} || filelib:is_file(XmlHrl)], Options = [{outdir, Ebin}, {i, "include"}, {i, EjabInc}, verbose, report_errors, report_warnings] - ++ Logger ++ ExtLib, + ++ ExtLib, + [file:copy(App, Ebin) || App <- filelib:wildcard("src/*.app")], Result = [case compile:file(File, Options) of {ok, _} -> ok; {ok, _, _} -> ok; @@ -503,16 +549,59 @@ install(Module, Spec, DestDir) -> Error -> Error end. +%% -- minimalist rebar spec parser, only support git + +fetch_rebar_deps() -> + case rebar_deps("rebar.config")++rebar_deps("rebar.config.script") of + [] -> + ok; + Deps -> + filelib:ensure_dir(filename:join("deps", ".")), + lists:foreach(fun({_App, Cmd}) -> + os:cmd("cd deps; "++Cmd++"; cd ..") + end, Deps) + end. +rebar_deps(Script) -> + case file:script(Script) of + {ok, Config} when is_list(Config) -> + [rebar_dep(Dep) || Dep <- proplists:get_value(deps, Config, [])]; + {ok, {deps, Deps}} -> + [rebar_dep(Dep) || Dep <- Deps]; + _ -> + [] + end. +rebar_dep({App, _, {git, Url}}) -> + {App, "git clone "++Url++" "++filename:basename(App)}; +rebar_dep({App, _, {git, Url, {branch, Ref}}}) -> + {App, "git clone -n "++Url++" "++filename:basename(App)++ + "; (cd "++filename:basename(App)++ + "; git checkout -q origin/"++Ref++")"}; +rebar_dep({App, _, {git, Url, {tag, Ref}}}) -> + {App, "git clone -n "++Url++" "++filename:basename(App)++ + "; (cd "++filename:basename(App)++ + "; git checkout -q "++Ref++")"}; +rebar_dep({App, _, {git, Url, Ref}}) -> + {App, "git clone -n "++Url++" "++filename:basename(App)++ + "; (cd "++filename:basename(App)++ + "; git checkout -q "++Ref++")"}. + %% -- YAML spec parser consult(File) -> - case p1_yaml:decode_from_file(File, [plain_as_atom]) of + case fast_yaml:decode_from_file(File, [plain_as_atom]) of {ok, []} -> {ok, []}; {ok, [Doc|_]} -> {ok, [format(Spec) || Spec <- Doc]}; - {error, Err} -> {error, p1_yaml:format_error(Err)} + {error, Err} -> {error, fast_yaml:format_error(Err)} end. format({Key, Val}) when is_binary(Val) -> {Key, binary_to_list(Val)}; format({Key, Val}) -> % TODO: improve Yaml parsing {Key, Val}. + +opt_type(allow_contrib_modules) -> + fun (false) -> false; + (no) -> false; + (_) -> true + end; +opt_type(_) -> [allow_contrib_modules]. diff --git a/src/extauth.erl b/src/extauth.erl index eb936ddf..50330b47 100644 --- a/src/extauth.erl +++ b/src/extauth.erl @@ -5,7 +5,7 @@ %%% Created : 30 Jul 2004 by Leif Johansson <leifj@it.su.se> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,11 +25,13 @@ -module(extauth). +-behaviour(ejabberd_config). + -author('leifj@it.su.se'). -export([start/2, stop/1, init/2, check_password/3, set_password/3, try_register/3, remove_user/2, - remove_user/3, is_user_exists/2]). + remove_user/3, is_user_exists/2, opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -93,15 +95,14 @@ remove_user(User, Server, Password) -> [<<"removeuser3">>, User, Server, Password]). call_port(Server, Msg) -> - LServer = jlib:nameprep(Server), + LServer = jid:nameprep(Server), ProcessName = get_process_name(LServer, random_instance(get_instances(LServer))), ProcessName ! {call, self(), Msg}, receive {eauth, Result} -> Result end. random_instance(MaxNum) -> - {A1, A2, A3} = now(), - random:seed(A1, A2, A3), + random:seed(p1_time_compat:timestamp()), random:uniform(MaxNum) - 1. get_instances(Server) -> @@ -157,3 +158,7 @@ encode(L) -> str:join(L, <<":">>). decode([0, 0]) -> false; decode([0, 1]) -> true. + +opt_type(extauth_instances) -> + fun (V) when is_integer(V), V > 0 -> V end; +opt_type(_) -> [extauth_instances]. diff --git a/src/gen_iq_handler.erl b/src/gen_iq_handler.erl index bbad1eca..c2b4252c 100644 --- a/src/gen_iq_handler.erl +++ b/src/gen_iq_handler.erl @@ -5,7 +5,7 @@ %%% Created : 22 Jan 2003 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -62,33 +62,37 @@ start_link(Host, Module, Function) -> add_iq_handler(Component, Host, NS, Module, Function, Type) -> case Type of - no_queue -> - Component:register_iq_handler(Host, NS, Module, - Function, no_queue); - one_queue -> - {ok, Pid} = supervisor:start_child(ejabberd_iq_sup, - [Host, Module, Function]), - Component:register_iq_handler(Host, NS, Module, - Function, {one_queue, Pid}); - N when is_integer(N) -> - Pids = lists:map(fun (_) -> - {ok, Pid} = - supervisor:start_child(ejabberd_iq_sup, - [Host, Module, - Function]), - Pid - end, - lists:seq(1, N)), - Component:register_iq_handler(Host, NS, Module, - Function, {queues, Pids}); - parallel -> - Component:register_iq_handler(Host, NS, Module, - Function, parallel) + no_queue -> + Component:register_iq_handler(Host, NS, Module, + Function, no_queue); + one_queue -> + {ok, Pid} = supervisor:start_child(ejabberd_iq_sup, + [Host, Module, Function]), + Component:register_iq_handler(Host, NS, Module, + Function, {one_queue, Pid}); + N when is_integer(N) -> + Pids = lists:map(fun (_) -> + {ok, Pid} = + supervisor:start_child(ejabberd_iq_sup, + [Host, Module, + Function]), + Pid + end, + lists:seq(1, N)), + Component:register_iq_handler(Host, NS, Module, + Function, {queues, Pids}); + parallel -> + Component:register_iq_handler(Host, NS, Module, + Function, parallel) end. +-spec remove_iq_handler(component(), binary(), binary()) -> any(). + remove_iq_handler(Component, Host, NS) -> Component:unregister_iq_handler(Host, NS). +-spec stop_iq_handler(atom(), atom(), [pid()]) -> any(). + stop_iq_handler(_Module, _Function, Opts) -> case Opts of {one_queue, Pid} -> gen_server:call(Pid, stop); @@ -100,21 +104,26 @@ stop_iq_handler(_Module, _Function, Opts) -> _ -> ok end. +-spec handle(binary(), atom(), atom(), opts(), jid(), jid(), iq()) -> any(). + handle(Host, Module, Function, Opts, From, To, IQ) -> case Opts of - no_queue -> - process_iq(Host, Module, Function, From, To, IQ); - {one_queue, Pid} -> Pid ! {process_iq, From, To, IQ}; - {queues, Pids} -> - Pid = lists:nth(erlang:phash(now(), length(Pids)), - Pids), - Pid ! {process_iq, From, To, IQ}; - parallel -> - spawn(?MODULE, process_iq, + no_queue -> + process_iq(Host, Module, Function, From, To, IQ); + {one_queue, Pid} -> + Pid ! {process_iq, From, To, IQ}; + {queues, Pids} -> + Pid = lists:nth(erlang:phash(p1_time_compat:unique_integer(), + length(Pids)), Pids), + Pid ! {process_iq, From, To, IQ}; + parallel -> + spawn(?MODULE, process_iq, [Host, Module, Function, From, To, IQ]); - _ -> todo + _ -> todo end. +-spec process_iq(binary(), atom(), atom(), jid(), jid(), iq()) -> any(). + process_iq(_Host, Module, Function, From, To, IQ) -> case catch Module:Function(From, To, IQ) of {'EXIT', Reason} -> ?ERROR_MSG("~p", [Reason]); @@ -146,44 +155,16 @@ transform_module_options(Opts) -> %% gen_server callbacks %%==================================================================== -%%-------------------------------------------------------------------- -%% Function: init(Args) -> {ok, State} | -%% {ok, State, Timeout} | -%% ignore | -%% {stop, Reason} -%% Description: Initiates the server -%%-------------------------------------------------------------------- init([Host, Module, Function]) -> {ok, #state{host = Host, module = Module, function = Function}}. -%%-------------------------------------------------------------------- -%% 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) -> Reply = ok, {stop, normal, Reply, 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({process_iq, From, To, IQ}, #state{host = Host, module = Module, function = Function} = @@ -192,22 +173,10 @@ handle_info({process_iq, From, To, IQ}, {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) -> 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 %%-------------------------------------------------------------------- - diff --git a/src/gen_mod.erl b/src/gen_mod.erl index b8e155a0..c45642d4 100644 --- a/src/gen_mod.erl +++ b/src/gen_mod.erl @@ -1,12 +1,11 @@ %%%---------------------------------------------------------------------- %%% File : gen_mod.erl %%% Author : Alexey Shchepin <alexey@process-one.net> -%%% Purpose : %%% Purpose : %%% Created : 24 Jan 2003 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -26,14 +25,17 @@ -module(gen_mod). +-behaviour(ejabberd_config). + -author('alexey@process-one.net'). --export([start/0, start_module/2, start_module/3, stop_module/2, - stop_module_keep_config/2, get_opt/3, get_opt/4, - get_opt_host/3, db_type/1, db_type/2, get_module_opt/5, - get_module_opt_host/3, loaded_modules/1, - loaded_modules_with_opts/1, get_hosts/2, - get_module_proc/2, is_loaded/2, default_db/1]). +-export([start/0, start_module/2, start_module/3, + stop_module/2, stop_module_keep_config/2, get_opt/3, + get_opt/4, get_opt_host/3, db_type/1, db_type/2, + get_module_opt/4, get_module_opt/5, get_module_opt_host/3, + loaded_modules/1, loaded_modules_with_opts/1, + get_hosts/2, get_module_proc/2, is_loaded/2, + start_modules/1, default_db/1, v_db/1, opt_type/1]). %%-export([behaviour_info/1]). @@ -45,11 +47,13 @@ opts = [] :: opts() | '_' | '$2'}). -type opts() :: [{atom(), any()}]. +-type db_type() :: odbc | mnesia | riak. -callback start(binary(), opts()) -> any(). -callback stop(binary()) -> any(). -export_type([opts/0]). +-export_type([db_type/0]). %%behaviour_info(callbacks) -> [{start, 2}, {stop, 1}]; %%behaviour_info(_Other) -> undefined. @@ -60,6 +64,17 @@ start() -> {keypos, #ejabberd_module.module_host}]), ok. +-spec start_modules(binary()) -> any(). + +start_modules(Host) -> + Modules = ejabberd_config:get_option( + {modules, Host}, + fun(L) when is_list(L) -> L end, []), + lists:foreach( + fun({Module, Opts}) -> + start_module(Host, Module, Opts) + end, Modules). + -spec start_module(binary(), atom()) -> any(). start_module(Host, Module) -> @@ -75,7 +90,8 @@ start_module(Host, Module) -> -spec start_module(binary(), atom(), opts()) -> any(). -start_module(Host, Module, Opts) -> +start_module(Host, Module, Opts0) -> + Opts = validate_opts(Module, Opts0), ets:insert(ejabberd_modules, #ejabberd_module{module_host = {Module, Host}, opts = Opts}), @@ -107,18 +123,12 @@ is_app_running(AppName) -> -spec stop_module(binary(), atom()) -> error | {aborted, any()} | {atomic, any()}. -%% @doc Stop the module in a host, and forget its configuration. stop_module(Host, Module) -> case stop_module_keep_config(Host, Module) of error -> error; ok -> ok end. -%% @doc Stop the module in a host, but keep its configuration. -%% As the module configuration is kept in the Mnesia local_config table, -%% when ejabberd is restarted the module will be started again. -%% This function is useful when ejabberd is being stopped -%% and it stops all modules. -spec stop_module_keep_config(binary(), atom()) -> error | ok. stop_module_keep_config(Host, Module) -> @@ -155,13 +165,20 @@ wait_for_stop1(MonitorReference) -> -type check_fun() :: fun((any()) -> any()) | {module(), atom()}. --spec get_opt(atom(), opts(), check_fun()) -> any(). +-spec get_opt(atom() | {atom(), binary()|global}, opts(), check_fun()) -> any(). get_opt(Opt, Opts, F) -> get_opt(Opt, Opts, F, undefined). --spec get_opt(atom(), opts(), check_fun(), any()) -> any(). +-spec get_opt(atom() | {atom(), binary()|global}, opts(), check_fun(), any()) -> any(). +get_opt({Opt, Host}, Opts, F, Default) -> + case lists:keysearch(Opt, 1, Opts) of + false -> + ejabberd_config:get_option({Opt, Host}, F, Default); + {value, {_, Val}} -> + ejabberd_config:prepare_opt_val(Opt, Val, F, Default) + end; get_opt(Opt, Opts, F, Default) -> case lists:keysearch(Opt, 1, Opts) of false -> @@ -170,6 +187,11 @@ get_opt(Opt, Opts, F, Default) -> ejabberd_config:prepare_opt_val(Opt, Val, F, Default) end. +-spec get_module_opt(global | binary(), atom(), atom(), check_fun()) -> any(). + +get_module_opt(Host, Module, Opt, F) -> + get_module_opt(Host, Module, Opt, F, undefined). + -spec get_module_opt(global | binary(), atom(), atom(), check_fun(), any()) -> any(). get_module_opt(global, Module, Opt, F, Default) -> @@ -209,26 +231,57 @@ get_opt_host(Host, Opts, Default) -> Val = get_opt(host, Opts, fun iolist_to_binary/1, Default), ejabberd_regexp:greplace(Val, <<"@HOST@">>, Host). --spec v_db(odbc | mnesia | riak | internal) -> odbc | mnesia | riak. +validate_opts(Module, Opts) -> + lists:filter( + fun({Opt, Val}) -> + case catch Module:mod_opt_type(Opt) of + VFun when is_function(VFun) -> + case catch VFun(Val) of + {'EXIT', _} -> + ?ERROR_MSG("ignoring invalid value '~p' for " + "option '~s' of module '~s'", + [Val, Opt, Module]), + false; + _ -> + true + end; + L when is_list(L) -> + SOpts = str:join([[$', atom_to_list(A), $'] || A <- L], <<", ">>), + ?ERROR_MSG("unknown option '~s' for module '~s' will be" + " likely ignored, available options are: ~s", + [Opt, Module, SOpts]), + true; + {'EXIT', {undef, _}} -> + ?WARNING_MSG("module '~s' doesn't export mod_opt_type/1", + [Module]), + true + end; + (Junk) -> + ?ERROR_MSG("failed to understand option ~p for module '~s'", + [Junk, Module]), + false + end, Opts). + +-spec v_db(db_type() | internal) -> db_type(). v_db(odbc) -> odbc; v_db(internal) -> mnesia; v_db(mnesia) -> mnesia; v_db(riak) -> riak. --spec db_type(opts()) -> odbc | mnesia | riak. +-spec db_type(opts()) -> db_type(). db_type(Opts) -> db_type(global, Opts). --spec db_type(binary() | global, atom() | opts()) -> odbc | mnesia | riak. +-spec db_type(binary() | global, atom() | opts()) -> db_type(). db_type(Host, Module) when is_atom(Module) -> get_module_opt(Host, Module, db_type, fun v_db/1, default_db(Host)); db_type(Host, Opts) when is_list(Opts) -> get_opt(db_type, Opts, fun v_db/1, default_db(Host)). --spec default_db(binary() | global) -> odbc | mnesia | riak. +-spec default_db(binary() | global) -> db_type(). default_db(Host) -> ejabberd_config:get_option({default_db, Host}, fun v_db/1, mnesia). @@ -278,3 +331,7 @@ get_module_proc(Host, Base) -> is_loaded(Host, Module) -> ets:member(ejabberd_modules, {Module, Host}). + +opt_type(default_db) -> fun v_db/1; +opt_type(modules) -> fun (L) when is_list(L) -> L end; +opt_type(_) -> [default_db, modules]. diff --git a/src/gen_pubsub_node.erl b/src/gen_pubsub_node.erl index c608dad8..0148da2e 100644 --- a/src/gen_pubsub_node.erl +++ b/src/gen_pubsub_node.erl @@ -1,32 +1,27 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. +%%%---------------------------------------------------------------------- +%%% File : gen_pubsub_node.erl +%%% Author : Christophe Romain <christophe.romain@process-one.net> +%%% Purpose : Define pubsub plugin behaviour +%%% Created : 1 Dec 2007 by Christophe Romain <christophe.romain@process-one.net> %%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. %%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. +%%% ejabberd, Copyright (C) 2002-2016 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. %%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Christophe Romain <christophe.romain@process-one.net> -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== - -%%% @private -%%% @doc <p>The module <strong>{@module}</strong> defines the PubSub node -%%% plugin behaviour. This behaviour is used to check that a PubSub plugin -%%% respects the current ejabberd PubSub plugin API.</p> +%%% 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(gen_pubsub_node). diff --git a/src/gen_pubsub_nodetree.erl b/src/gen_pubsub_nodetree.erl index ce6750db..73583af0 100644 --- a/src/gen_pubsub_nodetree.erl +++ b/src/gen_pubsub_nodetree.erl @@ -1,32 +1,27 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. +%%%---------------------------------------------------------------------- +%%% File : gen_pubsub_nodetree.erl +%%% Author : Christophe Romain <christophe.romain@process-one.net> +%%% Purpose : Define the pubsub node tree plugin behaviour +%%% Created : 1 Dec 2007 by Christophe Romain <christophe.romain@process-one.net> %%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. %%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. +%%% ejabberd, Copyright (C) 2002-2016 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. %%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Christophe Romain <christophe.romain@process-one.net> -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== - -%%% @private -%%% @doc <p>The module <strong>{@module}</strong> defines the PubSub node -%%% tree plugin behaviour. This behaviour is used to check that a PubSub -%%% node tree plugin respects the current ejabberd PubSub plugin API.</p> +%%% 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(gen_pubsub_nodetree). diff --git a/src/jd2ejd.erl b/src/jd2ejd.erl index bfa52bb2..099387c9 100644 --- a/src/jd2ejd.erl +++ b/src/jd2ejd.erl @@ -5,7 +5,7 @@ %%% Created : 2 Feb 2003 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -42,13 +42,13 @@ import_file(File) -> User = filename:rootname(filename:basename(File)), Server = filename:basename(filename:dirname(File)), - case jlib:nodeprep(User) /= error andalso - jlib:nameprep(Server) /= error + case jid:nodeprep(User) /= error andalso + jid:nameprep(Server) /= error of true -> case file:read_file(File) of {ok, Text} -> - case xml_stream:parse_element(Text) of + case fxml_stream:parse_element(Text) of El when is_record(El, xmlel) -> case catch process_xdb(User, Server, El) of {'EXIT', Reason} -> @@ -112,34 +112,34 @@ process_xdb(User, Server, xdb_data(_User, _Server, {xmlcdata, _CData}) -> ok; xdb_data(User, Server, #xmlel{attrs = Attrs} = El) -> - From = jlib:make_jid(User, Server, <<"">>), - case xml:get_attr_s(<<"xmlns">>, Attrs) of + From = jid:make(User, Server, <<"">>), + case fxml:get_attr_s(<<"xmlns">>, Attrs) of ?NS_AUTH -> - Password = xml:get_tag_cdata(El), + Password = fxml:get_tag_cdata(El), ejabberd_auth:set_password(User, Server, Password), ok; ?NS_ROSTER -> catch mod_roster:set_items(User, Server, El), ok; ?NS_LAST -> - TimeStamp = xml:get_attr_s(<<"last">>, Attrs), - Status = xml:get_tag_cdata(El), + TimeStamp = fxml:get_attr_s(<<"last">>, Attrs), + Status = fxml:get_tag_cdata(El), catch mod_last:store_last_info(User, Server, jlib:binary_to_integer(TimeStamp), Status), ok; ?NS_VCARD -> catch mod_vcard:process_sm_iq(From, - jlib:make_jid(<<"">>, Server, <<"">>), + jid:make(<<"">>, Server, <<"">>), #iq{type = set, xmlns = ?NS_VCARD, sub_el = El}), ok; <<"jabber:x:offline">> -> process_offline(Server, From, El), ok; XMLNS -> - case xml:get_attr_s(<<"j_private_flag">>, Attrs) of + case fxml:get_attr_s(<<"j_private_flag">>, Attrs) of <<"1">> -> catch mod_private:process_sm_iq(From, - jlib:make_jid(<<"">>, Server, + jid:make(<<"">>, Server, <<"">>), #iq{type = set, xmlns = ?NS_PRIVATE, @@ -158,13 +158,13 @@ xdb_data(User, Server, #xmlel{attrs = Attrs} = El) -> end. process_offline(Server, To, #xmlel{children = Els}) -> - LServer = jlib:nameprep(Server), + LServer = jid:nameprep(Server), lists:foreach(fun (#xmlel{attrs = Attrs} = El) -> - FromS = xml:get_attr_s(<<"from">>, Attrs), + FromS = fxml:get_attr_s(<<"from">>, Attrs), From = case FromS of <<"">> -> - jlib:make_jid(<<"">>, Server, <<"">>); - _ -> jlib:string_to_jid(FromS) + jid:make(<<"">>, Server, <<"">>); + _ -> jid:from_string(FromS) end, case From of error -> ok; diff --git a/src/jid.erl b/src/jid.erl new file mode 100644 index 00000000..7bdd652a --- /dev/null +++ b/src/jid.erl @@ -0,0 +1,232 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @doc +%%% JID processing library +%%% @end +%%% Created : 24 Nov 2015 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2016 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(jid). + +%% API +-export([start/0, + make/1, + make/3, + split/1, + from_string/1, + to_string/1, + is_nodename/1, + nodeprep/1, + nameprep/1, + resourceprep/1, + tolower/1, + remove_resource/1, + replace_resource/2]). + +-include("jlib.hrl"). + +-export_type([jid/0]). + +%%%=================================================================== +%%% API +%%%=================================================================== +-spec start() -> ok. + +start() -> + SplitPattern = binary:compile_pattern([<<"@">>, <<"/">>]), + catch ets:new(jlib, [named_table, protected, set, {keypos, 1}]), + ets:insert(jlib, {string_to_jid_pattern, SplitPattern}), + ok. + +-spec make(binary(), binary(), binary()) -> jid() | error. + +make(User, Server, Resource) -> + case nodeprep(User) of + error -> error; + LUser -> + case nameprep(Server) of + error -> error; + LServer -> + case resourceprep(Resource) of + error -> error; + LResource -> + #jid{user = User, server = Server, resource = Resource, + luser = LUser, lserver = LServer, + lresource = LResource} + end + end + end. + +-spec make({binary(), binary(), binary()}) -> jid() | error. + +make({User, Server, Resource}) -> + make(User, Server, Resource). + +%% This is the reverse of make_jid/1 +-spec split(jid()) -> {binary(), binary(), binary()} | error. + +split(#jid{user = U, server = S, resource = R}) -> + {U, S, R}; +split(_) -> + error. + +-spec from_string([binary()|string()]) -> jid() | error. +from_string(S) when is_list(S) -> + %% We do not accept list because we want to enforce good practice of + %% using binaries for string. However, we do not let it crash to avoid + %% losing associated ets table. + {error, need_jid_as_binary}; +from_string(S) when is_binary(S) -> + SplitPattern = ets:lookup_element(jlib, string_to_jid_pattern, 2), + Size = size(S), + End = Size-1, + case binary:match(S, SplitPattern) of + {0, _} -> + error; + {End, _} -> + error; + {Pos1, _} -> + case binary:at(S, Pos1) of + $/ -> + make(<<>>, + binary:part(S, 0, Pos1), + binary:part(S, Pos1+1, Size-Pos1-1)); + _ -> + Pos1N = Pos1+1, + case binary:match(S, SplitPattern, [{scope, {Pos1+1, Size-Pos1-1}}]) of + {End, _} -> + error; + {Pos1N, _} -> + error; + {Pos2, _} -> + case binary:at(S, Pos2) of + $/ -> + make(binary:part(S, 0, Pos1), + binary:part(S, Pos1+1, Pos2-Pos1-1), + binary:part(S, Pos2+1, Size-Pos2-1)); + _ -> error + end; + _ -> + make(binary:part(S, 0, Pos1), + binary:part(S, Pos1+1, Size-Pos1-1), + <<>>) + end + end; + _ -> + make(<<>>, S, <<>>) + end. + +-spec to_string(jid() | ljid()) -> binary(). + +to_string(#jid{user = User, server = Server, + resource = Resource}) -> + to_string({User, Server, Resource}); +to_string({N, S, R}) -> + Node = iolist_to_binary(N), + Server = iolist_to_binary(S), + Resource = iolist_to_binary(R), + S1 = case Node of + <<"">> -> <<"">>; + _ -> <<Node/binary, "@">> + end, + S2 = <<S1/binary, Server/binary>>, + S3 = case Resource of + <<"">> -> S2; + _ -> <<S2/binary, "/", Resource/binary>> + end, + S3. + +-spec is_nodename(binary()) -> boolean(). + +is_nodename(Node) -> + N = nodeprep(Node), + (N /= error) and (N /= <<>>). + +-define(LOWER(Char), + if Char >= $A, Char =< $Z -> Char + 32; + true -> Char + end). + +-spec nodeprep(binary()) -> binary() | error. + +nodeprep("") -> <<>>; +nodeprep(S) when byte_size(S) < 1024 -> + R = stringprep:nodeprep(S), + if byte_size(R) < 1024 -> R; + true -> error + end; +nodeprep(_) -> error. + +-spec nameprep(binary()) -> binary() | error. + +nameprep(S) when byte_size(S) < 1024 -> + R = stringprep:nameprep(S), + if byte_size(R) < 1024 -> R; + true -> error + end; +nameprep(_) -> error. + +-spec resourceprep(binary()) -> binary() | error. + +resourceprep(S) when byte_size(S) < 1024 -> + R = stringprep:resourceprep(S), + if byte_size(R) < 1024 -> R; + true -> error + end; +resourceprep(_) -> error. + +-spec tolower(jid() | ljid()) -> error | ljid(). + +tolower(#jid{luser = U, lserver = S, + lresource = R}) -> + {U, S, R}; +tolower({U, S, R}) -> + case nodeprep(U) of + error -> error; + LUser -> + case nameprep(S) of + error -> error; + LServer -> + case resourceprep(R) of + error -> error; + LResource -> {LUser, LServer, LResource} + end + end + end. + +-spec remove_resource(jid()) -> jid(); + (ljid()) -> ljid(). + +remove_resource(#jid{} = JID) -> + JID#jid{resource = <<"">>, lresource = <<"">>}; +remove_resource({U, S, _R}) -> {U, S, <<"">>}. + +-spec replace_resource(jid(), binary()) -> error | jid(). + +replace_resource(JID, Resource) -> + case resourceprep(Resource) of + error -> error; + LResource -> + JID#jid{resource = Resource, lresource = LResource} + end. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== diff --git a/src/jlib.erl b/src/jlib.erl index 76886a7d..8eaebbc8 100644 --- a/src/jlib.erl +++ b/src/jlib.erl @@ -5,7 +5,7 @@ %%% Created : 23 Nov 2002 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -27,6 +27,10 @@ -author('alexey@process-one.net'). +-protocol({xep, 59, '1.0'}). +-protocol({xep, 82, '1.1'}). +-protocol({xep, 203, '2.0'}). + -compile({no_auto_import, [atom_to_binary/2, binary_to_integer/1, integer_to_binary/1]}). @@ -35,15 +39,13 @@ make_error_reply/2, make_error_element/2, make_correct_from_to_attrs/3, replace_from_to_attrs/3, replace_from_to/3, replace_from_attrs/2, replace_from/2, - remove_attr/2, make_jid/3, make_jid/1, string_to_jid/1, - jid_to_string/1, is_nodename/1, tolower/1, nodeprep/1, - nameprep/1, resourceprep/1, jid_tolower/1, - jid_remove_resource/1, jid_replace_resource/2, + remove_attr/2, tolower/1, get_iq_namespace/1, iq_query_info/1, iq_query_or_response_info/1, is_iq_request_type/1, iq_to_xml/1, parse_xdata_submit/1, + is_standalone_chat_state/1, add_delay_info/3, add_delay_info/4, - timestamp_to_iso/1, timestamp_to_iso/2, + timestamp_to_legacy/1, timestamp_to_iso_basic/1, timestamp_to_iso/2, now_to_utc_string/1, now_to_local_string/1, datetime_string_to_timestamp/1, term_to_base64/1, base64_to_term/1, @@ -54,14 +56,28 @@ atom_to_binary/1, binary_to_atom/1, tuple_to_binary/1, l2i/1, i2l/1, i2l/2, queue_drop_while/2]). -%% TODO: Remove once XEP-0091 is Obsolete -%% TODO: Remove once XEP-0091 is Obsolete +%% The following functions are deprecated and will be removed soon +%% Use corresponding functions from jid.erl instead +-export([make_jid/3, make_jid/1, split_jid/1, string_to_jid/1, + jid_to_string/1, is_nodename/1, nodeprep/1, + nameprep/1, resourceprep/1, jid_tolower/1, + jid_remove_resource/1, jid_replace_resource/2]). + +-deprecated([{make_jid, '_'}, + {split_jid, 1}, + {string_to_jid, 1}, + {jid_to_string, 1}, + {is_nodename, 1}, + {nodeprep, 1}, + {nameprep, 1}, + {resourceprep, 1}, + {jid_tolower, 1}, + {jid_remove_resource, 1}, + {jid_replace_resource, 2}]). -include("ejabberd.hrl"). -include("jlib.hrl"). --export_type([jid/0]). - %send_iq(From, To, ID, SubTags) -> % ok. @@ -76,8 +92,8 @@ make_result_iq_reply(#xmlel{name = Name, attrs = Attrs, -spec make_result_iq_reply_attrs([attr()]) -> [attr()]. make_result_iq_reply_attrs(Attrs) -> - To = xml:get_attr(<<"to">>, Attrs), - From = xml:get_attr(<<"from">>, Attrs), + To = fxml:get_attr(<<"to">>, Attrs), + From = fxml:get_attr(<<"from">>, Attrs), Attrs1 = lists:keydelete(<<"to">>, 1, Attrs), Attrs2 = lists:keydelete(<<"from">>, 1, Attrs1), Attrs3 = case To of @@ -117,8 +133,8 @@ make_error_reply(#xmlel{name = Name, attrs = Attrs, -spec make_error_reply_attrs([attr()]) -> [attr()]. make_error_reply_attrs(Attrs) -> - To = xml:get_attr(<<"to">>, Attrs), - From = xml:get_attr(<<"from">>, Attrs), + To = fxml:get_attr(<<"to">>, Attrs), + From = fxml:get_attr(<<"from">>, Attrs), Attrs1 = lists:keydelete(<<"to">>, 1, Attrs), Attrs2 = lists:keydelete(<<"from">>, 1, Attrs1), Attrs3 = case To of @@ -143,7 +159,7 @@ make_error_element(Code, Desc) -> make_correct_from_to_attrs(From, To, Attrs) -> Attrs1 = lists:keydelete(<<"from">>, 1, Attrs), - Attrs2 = case xml:get_attr(<<"to">>, Attrs) of + Attrs2 = case fxml:get_attr(<<"to">>, Attrs) of {value, _} -> Attrs1; _ -> [{<<"to">>, To} | Attrs1] end, @@ -164,8 +180,8 @@ replace_from_to_attrs(From, To, Attrs) -> replace_from_to(From, To, #xmlel{name = Name, attrs = Attrs, children = Els}) -> NewAttrs = - replace_from_to_attrs(jlib:jid_to_string(From), - jlib:jid_to_string(To), Attrs), + replace_from_to_attrs(jid:to_string(From), + jid:to_string(To), Attrs), #xmlel{name = Name, attrs = NewAttrs, children = Els}. -spec replace_from_attrs(binary(), [attr()]) -> [attr()]. @@ -178,7 +194,7 @@ replace_from_attrs(From, Attrs) -> replace_from(From, #xmlel{name = Name, attrs = Attrs, children = Els}) -> - NewAttrs = replace_from_attrs(jlib:jid_to_string(From), + NewAttrs = replace_from_attrs(jid:to_string(From), Attrs), #xmlel{name = Name, attrs = NewAttrs, children = Els}. @@ -192,86 +208,32 @@ remove_attr(Attr, -spec make_jid(binary(), binary(), binary()) -> jid() | error. make_jid(User, Server, Resource) -> - case nodeprep(User) of - error -> error; - LUser -> - case nameprep(Server) of - error -> error; - LServer -> - case resourceprep(Resource) of - error -> error; - LResource -> - #jid{user = User, server = Server, resource = Resource, - luser = LUser, lserver = LServer, - lresource = LResource} - end - end - end. + jid:make(User, Server, Resource). -spec make_jid({binary(), binary(), binary()}) -> jid() | error. make_jid({User, Server, Resource}) -> - make_jid(User, Server, Resource). + jid:make({User, Server, Resource}). + +%% This is the reverse of make_jid/1 +-spec split_jid(jid()) -> {binary(), binary(), binary()} | error. +split_jid(J) -> + jid:split(J). -spec string_to_jid(binary()) -> jid() | error. string_to_jid(S) -> - string_to_jid1(binary_to_list(S), ""). - -string_to_jid1([$@ | _J], "") -> error; -string_to_jid1([$@ | J], N) -> - string_to_jid2(J, lists:reverse(N), ""); -string_to_jid1([$/ | _J], "") -> error; -string_to_jid1([$/ | J], N) -> - string_to_jid3(J, "", lists:reverse(N), ""); -string_to_jid1([C | J], N) -> - string_to_jid1(J, [C | N]); -string_to_jid1([], "") -> error; -string_to_jid1([], N) -> - make_jid(<<"">>, list_to_binary(lists:reverse(N)), <<"">>). - -%% Only one "@" is admitted per JID -string_to_jid2([$@ | _J], _N, _S) -> error; -string_to_jid2([$/ | _J], _N, "") -> error; -string_to_jid2([$/ | J], N, S) -> - string_to_jid3(J, N, lists:reverse(S), ""); -string_to_jid2([C | J], N, S) -> - string_to_jid2(J, N, [C | S]); -string_to_jid2([], _N, "") -> error; -string_to_jid2([], N, S) -> - make_jid(list_to_binary(N), list_to_binary(lists:reverse(S)), <<"">>). - -string_to_jid3([C | J], N, S, R) -> - string_to_jid3(J, N, S, [C | R]); -string_to_jid3([], N, S, R) -> - make_jid(list_to_binary(N), list_to_binary(S), - list_to_binary(lists:reverse(R))). + jid:from_string(S). -spec jid_to_string(jid() | ljid()) -> binary(). -jid_to_string(#jid{user = User, server = Server, - resource = Resource}) -> - jid_to_string({User, Server, Resource}); -jid_to_string({N, S, R}) -> - Node = iolist_to_binary(N), - Server = iolist_to_binary(S), - Resource = iolist_to_binary(R), - S1 = case Node of - <<"">> -> <<"">>; - _ -> <<Node/binary, "@">> - end, - S2 = <<S1/binary, Server/binary>>, - S3 = case Resource of - <<"">> -> S2; - _ -> <<S2/binary, "/", Resource/binary>> - end, - S3. +jid_to_string(J) -> + jid:to_string(J). -spec is_nodename(binary()) -> boolean(). is_nodename(Node) -> - N = nodeprep(Node), - (N /= error) and (N /= <<>>). + jid:is_nodename(Node). %tolower_c(C) when C >= $A, C =< $Z -> % C + 32; @@ -309,72 +271,36 @@ tolower_s([]) -> []. -spec nodeprep(binary()) -> binary() | error. -nodeprep("") -> <<>>; -nodeprep(S) when byte_size(S) < 1024 -> - R = stringprep:nodeprep(S), - if byte_size(R) < 1024 -> R; - true -> error - end; -nodeprep(_) -> error. +nodeprep(S) -> jid:nodeprep(S). -spec nameprep(binary()) -> binary() | error. -nameprep(S) when byte_size(S) < 1024 -> - R = stringprep:nameprep(S), - if byte_size(R) < 1024 -> R; - true -> error - end; -nameprep(_) -> error. +nameprep(S) -> jid:nameprep(S). -spec resourceprep(binary()) -> binary() | error. -resourceprep(S) when byte_size(S) < 1024 -> - R = stringprep:resourceprep(S), - if byte_size(R) < 1024 -> R; - true -> error - end; -resourceprep(_) -> error. +resourceprep(S) -> jid:resourceprep(S). -spec jid_tolower(jid() | ljid()) -> error | ljid(). -jid_tolower(#jid{luser = U, lserver = S, - lresource = R}) -> - {U, S, R}; -jid_tolower({U, S, R}) -> - case nodeprep(U) of - error -> error; - LUser -> - case nameprep(S) of - error -> error; - LServer -> - case resourceprep(R) of - error -> error; - LResource -> {LUser, LServer, LResource} - end - end - end. +jid_tolower(J) -> + jid:tolower(J). -spec jid_remove_resource(jid()) -> jid(); (ljid()) -> ljid(). -jid_remove_resource(#jid{} = JID) -> - JID#jid{resource = <<"">>, lresource = <<"">>}; -jid_remove_resource({U, S, _R}) -> {U, S, <<"">>}. +jid_remove_resource(J) -> jid:remove_resource(J). -spec jid_replace_resource(jid(), binary()) -> error | jid(). jid_replace_resource(JID, Resource) -> - case resourceprep(Resource) of - error -> error; - LResource -> - JID#jid{resource = Resource, lresource = LResource} - end. + jid:replace_resource(JID, Resource). -spec get_iq_namespace(xmlel()) -> binary(). get_iq_namespace(#xmlel{name = <<"iq">>, children = Els}) -> - case xml:remove_cdata(Els) of - [#xmlel{attrs = Attrs}] -> xml:get_attr_s(<<"xmlns">>, Attrs); + case fxml:remove_cdata(Els) of + [#xmlel{attrs = Attrs}] -> fxml:get_attr_s(<<"xmlns">>, Attrs); _ -> <<"">> end; get_iq_namespace(_) -> <<"">>. @@ -400,9 +326,9 @@ iq_query_or_response_info(El) -> iq_info_internal(El, any). iq_info_internal(#xmlel{name = <<"iq">>, attrs = Attrs, children = Els}, Filter) -> - ID = xml:get_attr_s(<<"id">>, Attrs), - Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), - {Type, Class} = case xml:get_attr_s(<<"type">>, Attrs) of + ID = fxml:get_attr_s(<<"id">>, Attrs), + Lang = fxml:get_attr_s(<<"xml:lang">>, Attrs), + {Type, Class} = case fxml:get_attr_s(<<"type">>, Attrs) of <<"set">> -> {set, request}; <<"get">> -> {get, request}; <<"result">> -> {result, reply}; @@ -410,15 +336,15 @@ iq_info_internal(#xmlel{name = <<"iq">>, attrs = Attrs, children = Els}, Filter) _ -> {invalid, invalid} end, if Type == invalid -> invalid; Class == request; Filter == any -> - FilteredEls = xml:remove_cdata(Els), + FilteredEls = fxml:remove_cdata(Els), {XMLNS, SubEl} = case {Class, FilteredEls} of {request, [#xmlel{attrs = Attrs2}]} -> - {xml:get_attr_s(<<"xmlns">>, Attrs2), hd(FilteredEls)}; + {fxml:get_attr_s(<<"xmlns">>, Attrs2), hd(FilteredEls)}; {reply, _} -> NonErrorEls = [El || #xmlel{name = SubName} = El <- FilteredEls, SubName /= <<"error">>], {case NonErrorEls of - [NonErrorEl] -> xml:get_tag_attr_s(<<"xmlns">>, NonErrorEl); + [NonErrorEl] -> fxml:get_tag_attr_s(<<"xmlns">>, NonErrorEl); _ -> <<"">> end, FilteredEls}; @@ -473,7 +399,7 @@ iq_to_xml(#iq{id = ID, type = Type, sub_el = SubEl}) -> ). parse_xdata_submit(#xmlel{attrs = Attrs, children = Els}) -> - case xml:get_attr_s(<<"type">>, Attrs) of + case fxml:get_attr_s(<<"type">>, Attrs) of <<"submit">> -> lists:reverse(parse_xdata_fields(Els, [])); <<"form">> -> %% This is a workaround to accept Psi's wrong forms @@ -492,7 +418,7 @@ parse_xdata_submit(#xmlel{attrs = Attrs, children = Els}) -> parse_xdata_fields([], Res) -> Res; parse_xdata_fields([#xmlel{name = <<"field">>, attrs = Attrs, children = SubEls} | Els], Res) -> - case xml:get_attr_s(<<"var">>, Attrs) of + case fxml:get_attr_s(<<"var">>, Attrs) of <<>> -> parse_xdata_fields(Els, Res); Var -> @@ -511,7 +437,7 @@ parse_xdata_fields([_ | Els], Res) -> parse_xdata_values([], Res) -> Res; parse_xdata_values([#xmlel{name = <<"value">>, children = SubEls} | Els], Res) -> - Val = xml:get_cdata(SubEls), + Val = fxml:get_cdata(SubEls), parse_xdata_values(Els, [Val | Res]); parse_xdata_values([_ | Els], Res) -> parse_xdata_values(Els, Res). @@ -520,7 +446,7 @@ parse_xdata_values([_ | Els], Res) -> rsm_decode(#iq{sub_el = SubEl}) -> rsm_decode(SubEl); rsm_decode(#xmlel{} = SubEl) -> - case xml:get_subtag(SubEl, <<"set">>) of + case fxml:get_subtag(SubEl, <<"set">>) of false -> none; #xmlel{name = <<"set">>, children = SubEls} -> lists:foldl(fun rsm_parse_element/2, #rsm_in{}, SubEls) @@ -529,26 +455,26 @@ rsm_decode(#xmlel{} = SubEl) -> rsm_parse_element(#xmlel{name = <<"max">>, attrs = []} = Elem, RsmIn) -> - CountStr = xml:get_tag_cdata(Elem), + CountStr = fxml:get_tag_cdata(Elem), {Count, _} = str:to_integer(CountStr), RsmIn#rsm_in{max = Count}; rsm_parse_element(#xmlel{name = <<"before">>, attrs = []} = Elem, RsmIn) -> - UID = xml:get_tag_cdata(Elem), + UID = fxml:get_tag_cdata(Elem), RsmIn#rsm_in{direction = before, id = UID}; rsm_parse_element(#xmlel{name = <<"after">>, attrs = []} = Elem, RsmIn) -> - UID = xml:get_tag_cdata(Elem), + UID = fxml:get_tag_cdata(Elem), RsmIn#rsm_in{direction = aft, id = UID}; rsm_parse_element(#xmlel{name = <<"index">>, attrs = []} = Elem, RsmIn) -> - IndexStr = xml:get_tag_cdata(Elem), + IndexStr = fxml:get_tag_cdata(Elem), {Index, _} = str:to_integer(IndexStr), RsmIn#rsm_in{index = Index}; rsm_parse_element(_, RsmIn) -> RsmIn. @@ -602,6 +528,26 @@ rsm_encode_count(Count, Arr) -> children = [{xmlcdata, i2l(Count)}]} | Arr]. +-spec is_standalone_chat_state(xmlel()) -> boolean(). + +is_standalone_chat_state(#xmlel{name = <<"message">>} = El) -> + ChatStates = [<<"active">>, <<"inactive">>, <<"gone">>, <<"composing">>, + <<"paused">>], + Stripped = + lists:foldl(fun(ChatState, AccEl) -> + fxml:remove_subtags(AccEl, ChatState, + {<<"xmlns">>, ?NS_CHATSTATES}) + end, El, ChatStates), + case Stripped of + #xmlel{children = [#xmlel{name = <<"thread">>}]} -> + true; + #xmlel{children = []} -> + true; + _ -> + false + end; +is_standalone_chat_state(_El) -> false. + -spec add_delay_info(xmlel(), jid() | ljid() | binary(), erlang:timestamp()) -> xmlel(). @@ -612,24 +558,15 @@ add_delay_info(El, From, Time) -> binary()) -> xmlel(). add_delay_info(El, From, Time, Desc) -> - %% TODO: Remove support for <x/>, XEP-0091 is obsolete. - El1 = add_delay_info(El, From, Time, Desc, <<"delay">>, ?NS_DELAY), - El2 = add_delay_info(El1, From, Time, Desc, <<"x">>, ?NS_DELAY91), - El2. - --spec add_delay_info(xmlel(), jid() | ljid() | binary(), erlang:timestamp(), - binary(), binary(), binary()) -> xmlel(). - -add_delay_info(El, From, Time, Desc, Name, XMLNS) -> - case xml:get_subtag_with_xmlns(El, Name, XMLNS) of + case fxml:get_subtag_with_xmlns(El, <<"delay">>, ?NS_DELAY) of false -> %% Add new tag - DelayTag = create_delay_tag(Time, From, Desc, XMLNS), - xml:append_subtags(El, [DelayTag]); + DelayTag = create_delay_tag(Time, From, Desc), + fxml:append_subtags(El, [DelayTag]); DelayTag -> %% Update existing tag NewDelayTag = - case {xml:get_tag_cdata(DelayTag), Desc} of + case {fxml:get_tag_cdata(DelayTag), Desc} of {<<"">>, <<"">>} -> DelayTag; {OldDesc, <<"">>} -> @@ -645,34 +582,28 @@ add_delay_info(El, From, Time, Desc, Name, XMLNS) -> DelayTag#xmlel{children = [{xmlcdata, OldDesc}]} end end, - NewEl = xml:remove_subtags(El, Name, {<<"xmlns">>, XMLNS}), - xml:append_subtags(NewEl, [NewDelayTag]) + NewEl = fxml:remove_subtags(El, <<"delay">>, {<<"xmlns">>, ?NS_DELAY}), + fxml:append_subtags(NewEl, [NewDelayTag]) end. --spec create_delay_tag(erlang:timestamp(), jid() | ljid() | binary(), binary(), - binary()) -> xmlel() | error. - -create_delay_tag(TimeStamp, FromJID, Desc, XMLNS) when is_tuple(FromJID) -> - From = jlib:jid_to_string(FromJID), - {Name, Stamp} = case XMLNS of - ?NS_DELAY -> - {<<"delay">>, now_to_utc_string(TimeStamp, 3)}; - ?NS_DELAY91 -> - DateTime = calendar:now_to_universal_time(TimeStamp), - {<<"x">>, timestamp_to_iso(DateTime)} - end, +-spec create_delay_tag(erlang:timestamp(), jid() | ljid() | binary(), binary()) + -> xmlel() | error. + +create_delay_tag(TimeStamp, FromJID, Desc) when is_tuple(FromJID) -> + From = jid:to_string(FromJID), + Stamp = now_to_utc_string(TimeStamp, 3), Children = case Desc of <<"">> -> []; _ -> [{xmlcdata, Desc}] end, - #xmlel{name = Name, + #xmlel{name = <<"delay">>, attrs = - [{<<"xmlns">>, XMLNS}, {<<"from">>, From}, + [{<<"xmlns">>, ?NS_DELAY}, {<<"from">>, From}, {<<"stamp">>, Stamp}], children = Children}; -create_delay_tag(DateTime, Host, Desc, XMLNS) when is_binary(Host) -> - FromJID = jlib:make_jid(<<"">>, Host, <<"">>), - create_delay_tag(DateTime, FromJID, Desc, XMLNS). +create_delay_tag(DateTime, Host, Desc) when is_binary(Host) -> + FromJID = jid:make(<<"">>, Host, <<"">>), + create_delay_tag(DateTime, FromJID, Desc). -type tz() :: {binary(), {integer(), integer()}} | {integer(), integer()} | utc. @@ -681,6 +612,9 @@ create_delay_tag(DateTime, Host, Desc, XMLNS) when is_binary(Host) -> %% Minutes = integer() -spec timestamp_to_iso(calendar:datetime(), tz()) -> {binary(), binary()}. +%% This is the XEP-0082 date and time format +%% http://xmpp.org/extensions/xep-0082.html + timestamp_to_iso({{Year, Month, Day}, {Hour, Minute, Second}}, Timezone) -> @@ -701,13 +635,22 @@ timestamp_to_iso({{Year, Month, Day}, end, {iolist_to_binary(Timestamp_string), iolist_to_binary(Timezone_string)}. --spec timestamp_to_iso(calendar:datetime()) -> binary(). -timestamp_to_iso({{Year, Month, Day}, +-spec timestamp_to_legacy(calendar:datetime()) -> binary(). +%% This is the jabber legacy format +%% http://xmpp.org/extensions/xep-0091.html#time +timestamp_to_legacy({{Year, Month, Day}, {Hour, Minute, Second}}) -> iolist_to_binary(io_lib:format("~4..0B~2..0B~2..0BT~2..0B:~2..0B:~2..0B", [Year, Month, Day, Hour, Minute, Second])). +-spec timestamp_to_iso_basic(calendar:datetime()) -> binary(). +%% This is the ISO 8601 basic bormat +timestamp_to_iso_basic({{Year, Month, Day}, + {Hour, Minute, Second}}) -> + iolist_to_binary(io_lib:format("~4..0B~2..0B~2..0BT~2..0B~2..0B~2..0B", + [Year, Month, Day, Hour, Minute, Second])). + -spec now_to_utc_string(erlang:timestamp()) -> binary(). now_to_utc_string({MegaSecs, Secs, MicroSecs}) -> @@ -863,7 +806,7 @@ decode_base64(S) -> decode_base64_bin(S, <<>>) end. -take_without_spaces(Bin, Count) -> +take_without_spaces(Bin, Count) -> take_without_spaces(Bin, Count, <<>>). take_without_spaces(Bin, 0, Acc) -> @@ -941,16 +884,16 @@ binary_to_atom(Bin) -> erlang:binary_to_atom(Bin, utf8). binary_to_integer(Bin) -> - list_to_integer(binary_to_list(Bin)). + erlang:binary_to_integer(Bin). binary_to_integer(Bin, Base) -> - list_to_integer(binary_to_list(Bin), Base). + erlang:binary_to_integer(Bin, Base). integer_to_binary(I) -> - list_to_binary(integer_to_list(I)). + erlang:integer_to_binary(I). integer_to_binary(I, Base) -> - list_to_binary(erlang:integer_to_list(I, Base)). + erlang:integer_to_binary(I, Base). tuple_to_binary(T) -> iolist_to_binary(tuple_to_list(T)). diff --git a/src/mod_adhoc.erl b/src/mod_adhoc.erl index ec41e73f..9947c84a 100644 --- a/src/mod_adhoc.erl +++ b/src/mod_adhoc.erl @@ -5,7 +5,7 @@ %%% Created : 15 Nov 2005 by Magnus Henoch <henoch@dtek.chalmers.se> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -27,13 +27,15 @@ -author('henoch@dtek.chalmers.se'). +-protocol({xep, 50, '1.2'}). + -behaviour(gen_mod). -export([start/2, stop/1, process_local_iq/3, process_sm_iq/3, get_local_commands/5, get_local_identity/5, get_local_features/5, get_sm_commands/5, get_sm_identity/5, get_sm_features/5, - ping_item/4, ping_command/4]). + ping_item/4, ping_command/4, mod_opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -140,7 +142,7 @@ get_sm_commands(Acc, _From, end, Nodes = [#xmlel{name = <<"item">>, attrs = - [{<<"jid">>, jlib:jid_to_string(To)}, + [{<<"jid">>, jid:to_string(To)}, {<<"node">>, ?NS_COMMANDS}, {<<"name">>, translate:translate(Lang, <<"Commands">>)}], @@ -278,3 +280,8 @@ ping_command(_Acc, _From, _To, true -> {error, ?ERR_BAD_REQUEST} end; ping_command(Acc, _From, _To, _Request) -> Acc. + +mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; +mod_opt_type(report_commands_node) -> + fun (B) when is_boolean(B) -> B end; +mod_opt_type(_) -> [iqdisc, report_commands_node]. diff --git a/src/mod_admin_extra.erl b/src/mod_admin_extra.erl index 023c29dc..f0e56719 100644 --- a/src/mod_admin_extra.erl +++ b/src/mod_admin_extra.erl @@ -30,90 +30,48 @@ -include("logger.hrl"). --export([start/2, stop/1, - %% Node - compile/1, - get_cookie/0, - remove_node/1, - export2odbc/2, - %% Accounts - set_password/3, - check_password_hash/4, - delete_old_users/1, - delete_old_users_vhost/2, - ban_account/3, - num_active_users/2, - %% Sessions - num_resources/2, - resource_num/3, - kick_session/4, - status_num/2, status_num/1, - status_list/2, status_list/1, - connected_users_info/0, - connected_users_vhost/1, - set_presence/7, - user_sessions_info/2, - %% Vcard - set_nickname/3, - get_vcard/3, - get_vcard/4, - get_vcard_multi/4, - set_vcard/4, - set_vcard/5, - %% Roster - add_rosteritem/7, - delete_rosteritem/4, - process_rosteritems/5, - get_roster/2, - push_roster/3, - push_roster_all/1, - push_alltoall/2, - %% mod_last - get_last/2, - %% mod_private - private_get/4, - private_set/3, - %% mod_shared_roster - srg_create/5, - srg_delete/2, - srg_list/1, - srg_get_info/2, - srg_get_members/2, - srg_user_add/4, - srg_user_del/4, - %% Stanza - send_message/5, - send_stanza_c2s/4, - privacy_set/3, - %% Stats - stats/1, stats/2 - ]). +-export([start/2, stop/1, compile/1, get_cookie/0, + remove_node/1, set_password/3, + check_password_hash/4, delete_old_users/1, + delete_old_users_vhost/2, ban_account/3, + num_active_users/2, num_resources/2, resource_num/3, + kick_session/4, status_num/2, status_num/1, + status_list/2, status_list/1, connected_users_info/0, + connected_users_vhost/1, set_presence/7, + user_sessions_info/2, set_nickname/3, get_vcard/3, + get_vcard/4, get_vcard_multi/4, set_vcard/4, + set_vcard/5, add_rosteritem/7, delete_rosteritem/4, + process_rosteritems/5, get_roster/2, push_roster/3, + push_roster_all/1, push_alltoall/2, get_last/2, + private_get/4, private_set/3, srg_create/5, + srg_delete/2, srg_list/1, srg_get_info/2, + srg_get_members/2, srg_user_add/4, srg_user_del/4, + send_message/5, send_stanza/3, send_stanza_c2s/4, privacy_set/3, + stats/1, stats/2, mod_opt_type/1, get_commands_spec/0]). + -include("ejabberd.hrl"). -include("ejabberd_commands.hrl"). -include("mod_roster.hrl"). +-include("ejabberd_sm.hrl"). -include("jlib.hrl"). -%% Copied from ejabberd_sm.erl --record(session, {sid, usr, us, priority, info}). - - %%% %%% gen_mod %%% start(_Host, _Opts) -> - ejabberd_commands:register_commands(commands()). + ejabberd_commands:register_commands(get_commands_spec()). stop(_Host) -> - ejabberd_commands:unregister_commands(commands()). + ejabberd_commands:unregister_commands(get_commands_spec()). %%% %%% Register commands %%% -commands() -> +get_commands_spec() -> Vcard1FieldsString = "Some vcard field names in get/set_vcard are:\n" " FN - Full Name\n" " NICKNAME - Nickname\n" @@ -143,91 +101,156 @@ commands() -> desc = "Recompile and reload Erlang source code file", module = ?MODULE, function = compile, args = [{file, string}], - result = {res, rescode}}, + args_example = ["/home/me/srcs/ejabberd/mod_example.erl"], + args_desc = ["Filename of erlang source file to compile"], + result = {res, rescode}, + result_example = ok, + result_desc = "Status code: 0 on success, 1 otherwise"}, #ejabberd_commands{name = get_cookie, tags = [erlang], desc = "Get the Erlang cookie of this node", module = ?MODULE, function = get_cookie, args = [], - result = {cookie, string}}, + result = {cookie, string}, + result_example = "MWTAVMODFELNLSMYXPPD", + result_desc = "Erlang cookie used for authentication by ejabberd"}, #ejabberd_commands{name = remove_node, tags = [erlang], desc = "Remove an ejabberd node from Mnesia clustering config", module = ?MODULE, function = remove_node, args = [{node, string}], - result = {res, rescode}}, - #ejabberd_commands{name = export2odbc, tags = [mnesia], %% Copied to ejabberd 2.1.x after 11 - desc = "Export Mnesia tables to files in directory", - module = ?MODULE, function = export2odbc, - args = [{host, string}, {path, string}], - result = {res, rescode}}, - + args_example = ["ejabberd@server2"], + args_desc = ["Name of erlang node to remove"], + result = {res, rescode}, + result_example = ok, + result_desc = "Status code: 0 on success, 1 otherwise"}, #ejabberd_commands{name = num_active_users, tags = [accounts, stats], desc = "Get number of users active in the last days", + policy = admin, module = ?MODULE, function = num_active_users, args = [{host, binary}, {days, integer}], - result = {users, integer}}, + args_example = [<<"myserver.com">>, 3], + args_desc = ["Name of host to check", "Number of days to calculate sum"], + result = {users, integer}, + result_example = 123, + result_desc = "Number of users active on given server in last n days"}, #ejabberd_commands{name = delete_old_users, tags = [accounts, purge], desc = "Delete users that didn't log in last days, or that never logged", module = ?MODULE, function = delete_old_users, args = [{days, integer}], - result = {res, restuple}}, + args_example = [30], + args_desc = ["Last login age in days of accounts that should be removed"], + result = {res, restuple}, + result_example = {ok, <<"Deleted 2 users: [\"oldman@myserver.com\", \"test@myserver.com\"]">>}, + result_desc = "Result tuple"}, #ejabberd_commands{name = delete_old_users_vhost, tags = [accounts, purge], desc = "Delete users that didn't log in last days in vhost, or that never logged", module = ?MODULE, function = delete_old_users_vhost, args = [{host, binary}, {days, integer}], - result = {res, restuple}}, - + args_example = [<<"myserver.com">>, 30], + args_desc = ["Server name", + "Last login age in days of accounts that should be removed"], + result = {res, restuple}, + result_example = {ok, <<"Deleted 2 users: [\"oldman@myserver.com\", \"test@myserver.com\"]">>}, + result_desc = "Result tuple"}, #ejabberd_commands{name = check_account, tags = [accounts], desc = "Check if an account exists or not", module = ejabberd_auth, function = is_user_exists, args = [{user, binary}, {host, binary}], - result = {res, rescode}}, + args_example = [<<"peter">>, <<"myserver.com">>], + args_desc = ["User name to check", "Server to check"], + result = {res, rescode}, + result_example = ok, + result_desc = "Status code: 0 on success, 1 otherwise"}, #ejabberd_commands{name = check_password, tags = [accounts], desc = "Check if a password is correct", module = ejabberd_auth, function = check_password, args = [{user, binary}, {host, binary}, {password, binary}], - result = {res, rescode}}, + args_example = [<<"peter">>, <<"myserver.com">>, <<"secret">>], + args_desc = ["User name to check", "Server to check", "Password to check"], + result = {res, rescode}, + result_example = ok, + result_desc = "Status code: 0 on success, 1 otherwise"}, #ejabberd_commands{name = check_password_hash, tags = [accounts], desc = "Check if the password hash is correct", longdesc = "Allowed hash methods: md5, sha.", module = ?MODULE, function = check_password_hash, - args = [{user, binary}, {host, binary}, {passwordhash, binary}, {hashmethod, binary}], - result = {res, rescode}}, + args = [{user, binary}, {host, binary}, {passwordhash, string}, + {hashmethod, string}], + args_example = [<<"peter">>, <<"myserver.com">>, + <<"5ebe2294ecd0e0f08eab7690d2a6ee69">>, <<"md5">>], + args_desc = ["User name to check", "Server to check", + "Password's hash value", "Name of hash method"], + result = {res, rescode}, + result_example = ok, + result_desc = "Status code: 0 on success, 1 otherwise"}, #ejabberd_commands{name = change_password, tags = [accounts], desc = "Change the password of an account", module = ?MODULE, function = set_password, args = [{user, binary}, {host, binary}, {newpass, binary}], - result = {res, rescode}}, + args_example = [<<"peter">>, <<"myserver.com">>, <<"blank">>], + args_desc = ["User name", "Server name", + "New password for user"], + result = {res, rescode}, + result_example = ok, + result_desc = "Status code: 0 on success, 1 otherwise"}, #ejabberd_commands{name = ban_account, tags = [accounts], desc = "Ban an account: kick sessions and set random password", module = ?MODULE, function = ban_account, args = [{user, binary}, {host, binary}, {reason, binary}], - result = {res, rescode}}, - + args_example = [<<"attacker">>, <<"myserver.com">>, <<"Spaming other users">>], + args_desc = ["User name to ban", "Server name", + "Reason for banning user"], + result = {res, rescode}, + result_example = ok, + result_desc = "Status code: 0 on success, 1 otherwise"}, #ejabberd_commands{name = num_resources, tags = [session], desc = "Get the number of resources of a user", module = ?MODULE, function = num_resources, args = [{user, binary}, {host, binary}], - result = {resources, integer}}, + args_example = [<<"peter">>, <<"myserver.com">>], + args_desc = ["User name", "Server name"], + result = {resources, integer}, + result_example = 5, + result_desc = "Number of active resources for a user"}, #ejabberd_commands{name = resource_num, tags = [session], desc = "Resource string of a session number", module = ?MODULE, function = resource_num, args = [{user, binary}, {host, binary}, {num, integer}], - result = {resource, string}}, + args_example = [<<"peter">>, <<"myserver.com">>, 2], + args_desc = ["User name", "Server name", "ID of resource to return"], + result = {resource, string}, + result_example = <<"Psi">>, + result_desc = "Name of user resource"}, #ejabberd_commands{name = kick_session, tags = [session], desc = "Kick a user session", module = ?MODULE, function = kick_session, args = [{user, binary}, {host, binary}, {resource, binary}, {reason, binary}], - result = {res, rescode}}, + args_example = [<<"peter">>, <<"myserver.com">>, <<"Psi">>, + <<"Stuck connection">>], + args_desc = ["User name", "Server name", "User's resource", + "Reason for closing session"], + result = {res, rescode}, + result_example = ok, + result_desc = "Status code: 0 on success, 1 otherwise"}, #ejabberd_commands{name = status_num_host, tags = [session, stats], desc = "Number of logged users with this status in host", + policy = admin, module = ?MODULE, function = status_num, args = [{host, binary}, {status, binary}], - result = {users, integer}}, + args_example = [<<"myserver.com">>, <<"dnd">>], + args_desc = ["Server name", "Status type to check"], + result = {users, integer}, + result_example = 23, + result_desc = "Number of connected sessions with given status type"}, #ejabberd_commands{name = status_num, tags = [session, stats], desc = "Number of logged users with this status", + policy = admin, module = ?MODULE, function = status_num, args = [{status, binary}], - result = {users, integer}}, + args_example = [<<"dnd">>], + args_desc = ["Status type to check"], + result = {users, integer}, + result_example = 23, + result_desc = "Number of connected sessions with given status type"}, #ejabberd_commands{name = status_list_host, tags = [session], desc = "List of users logged in host with their statuses", module = ?MODULE, function = status_list, @@ -275,7 +298,7 @@ commands() -> tags = [session], desc = "Get the list of established sessions in a vhost", module = ?MODULE, function = connected_users_vhost, - args = [{host, string}], + args = [{host, binary}], result = {connected_users_vhost, {list, {sessions, string}}}}, #ejabberd_commands{name = user_sessions_info, tags = [session], @@ -347,7 +370,7 @@ commands() -> desc = "Set multiple contents in a vCard subfield", longdesc = Vcard2FieldsString ++ "\n\n" ++ Vcard1FieldsString ++ "\n" ++ VcardXEP, module = ?MODULE, function = set_vcard, - args = [{user, binary}, {host, binary}, {name, binary}, {subname, binary}, {contents, {list, binary}}], + args = [{user, binary}, {host, binary}, {name, binary}, {subname, binary}, {contents, {list, {value, binary}}}], result = {res, rescode}}, #ejabberd_commands{name = add_rosteritem, tags = [roster], @@ -408,8 +431,9 @@ commands() -> }}}, #ejabberd_commands{name = get_roster, tags = [roster], desc = "Get roster of a local user", + policy = user, module = ?MODULE, function = get_roster, - args = [{user, binary}, {host, binary}], + args = [], result = {contacts, {list, {contact, {tuple, [ {jid, string}, {nick, string}, @@ -420,17 +444,17 @@ commands() -> #ejabberd_commands{name = push_roster, tags = [roster], desc = "Push template roster from file to a user", module = ?MODULE, function = push_roster, - args = [{file, string}, {user, string}, {host, string}], + args = [{file, binary}, {user, binary}, {host, binary}], result = {res, rescode}}, #ejabberd_commands{name = push_roster_all, tags = [roster], desc = "Push template roster from file to all those users", module = ?MODULE, function = push_roster_all, - args = [{file, string}], + args = [{file, binary}], result = {res, rescode}}, #ejabberd_commands{name = push_alltoall, tags = [roster], desc = "Add all the users to all the users of Host in Group", module = ?MODULE, function = push_alltoall, - args = [{host, string}, {group, string}], + args = [{host, binary}, {group, binary}], result = {res, rescode}}, #ejabberd_commands{name = get_last, tags = [last], @@ -503,6 +527,13 @@ commands() -> args = [{user, binary}, {host, binary}, {group, binary}, {grouphost, binary}], result = {res, rescode}}, + #ejabberd_commands{name = get_offline_count, + tags = [offline], + desc = "Get the number of unread offline messages", + policy = user, + module = mod_offline, function = count_offline_messages, + args = [], + result = {res, integer}}, #ejabberd_commands{name = send_message, tags = [stanza], desc = "Send a message to a local or remote bare of full JID", module = ?MODULE, function = send_message, @@ -514,6 +545,11 @@ commands() -> module = ?MODULE, function = send_stanza_c2s, args = [{user, binary}, {host, binary}, {resource, binary}, {stanza, binary}], result = {res, rescode}}, + #ejabberd_commands{name = send_stanza, tags = [stanza], + desc = "Send a stanza; provide From JID and valid To JID", + module = ?MODULE, function = send_stanza, + args = [{from, binary}, {to, binary}, {stanza, binary}], + result = {res, rescode}}, #ejabberd_commands{name = privacy_set, tags = [stanza], desc = "Send a IQ set privacy stanza for a local account", module = ?MODULE, function = privacy_set, @@ -521,12 +557,14 @@ commands() -> result = {res, rescode}}, #ejabberd_commands{name = stats, tags = [stats], - desc = "Get statistical value: registeredusers onlineusers onlineusersnode uptimeseconds", + desc = "Get statistical value: registeredusers onlineusers onlineusersnode uptimeseconds processes", + policy = admin, module = ?MODULE, function = stats, args = [{name, binary}], result = {stat, integer}}, #ejabberd_commands{name = stats_host, tags = [stats], desc = "Get statistical value for this host: registeredusers onlineusers", + policy = admin, module = ?MODULE, function = stats, args = [{name, binary}, {host, binary}], result = {stat, integer}} @@ -547,25 +585,6 @@ remove_node(Node) -> mnesia:del_table_copy(schema, list_to_atom(Node)), ok. -export2odbc(Host, Directory) -> - Tables = [ - {export_last, last}, - {export_offline, offline}, - {export_passwd, passwd}, - {export_private_storage, private_storage}, - {export_roster, roster}, - {export_vcard, vcard}, - {export_vcard_search, vcard_search}], - Export = fun({TableFun, Table}) -> - Filename = filename:join([Directory, atom_to_list(Table)++".txt"]), - io:format("Trying to export Mnesia table '~p' on Host '~s' to file '~s'~n", [Table, Host, Filename]), - Res = (catch ejd2odbc:TableFun(Host, Filename)), - io:format(" Result: ~p~n", [Res]) - end, - lists:foreach(Export, Tables), - ok. - - %%% %%% Accounts %%% @@ -581,12 +600,16 @@ set_password(User, Host, Password) -> %% Copied some code from ejabberd_commands.erl check_password_hash(User, Host, PasswordHash, HashMethod) -> AccountPass = ejabberd_auth:get_password_s(User, Host), - AccountPassHash = case HashMethod of - "md5" -> get_md5(AccountPass); - "sha" -> get_sha(AccountPass); + AccountPassHash = case {AccountPass, HashMethod} of + {A, _} when is_tuple(A) -> scrammed; + {_, "md5"} -> get_md5(AccountPass); + {_, "sha"} -> get_sha(AccountPass); _ -> undefined end, case AccountPassHash of + scrammed -> + ?ERROR_MSG("Passwords are scrammed, and check_password_hash can not work.", []), + throw(passwords_scrammed_command_cannot_work); undefined -> error; PasswordHash -> ok; _ -> error @@ -603,8 +626,7 @@ num_active_users(Host, Days) -> %% Code based on ejabberd/src/web/ejabberd_web_admin.erl list_last_activity(Host, Integral, Days) -> - {MegaSecs, Secs, _MicroSecs} = now(), - TimeStamp = MegaSecs * 1000000 + Secs, + TimeStamp = p1_time_compat:system_time(seconds), TS = TimeStamp - Days * 86400, case catch mnesia:dirty_select( last_activity, [{{last_activity, {'_', Host}, '$1', '_'}, @@ -669,8 +691,7 @@ delete_old_users(Days, Users) -> SecOlder = Days*24*60*60, %% Get current time - {MegaSecs, Secs, _MicroSecs} = now(), - TimeStamp_now = MegaSecs * 1000000 + Secs, + TimeStamp_now = p1_time_compat:system_time(seconds), %% For a user, remove if required and answer true F = fun({LUser, LServer}) -> @@ -727,28 +748,14 @@ kick_sessions(User, Server, Reason) -> fun(Resource) -> kick_this_session(User, Server, Resource, Reason) end, - get_resources(User, Server)). - -get_resources(User, Server) -> - lists:map( - fun(Session) -> - element(3, Session#session.usr) - end, - get_sessions(User, Server)). - -get_sessions(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - Sessions = mnesia:dirty_index_read(session, {LUser, LServer}, #session.us), - true = is_list(Sessions), - Sessions. + ejabberd_sm:get_user_resources(User, Server)). set_random_password(User, Server, Reason) -> NewPass = build_random_password(Reason), set_password_auth(User, Server, NewPass). build_random_password(Reason) -> - Date = jlib:timestamp_to_iso(calendar:universal_time()), + Date = jlib:timestamp_to_legacy(calendar:universal_time()), RandomString = randoms:get_string(), <<"BANNED_ACCOUNT--", Date/binary, "--", RandomString/binary, "--", Reason/binary>>. @@ -783,8 +790,8 @@ kick_session(User, Server, Resource, ReasonText) -> ok. kick_this_session(User, Server, Resource, Reason) -> - ejabberd_sm:route(jlib:make_jid(<<"">>, <<"">>, <<"">>), - jlib:make_jid(User, Server, Resource), + ejabberd_sm:route(jid:make(<<"">>, <<"">>, <<"">>), + jid:make(User, Server, Resource), {broadcast, {exit, Reason}}). status_num(Host, Status) -> @@ -840,7 +847,8 @@ connected_users_info() -> PI when is_integer(PI) -> PI; _ -> nil end, - {[U, $@, S, $/, R], atom_to_list(Conn), IPS, Port, PriorityI, NodeS, Uptime} + {binary_to_list(<<U/binary, $@, S/binary, $/, R/binary>>), + atom_to_list(Conn), IPS, Port, PriorityI, NodeS, Uptime} end, USRIs). @@ -863,8 +871,8 @@ stringize(String) -> set_presence(User, Host, Resource, Type, Show, Status, Priority) -> Pid = ejabberd_sm:get_session_pid(User, Host, Resource), - USR = jlib:jid_to_string(jlib:make_jid(User, Host, Resource)), - US = jlib:jid_to_string(jlib:make_jid(User, Host, <<>>)), + USR = jid:to_string(jid:make(User, Host, Resource)), + US = jid:to_string(jid:make(User, Host, <<>>)), Message = {route_xmlstreamelement, {xmlel, <<"presence">>, [{<<"from">>, USR}, {<<"to">>, US}, {<<"type">>, Type}], @@ -950,7 +958,7 @@ get_module_resource(Server) -> get_vcard_content(User, Server, Data) -> [{_, Module, Function, _Opts}] = ets:lookup(sm_iqtable, {?NS_VCARD, Server}), - JID = jlib:make_jid(User, Server, get_module_resource(Server)), + JID = jid:make(User, Server, get_module_resource(Server)), IQ = #iq{type = get, xmlns = ?NS_VCARD}, IQr = Module:Function(JID, JID, IQ), [A1] = IQr#iq.sub_el, @@ -958,7 +966,7 @@ get_vcard_content(User, Server, Data) -> [_|_] -> case get_vcard(Data, A1) of [false] -> throw(error_no_value_found_in_vcard); - ElemList -> ?DEBUG("ELS ~p", [ElemList]), [xml:get_tag_cdata(Elem) || Elem <- ElemList] + ElemList -> ?DEBUG("ELS ~p", [ElemList]), [fxml:get_tag_cdata(Elem) || Elem <- ElemList] end; [] -> throw(error_no_vcard_found) @@ -979,7 +987,7 @@ get_vcard([Data], A1) -> get_subtag(A1, Data). get_subtag(Xmlelement, Name) -> - [xml:get_subtag(Xmlelement, Name)]. + [fxml:get_subtag(Xmlelement, Name)]. set_vcard_content(User, Server, Data, SomeContent) -> ContentList = case SomeContent of @@ -987,7 +995,7 @@ set_vcard_content(User, Server, Data, SomeContent) -> Bin when is_binary(Bin) -> [SomeContent] end, [{_, Module, Function, _Opts}] = ets:lookup(sm_iqtable, {?NS_VCARD, Server}), - JID = jlib:make_jid(User, Server, get_module_resource(Server)), + JID = jid:make(User, Server, get_module_resource(Server)), IQ = #iq{type = get, xmlns = ?NS_VCARD}, IQr = Module:Function(JID, JID, IQ), @@ -1009,7 +1017,7 @@ set_vcard_content(User, Server, Data, SomeContent) -> take_vcard_tel(TelType, [{xmlel, <<"TEL">>, _, SubEls}=OldEl | OldEls], NewEls, Taken) -> {Taken2, NewEls2} = case lists:keymember(TelType, 2, SubEls) of - true -> {xml:get_subtag(OldEl, <<"NUMBER">>), NewEls}; + true -> {fxml:get_subtag(OldEl, <<"NUMBER">>), NewEls}; false -> {Taken, [OldEl | NewEls]} end, take_vcard_tel(TelType, OldEls, NewEls2, Taken2); @@ -1068,7 +1076,7 @@ subscribe(LU, LS, User, Server, Nick, Group, Subscription, _Xattrs) -> mod_roster:set_items( LU, LS, {xmlel, <<"query">>, - [{<<"xmlns">>, <<"jabber:iq:roster">>}], + [{<<"xmlns">>, ?NS_ROSTER}], [ItemEl]}). delete_rosteritem(LocalUser, LocalServer, User, Server) -> @@ -1085,7 +1093,7 @@ unsubscribe(LU, LS, User, Server) -> mod_roster:set_items( LU, LS, {xmlel, <<"query">>, - [{<<"xmlns">>, <<"jabber:iq:roster">>}], + [{<<"xmlns">>, ?NS_ROSTER}], [ItemEl]}). %% ----------------------------- @@ -1101,7 +1109,7 @@ get_roster(User, Server) -> make_roster_xmlrpc(Roster) -> lists:foldl( fun(Item, Res) -> - JIDS = jlib:jid_to_string(Item#roster.jid), + JIDS = jid:to_string(Item#roster.jid), Nick = Item#roster.name, Subs = atom_to_list(Item#roster.subscription), Ask = atom_to_list(Item#roster.ask), @@ -1167,23 +1175,23 @@ push_roster_item(LU, LS, U, S, Action) -> end, ejabberd_sm:get_user_resources(LU, LS)). push_roster_item(LU, LS, R, U, S, Action) -> - LJID = jlib:make_jid(LU, LS, R), + LJID = jid:make(LU, LS, R), BroadcastEl = build_broadcast(U, S, Action), ejabberd_sm:route(LJID, LJID, BroadcastEl), Item = build_roster_item(U, S, Action), ResIQ = build_iq_roster_push(Item), - ejabberd_router:route(LJID, LJID, ResIQ). + ejabberd_router:route(jid:remove_resource(LJID), LJID, ResIQ). build_roster_item(U, S, {add, Nick, Subs, Group}) -> {xmlel, <<"item">>, - [{<<"jid">>, jlib:jid_to_string(jlib:make_jid(U, S, <<>>))}, + [{<<"jid">>, jid:to_string(jid:make(U, S, <<>>))}, {<<"name">>, Nick}, {<<"subscription">>, Subs}], [{xmlel, <<"group">>, [], [{xmlcdata, Group}]}] }; build_roster_item(U, S, remove) -> {xmlel, <<"item">>, - [{<<"jid">>, jlib:jid_to_string(jlib:make_jid(U, S, <<>>))}, + [{<<"jid">>, jid:to_string(jid:make(U, S, <<>>))}, {<<"subscription">>, <<"remove">>}], [] }. @@ -1242,20 +1250,20 @@ get_last(User, Server) -> %% <aa xmlns='bb'>Cluth</aa> private_get(Username, Host, Element, Ns) -> - From = jlib:make_jid(Username, Host, <<>>), - To = jlib:make_jid(Username, Host, <<>>), + From = jid:make(Username, Host, <<>>), + To = jid:make(Username, Host, <<>>), IQ = {iq, <<>>, get, ?NS_PRIVATE, <<>>, {xmlel, <<"query">>, [{<<"xmlns">>,?NS_PRIVATE}], [{xmlel, Element, [{<<"xmlns">>, Ns}], []}]}}, ResIq = mod_private:process_sm_iq(From, To, IQ), [{xmlel, <<"query">>, - [{<<"xmlns">>, <<"jabber:iq:private">>}], + [{<<"xmlns">>, ?NS_PRIVATE}], [SubEl]}] = ResIq#iq.sub_el, - binary_to_list(xml:element_to_binary(SubEl)). + binary_to_list(fxml:element_to_binary(SubEl)). private_set(Username, Host, ElementString) -> - case xml_stream:parse_element(ElementString) of + case fxml_stream:parse_element(ElementString) of {error, Error} -> io:format("Error found parsing the element:~n ~p~nError: ~p~n", [ElementString, Error]), @@ -1265,8 +1273,8 @@ private_set(Username, Host, ElementString) -> end. private_set2(Username, Host, Xml) -> - From = jlib:make_jid(Username, Host, <<>>), - To = jlib:make_jid(Username, Host, <<>>), + From = jid:make(Username, Host, <<>>), + To = jid:make(Username, Host, <<>>), IQ = {iq, <<>>, set, ?NS_PRIVATE, <<>>, {xmlel, <<"query">>, [{<<"xmlns">>, ?NS_PRIVATE}], @@ -1310,7 +1318,7 @@ btl(B) -> binary_to_list(B). srg_get_members(Group, Host) -> Members = mod_shared_roster:get_group_explicit_users(Host,Group), - [jlib:jid_to_string(jlib:make_jid(MUser, MServer, <<>>)) + [jid:to_string(jid:make(MUser, MServer, <<>>)) || {MUser, MServer} <- Members]. srg_user_add(User, Host, Group, GroupHost) -> @@ -1341,8 +1349,8 @@ send_message(Type, From, To, Subject, Body) -> %% If the user is local and is online in several resources, %% the packet is sent to all its resources. send_packet_all_resources(FromJIDString, ToJIDString, Packet) -> - FromJID = jlib:string_to_jid(FromJIDString), - ToJID = jlib:string_to_jid(ToJIDString), + FromJID = jid:from_string(FromJIDString), + ToJID = jid:from_string(ToJIDString), ToUser = ToJID#jid.user, ToServer = ToJID#jid.server, case ToJID#jid.resource of @@ -1366,7 +1374,7 @@ send_packet_all_resources(FromJID, ToUser, ToServer, Packet) -> end. send_packet_all_resources(FromJID, ToU, ToS, ToR, Packet) -> - ToJID = jlib:make_jid(ToU, ToS, ToR), + ToJID = jid:make(ToU, ToS, ToR), ejabberd_router:route(FromJID, ToJID, Packet). build_packet(Type, Subject, Body) -> @@ -1378,15 +1386,33 @@ build_packet(Type, Subject, Body) -> [{xmlel, <<"body">>, [], [{xmlcdata, Body}]} | Tail] }. +send_stanza(FromString, ToString, Stanza) -> + case fxml_stream:parse_element(Stanza) of + {error, Error} -> + {error, Error}; + XmlEl -> + #xmlel{attrs = Attrs} = XmlEl, + From = jid:from_string(proplists:get_value(<<"from">>, Attrs, FromString)), + To = jid:from_string(proplists:get_value(<<"to">>, Attrs, ToString)), + ejabberd_router:route(From, To, XmlEl) + end. + send_stanza_c2s(Username, Host, Resource, Stanza) -> - C2sPid = ejabberd_sm:get_session_pid(Username, Host, Resource), - XmlEl = xml_stream:parse_element(Stanza), - p1_fsm:send_event(C2sPid, {xmlstreamelement, XmlEl}). + case {fxml_stream:parse_element(Stanza), + ejabberd_sm:get_session_pid(Username, Host, Resource)} + of + {{error, Error}, _} -> + {error, Error}; + {_, none} -> + {error, no_session}; + {XmlEl, C2sPid} -> + p1_fsm:send_event(C2sPid, {xmlstreamelement, XmlEl}) + end. privacy_set(Username, Host, QueryS) -> - From = jlib:make_jid(Username, Host, <<"">>), - To = jlib:make_jid(<<"">>, Host, <<"">>), - QueryEl = xml_stream:parse_element(QueryS), + From = jid:make(Username, Host, <<"">>), + To = jid:make(<<"">>, Host, <<"">>), + QueryEl = fxml_stream:parse_element(QueryS), StanzaEl = {xmlel, <<"iq">>, [{<<"type">>, <<"set">>}], [QueryEl]}, IQ = jlib:iq_query_info(StanzaEl), ejabberd_hooks:run_fold( @@ -1404,6 +1430,7 @@ privacy_set(Username, Host, QueryS) -> stats(Name) -> case Name of <<"uptimeseconds">> -> trunc(element(1, erlang:statistics(wall_clock))/1000); + <<"processes">> -> length(erlang:processes()); <<"registeredusers">> -> lists:foldl(fun(Host, Sum) -> ejabberd_auth:get_vh_registered_users_number(Host) + Sum end, 0, ?MYHOSTS); <<"onlineusersnode">> -> length(ejabberd_sm:dirty_get_my_sessions_list()); <<"onlineusers">> -> length(ejabberd_sm:dirty_get_sessions_list()) @@ -1526,7 +1553,7 @@ decide_rip_jid({UName, UServer, _UResource}, Match_list) -> decide_rip_jid({UName, UServer}, Match_list) -> lists:any( fun(Match_string) -> - MJID = jlib:string_to_jid(list_to_binary(Match_string)), + MJID = jid:from_string(list_to_binary(Match_string)), MName = MJID#jid.luser, MServer = MJID#jid.lserver, Is_server = is_glob_match(UServer, MServer), @@ -1559,3 +1586,6 @@ is_glob_match(String, <<"!", Glob/binary>>) -> not is_regexp_match(String, ejabberd_regexp:sh_to_awk(Glob)); is_glob_match(String, Glob) -> is_regexp_match(String, ejabberd_regexp:sh_to_awk(Glob)). + +mod_opt_type(module_resource) -> fun (A) -> A end; +mod_opt_type(_) -> [module_resource]. diff --git a/src/mod_announce.erl b/src/mod_announce.erl index 542417ff..d30cf57f 100644 --- a/src/mod_announce.erl +++ b/src/mod_announce.erl @@ -5,7 +5,7 @@ %%% Created : 11 Aug 2003 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -31,20 +31,11 @@ -behaviour(gen_mod). --export([start/2, - init/0, - stop/1, - export/1, - import/1, - import/3, - announce/3, - send_motd/1, - disco_identity/5, - disco_features/5, - disco_items/5, - send_announcement_to_all/3, - announce_commands/4, - announce_items/4]). +-export([start/2, init/0, stop/1, export/1, import/1, + import/3, announce/3, send_motd/1, disco_identity/5, + disco_features/5, disco_items/5, + send_announcement_to_all/3, announce_commands/4, + announce_items/4, mod_opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -700,10 +691,10 @@ announce_all(From, To, Packet) -> Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), ejabberd_router:route(To, From, Err); allow -> - Local = jlib:make_jid(<<>>, To#jid.server, <<>>), + Local = jid:make(<<>>, To#jid.server, <<>>), lists:foreach( fun({User, Server}) -> - Dest = jlib:make_jid(User, Server, <<>>), + Dest = jid:make(User, Server, <<>>), ejabberd_router:route(Local, Dest, Packet) end, ejabberd_auth:get_vh_registered_users(Host)) end. @@ -715,10 +706,10 @@ announce_all_hosts_all(From, To, Packet) -> Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), ejabberd_router:route(To, From, Err); allow -> - Local = jlib:make_jid(<<>>, To#jid.server, <<>>), + Local = jid:make(<<>>, To#jid.server, <<>>), lists:foreach( fun({User, Server}) -> - Dest = jlib:make_jid(User, Server, <<>>), + Dest = jid:make(User, Server, <<>>), ejabberd_router:route(Local, Dest, Packet) end, ejabberd_auth:dirty_get_registered_users()) end. @@ -749,10 +740,10 @@ announce_all_hosts_online(From, To, Packet) -> end. announce_online1(Sessions, Server, Packet) -> - Local = jlib:make_jid(<<>>, Server, <<>>), + Local = jid:make(<<>>, Server, <<>>), lists:foreach( fun({U, S, R}) -> - Dest = jlib:make_jid(U, S, R), + Dest = jid:make(U, S, R), ejabberd_router:route(Local, Dest, Packet) end, Sessions). @@ -779,7 +770,7 @@ announce_all_hosts_motd(From, To, Packet) -> end. announce_motd(Host, Packet) -> - LServer = jlib:nameprep(Host), + LServer = jid:nameprep(Host), announce_motd_update(LServer, Packet), Sessions = ejabberd_sm:get_vh_session_list(LServer), announce_online1(Sessions, LServer, Packet), @@ -854,7 +845,7 @@ announce_motd_update(LServer, Packet) -> packet = Packet}, motd_schema())}; odbc -> - XML = ejabberd_odbc:escape(xml:element_to_binary(Packet)), + XML = ejabberd_odbc:escape(fxml:element_to_binary(Packet)), F = fun() -> odbc_queries:update_t( <<"motd">>, @@ -931,7 +922,7 @@ send_motd(#jid{luser = LUser, lserver = LServer} = JID, mnesia) -> [#motd_users{}] -> ok; _ -> - Local = jlib:make_jid(<<>>, LServer, <<>>), + Local = jid:make(<<>>, LServer, <<>>), ejabberd_router:route(Local, JID, Packet), F = fun() -> mnesia:write(#motd_users{us = US}) @@ -949,7 +940,7 @@ send_motd(#jid{luser = LUser, lserver = LServer} = JID, riak) -> {ok, #motd_users{}} -> ok; _ -> - Local = jlib:make_jid(<<>>, LServer, <<>>), + Local = jid:make(<<>>, LServer, <<>>), ejabberd_router:route(Local, JID, Packet), {atomic, ejabberd_riak:put( #motd_users{us = US}, motd_users_schema(), @@ -962,7 +953,7 @@ send_motd(#jid{luser = LUser, lserver = LServer} = JID, odbc) when LUser /= <<>> case catch ejabberd_odbc:sql_query( LServer, [<<"select xml from motd where username='';">>]) of {selected, [<<"xml">>], [[XML]]} -> - case xml_stream:parse_element(XML) of + case fxml_stream:parse_element(XML) of {error, _} -> ok; Packet -> @@ -972,7 +963,7 @@ send_motd(#jid{luser = LUser, lserver = LServer} = JID, odbc) when LUser /= <<>> [<<"select username from motd " "where username='">>, Username, <<"';">>]) of {selected, [<<"username">>], []} -> - Local = jlib:make_jid(<<"">>, LServer, <<"">>), + Local = jid:make(<<"">>, LServer, <<"">>), ejabberd_router:route(Local, JID, Packet), F = fun() -> odbc_queries:update_t( @@ -995,8 +986,8 @@ send_motd(_, odbc) -> get_stored_motd(LServer) -> case get_stored_motd_packet(LServer, gen_mod:db_type(LServer, ?MODULE)) of {ok, Packet} -> - {xml:get_subtag_cdata(Packet, <<"subject">>), - xml:get_subtag_cdata(Packet, <<"body">>)}; + {fxml:get_subtag_cdata(Packet, <<"subject">>), + fxml:get_subtag_cdata(Packet, <<"body">>)}; error -> {<<>>, <<>>} end. @@ -1019,7 +1010,7 @@ get_stored_motd_packet(LServer, odbc) -> case catch ejabberd_odbc:sql_query( LServer, [<<"select xml from motd where username='';">>]) of {selected, [<<"xml">>], [[XML]]} -> - case xml_stream:parse_element(XML) of + case fxml_stream:parse_element(XML) of {error, _} -> error; Packet -> @@ -1047,10 +1038,10 @@ send_announcement_to_all(Host, SubjectS, BodyS) -> children = SubjectEls ++ BodyEls }, Sessions = ejabberd_sm:dirty_get_sessions_list(), - Local = jlib:make_jid(<<>>, Host, <<>>), + Local = jid:make(<<>>, Host, <<>>), lists:foreach( fun({U, S, R}) -> - Dest = jlib:make_jid(U, S, R), + Dest = jid:make(U, S, R), ejabberd_router:route(Local, Dest, Packet) end, Sessions). @@ -1076,7 +1067,7 @@ update_motd_table() -> fun(#motd{server = S}) -> S end, fun(#motd{server = S, packet = P} = R) -> NewS = iolist_to_binary(S), - NewP = xml:to_xmlel(P), + NewP = fxml:to_xmlel(P), R#motd{server = NewS, packet = NewP} end); _ -> @@ -1114,7 +1105,7 @@ export(_Server) -> when LServer == Host -> [[<<"delete from motd where username='';">>], [<<"insert into motd(username, xml) values ('', '">>, - ejabberd_odbc:escape(xml:element_to_binary(El)), + ejabberd_odbc:escape(fxml:element_to_binary(El)), <<"');">>]]; (_Host, _R) -> [] @@ -1133,7 +1124,7 @@ export(_Server) -> import(LServer) -> [{<<"select xml from motd where username='';">>, fun([XML]) -> - El = xml_stream:parse_element(XML), + El = fxml_stream:parse_element(XML), #motd{server = LServer, packet = El} end}, {<<"select username from motd where xml='';">>, @@ -1152,3 +1143,8 @@ import(_LServer, riak, #motd_users{us = {_, S}} = Users) -> [{'2i', [{<<"server">>, S}]}]); import(_, _, _) -> pass. + +mod_opt_type(access) -> + fun (A) when is_atom(A) -> A end; +mod_opt_type(db_type) -> fun gen_mod:v_db/1; +mod_opt_type(_) -> [access, db_type]. diff --git a/src/mod_blocking.erl b/src/mod_blocking.erl index 17278681..815884ff 100644 --- a/src/mod_blocking.erl +++ b/src/mod_blocking.erl @@ -5,7 +5,7 @@ %%% Created : 24 Aug 2008 by Stephan Maka <stephan@spaceboyz.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -17,10 +17,9 @@ %%% 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 +%%% 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. %%% %%%---------------------------------------------------------------------- @@ -28,8 +27,10 @@ -behaviour(gen_mod). +-protocol({xep, 191, '1.2'}). + -export([start/2, stop/1, process_iq/3, - process_iq_set/4, process_iq_get/5]). + process_iq_set/4, process_iq_get/5, mod_opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -75,7 +76,7 @@ process_iq_set(_, From, _To, sub_el = #xmlel{name = SubElName, children = SubEls}}) -> #jid{luser = LUser, lserver = LServer} = From, - Res = case {SubElName, xml:remove_cdata(SubEls)} of + Res = case {SubElName, fxml:remove_cdata(SubEls)} of {<<"block">>, []} -> {error, ?ERR_BAD_REQUEST}; {<<"block">>, Els} -> JIDs = parse_blocklist_items(Els, []), @@ -115,9 +116,9 @@ parse_blocklist_items([#xmlel{name = <<"item">>, attrs = Attrs} | Els], JIDs) -> - case xml:get_attr(<<"jid">>, Attrs) of + case fxml:get_attr(<<"jid">>, Attrs) of {value, JID1} -> - JID = jlib:jid_tolower(jlib:string_to_jid(JID1)), + JID = jid:tolower(jid:from_string(JID1)), parse_blocklist_items(Els, [JID | JIDs]); false -> parse_blocklist_items(Els, JIDs) end; @@ -222,23 +223,18 @@ process_blocklist_block(LUser, LServer, Filter, odbc) -> Default = case mod_privacy:sql_get_default_privacy_list_t(LUser) of - {selected, [<<"name">>], []} -> + {selected, []} -> Name = <<"Blocked contacts">>, mod_privacy:sql_add_privacy_list(LUser, Name), mod_privacy:sql_set_default_privacy_list(LUser, Name), Name; - {selected, [<<"name">>], [[Name]]} -> Name + {selected, [{Name}]} -> Name end, - {selected, [<<"id">>], [[ID]]} = + {selected, [{ID}]} = mod_privacy:sql_get_privacy_list_id_t(LUser, Default), - case mod_privacy:sql_get_privacy_list_data_by_id_t(ID) - of - {selected, - [<<"t">>, <<"value">>, <<"action">>, <<"ord">>, - <<"match_all">>, <<"match_iq">>, <<"match_message">>, - <<"match_presence_in">>, <<"match_presence_out">>], - RItems = [_ | _]} -> + case mod_privacy:sql_get_privacy_list_data_by_id_t(ID) of + {selected, RItems = [_ | _]} -> List = lists:flatmap(fun mod_privacy:raw_to_item/1, RItems); _ -> List = [] end, @@ -344,17 +340,12 @@ unblock_by_filter(LUser, LServer, Filter, odbc) -> F = fun () -> case mod_privacy:sql_get_default_privacy_list_t(LUser) of - {selected, [<<"name">>], []} -> ok; - {selected, [<<"name">>], [[Default]]} -> - {selected, [<<"id">>], [[ID]]} = + {selected, []} -> ok; + {selected, [{Default}]} -> + {selected, [{ID}]} = mod_privacy:sql_get_privacy_list_id_t(LUser, Default), - case mod_privacy:sql_get_privacy_list_data_by_id_t(ID) - of - {selected, - [<<"t">>, <<"value">>, <<"action">>, <<"ord">>, - <<"match_all">>, <<"match_iq">>, <<"match_message">>, - <<"match_presence_in">>, <<"match_presence_out">>], - RItems = [_ | _]} -> + case mod_privacy:sql_get_privacy_list_data_by_id_t(ID) of + {selected, RItems = [_ | _]} -> List = lists:flatmap(fun mod_privacy:raw_to_item/1, RItems), NewList = Filter(List), @@ -374,13 +365,13 @@ make_userlist(Name, List) -> #userlist{name = Name, list = List, needdb = NeedDb}. broadcast_list_update(LUser, LServer, Name, UserList) -> - ejabberd_sm:route(jlib:make_jid(LUser, LServer, + ejabberd_sm:route(jid:make(LUser, LServer, <<"">>), - jlib:make_jid(LUser, LServer, <<"">>), + jid:make(LUser, LServer, <<"">>), {broadcast, {privacy_list, UserList, Name}}). broadcast_blocklist_event(LUser, LServer, Event) -> - JID = jlib:make_jid(LUser, LServer, <<"">>), + JID = jid:make(LUser, LServer, <<"">>), ejabberd_sm:route(JID, JID, {broadcast, {blocking, Event}}). @@ -396,7 +387,7 @@ process_blocklist_get(LUser, LServer) -> #xmlel{name = <<"item">>, attrs = [{<<"jid">>, - jlib:jid_to_string(JID)}], + jid:to_string(JID)}], children = []} end, JIDs), @@ -434,18 +425,17 @@ process_blocklist_get(LUser, LServer, odbc) -> case catch mod_privacy:sql_get_default_privacy_list(LUser, LServer) of - {selected, [<<"name">>], []} -> []; - {selected, [<<"name">>], [[Default]]} -> + {selected, []} -> []; + {selected, [{Default}]} -> case catch mod_privacy:sql_get_privacy_list_data(LUser, LServer, Default) of - {selected, - [<<"t">>, <<"value">>, <<"action">>, <<"ord">>, - <<"match_all">>, <<"match_iq">>, <<"match_message">>, - <<"match_presence_in">>, <<"match_presence_out">>], - RItems} -> + {selected, RItems} -> lists:flatmap(fun mod_privacy:raw_to_item/1, RItems); {'EXIT', _} -> error end; {'EXIT', _} -> error end. + +mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; +mod_opt_type(_) -> [iqdisc]. diff --git a/src/mod_caps.erl b/src/mod_caps.erl index 36c8c0ee..0646d381 100644 --- a/src/mod_caps.erl +++ b/src/mod_caps.erl @@ -5,7 +5,7 @@ %%% Created : 7 Oct 2006 by Magnus Henoch <henoch@dtek.chalmers.se> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -29,6 +29,8 @@ -author('henoch@dtek.chalmers.se'). +-protocol({xep, 115, '1.5'}). + -behaviour(gen_server). -behaviour(gen_mod). @@ -45,10 +47,9 @@ -export([init/1, handle_info/2, handle_call/3, handle_cast/2, terminate/2, code_change/3]). -%% hook handlers --export([user_send_packet/3, user_receive_packet/4, +-export([user_send_packet/4, user_receive_packet/5, c2s_presence_in/2, c2s_filter_packet/6, - c2s_broadcast_recipients/6]). + c2s_broadcast_recipients/6, mod_opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -119,12 +120,12 @@ read_caps(Els) -> read_caps(Els, nothing). read_caps([#xmlel{name = <<"c">>, attrs = Attrs} | Tail], Result) -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of + case fxml:get_attr_s(<<"xmlns">>, Attrs) of ?NS_CAPS -> - Node = xml:get_attr_s(<<"node">>, Attrs), - Version = xml:get_attr_s(<<"ver">>, Attrs), - Hash = xml:get_attr_s(<<"hash">>, Attrs), - Exts = str:tokens(xml:get_attr_s(<<"ext">>, Attrs), + Node = fxml:get_attr_s(<<"node">>, Attrs), + Version = fxml:get_attr_s(<<"ver">>, Attrs), + Hash = fxml:get_attr_s(<<"hash">>, Attrs), + Exts = str:tokens(fxml:get_attr_s(<<"ext">>, Attrs), <<" ">>), read_caps(Tail, #caps{node = Node, hash = Hash, version = Version, @@ -134,7 +135,7 @@ read_caps([#xmlel{name = <<"c">>, attrs = Attrs} read_caps([#xmlel{name = <<"x">>, attrs = Attrs} | Tail], Result) -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of + case fxml:get_attr_s(<<"xmlns">>, Attrs) of ?NS_MUC_USER -> nothing; _ -> read_caps(Tail, Result) end; @@ -142,12 +143,13 @@ read_caps([_ | Tail], Result) -> read_caps(Tail, Result); read_caps([], Result) -> Result. -user_send_packet(#jid{luser = User, lserver = Server} = From, +user_send_packet(#xmlel{name = <<"presence">>, attrs = Attrs, + children = Els} = Pkt, + _C2SState, + #jid{luser = User, lserver = Server} = From, #jid{luser = User, lserver = Server, - lresource = <<"">>}, - #xmlel{name = <<"presence">>, attrs = Attrs, - children = Els} = Pkt) -> - Type = xml:get_attr_s(<<"type">>, Attrs), + lresource = <<"">>}) -> + Type = fxml:get_attr_s(<<"type">>, Attrs), if Type == <<"">>; Type == <<"available">> -> case read_caps(Els) of nothing -> ok; @@ -157,14 +159,15 @@ user_send_packet(#jid{luser = User, lserver = Server} = From, true -> ok end, Pkt; -user_send_packet( _From, _To, Pkt) -> +user_send_packet(Pkt, _C2SState, _From, _To) -> Pkt. -user_receive_packet(#jid{lserver = Server}, - From, _To, - #xmlel{name = <<"presence">>, attrs = Attrs, - children = Els} = Pkt) -> - Type = xml:get_attr_s(<<"type">>, Attrs), +user_receive_packet(#xmlel{name = <<"presence">>, attrs = Attrs, + children = Els} = Pkt, + _C2SState, + #jid{lserver = Server}, + From, _To) -> + Type = fxml:get_attr_s(<<"type">>, Attrs), IsRemote = not lists:member(From#jid.lserver, ?MYHOSTS), if IsRemote and ((Type == <<"">>) or (Type == <<"available">>)) -> @@ -176,7 +179,7 @@ user_receive_packet(#jid{lserver = Server}, true -> ok end, Pkt; -user_receive_packet( _JID, _From, _To, Pkt) -> +user_receive_packet(Pkt, _C2SState, _JID, _From, _To) -> Pkt. -spec caps_stream_features([xmlel()], binary()) -> [xmlel()]. @@ -224,7 +227,7 @@ disco_info(Acc, Host, Module, Node, Lang) -> c2s_presence_in(C2SState, {From, To, {_, _, Attrs, Els}}) -> - Type = xml:get_attr_s(<<"type">>, Attrs), + Type = fxml:get_attr_s(<<"type">>, Attrs), Subscription = ejabberd_c2s:get_subscription(From, C2SState), Insert = ((Type == <<"">>) or (Type == <<"available">>)) @@ -232,7 +235,7 @@ c2s_presence_in(C2SState, Delete = (Type == <<"unavailable">>) or (Type == <<"error">>), if Insert or Delete -> - LFrom = jlib:jid_tolower(From), + LFrom = jid:tolower(From), Rs = case ejabberd_c2s:get_aux_field(caps_resources, C2SState) of @@ -240,27 +243,24 @@ c2s_presence_in(C2SState, error -> gb_trees:empty() end, Caps = read_caps(Els), - {CapsUpdated, NewRs} = case Caps of - nothing when Insert == true -> {false, Rs}; - _ when Insert == true -> - case gb_trees:lookup(LFrom, Rs) of - {value, Caps} -> {false, Rs}; - none -> - {true, - gb_trees:insert(LFrom, Caps, - Rs)}; - _ -> - {true, - gb_trees:update(LFrom, Caps, Rs)} - end; - _ -> {false, gb_trees:delete_any(LFrom, Rs)} - end, - if CapsUpdated -> - ejabberd_hooks:run(caps_update, To#jid.lserver, - [From, To, - get_features(To#jid.lserver, Caps)]); - true -> ok - end, + NewRs = case Caps of + nothing when Insert == true -> Rs; + _ when Insert == true -> + case gb_trees:lookup(LFrom, Rs) of + {value, Caps} -> Rs; + none -> + ejabberd_hooks:run(caps_add, To#jid.lserver, + [From, To, + get_features(To#jid.lserver, Caps)]), + gb_trees:insert(LFrom, Caps, Rs); + _ -> + ejabberd_hooks:run(caps_update, To#jid.lserver, + [From, To, + get_features(To#jid.lserver, Caps)]), + gb_trees:update(LFrom, Caps, Rs) + end; + _ -> gb_trees:delete_any(LFrom, Rs) + end, ejabberd_c2s:set_aux_field(caps_resources, NewRs, C2SState); true -> C2SState @@ -269,7 +269,7 @@ c2s_presence_in(C2SState, c2s_filter_packet(InAcc, Host, C2SState, {pep_message, Feature}, To, _Packet) -> case ejabberd_c2s:get_aux_field(caps_resources, C2SState) of {ok, Rs} -> - LTo = jlib:jid_tolower(To), + LTo = jid:tolower(To), case gb_trees:lookup(LTo, Rs) of {value, Caps} -> Drop = not lists:member(Feature, get_features(Host, Caps)), @@ -417,7 +417,7 @@ feature_request(Host, From, Caps, feature_response(IQReply, Host, From, Caps, SubNodes) end, - ejabberd_local:route_iq(jlib:make_jid(<<"">>, Host, + ejabberd_local:route_iq(jid:make(<<"">>, Host, <<"">>), From, IQ, F); true -> feature_request(Host, From, Caps, Tail) @@ -434,7 +434,7 @@ feature_response(#iq{type = result, Features = lists:flatmap(fun (#xmlel{name = <<"feature">>, attrs = FAttrs}) -> - [xml:get_attr_s(<<"var">>, FAttrs)]; + [fxml:get_attr_s(<<"var">>, FAttrs)]; (_) -> [] end, Els), @@ -449,7 +449,7 @@ feature_response(_IQResult, Host, From, Caps, feature_request(Host, From, Caps, SubNodes). caps_read_fun(Host, Node) -> - LServer = jlib:nameprep(Host), + LServer = jid:nameprep(Host), DBType = gen_mod:db_type(LServer, ?MODULE), caps_read_fun(LServer, Node, DBType). @@ -488,7 +488,7 @@ caps_read_fun(LServer, {Node, SubNode}, odbc) -> end. caps_write_fun(Host, Node, Features) -> - LServer = jlib:nameprep(Host), + LServer = jid:nameprep(Host), DBType = gen_mod:db_type(LServer, ?MODULE), caps_write_fun(LServer, Node, Features, DBType). @@ -511,7 +511,7 @@ caps_write_fun(LServer, NodePair, Features, odbc) -> end. make_my_disco_hash(Host) -> - JID = jlib:make_jid(<<"">>, Host, <<"">>), + JID = jid:make(<<"">>, Host, <<"">>), case {ejabberd_hooks:run_fold(disco_local_features, Host, empty, [JID, JID, <<"">>, <<"">>]), ejabberd_hooks:run_fold(disco_local_identity, Host, [], @@ -567,7 +567,7 @@ concat_features(Els) -> lists:usort(lists:flatmap(fun (#xmlel{name = <<"feature">>, attrs = Attrs}) -> - [[xml:get_attr_s(<<"var">>, Attrs), $<]]; + [[fxml:get_attr_s(<<"var">>, Attrs), $<]]; (_) -> [] end, Els)). @@ -576,11 +576,11 @@ concat_identities(Els) -> lists:sort(lists:flatmap(fun (#xmlel{name = <<"identity">>, attrs = Attrs}) -> - [[xml:get_attr_s(<<"category">>, Attrs), - $/, xml:get_attr_s(<<"type">>, Attrs), + [[fxml:get_attr_s(<<"category">>, Attrs), + $/, fxml:get_attr_s(<<"type">>, Attrs), $/, - xml:get_attr_s(<<"xml:lang">>, Attrs), - $/, xml:get_attr_s(<<"name">>, Attrs), + fxml:get_attr_s(<<"xml:lang">>, Attrs), + $/, fxml:get_attr_s(<<"name">>, Attrs), $<]]; (_) -> [] end, @@ -589,8 +589,8 @@ concat_identities(Els) -> concat_info(Els) -> lists:sort(lists:flatmap(fun (#xmlel{name = <<"x">>, attrs = Attrs, children = Fields}) -> - case {xml:get_attr_s(<<"xmlns">>, Attrs), - xml:get_attr_s(<<"type">>, Attrs)} + case {fxml:get_attr_s(<<"xmlns">>, Attrs), + fxml:get_attr_s(<<"type">>, Attrs)} of {?NS_XDATA, <<"result">>} -> [concat_xdata_fields(Fields)]; @@ -606,10 +606,10 @@ concat_xdata_fields(Fields) -> attrs = Attrs, children = Els} = El, [FormType, VarFields] = Acc) -> - case xml:get_attr_s(<<"var">>, Attrs) of + case fxml:get_attr_s(<<"var">>, Attrs) of <<"">> -> Acc; <<"FORM_TYPE">> -> - [xml:get_subtag_cdata(El, + [fxml:get_subtag_cdata(El, <<"value">>), VarFields]; Var -> @@ -622,7 +622,7 @@ concat_xdata_fields(Fields) -> children = VEls}) -> - [[xml:get_cdata(VEls), + [[fxml:get_cdata(VEls), $<]]; (_) -> [] @@ -648,7 +648,7 @@ gb_trees_fold_iter(F, Acc, Iter) -> end. now_ts() -> - {MegaSecs, Secs, _} = now(), MegaSecs * 1000000 + Secs. + p1_time_compat:system_time(seconds). is_valid_node(Node) -> case str:tokens(Node, <<"#">>) of @@ -752,3 +752,11 @@ import_next(LServer, DBType, NodePair) -> ok end, import_next(LServer, DBType, ets:next(caps_features_tmp, NodePair)). + +mod_opt_type(cache_life_time) -> + fun (I) when is_integer(I), I > 0 -> I end; +mod_opt_type(cache_size) -> + fun (I) when is_integer(I), I > 0 -> I end; +mod_opt_type(db_type) -> fun gen_mod:v_db/1; +mod_opt_type(_) -> + [cache_life_time, cache_size, db_type]. diff --git a/src/mod_carboncopy.erl b/src/mod_carboncopy.erl index 24c09bff..81cf78a9 100644 --- a/src/mod_carboncopy.erl +++ b/src/mod_carboncopy.erl @@ -7,7 +7,7 @@ %%% {mod_carboncopy, []} %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,7 +25,9 @@ %%% %%%---------------------------------------------------------------------- -module (mod_carboncopy). + -author ('ecestari@process-one.net'). +-protocol({xep, 280, '0.8'}). -behavior(gen_mod). @@ -33,13 +35,9 @@ -export([start/2, stop/1]). -%% Hooks: --export([user_send_packet/3, - user_receive_packet/4, - iq_handler2/3, - iq_handler1/3, - remove_connection/4, - is_carbon_copy/1]). +-export([user_send_packet/4, user_receive_packet/5, + iq_handler2/3, iq_handler1/3, remove_connection/4, + is_carbon_copy/1, mod_opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -57,9 +55,9 @@ is_carbon_copy(Packet) -> is_carbon_copy(Packet, <<"received">>). is_carbon_copy(Packet, Direction) -> - case xml:get_subtag(Packet, Direction) of + case fxml:get_subtag(Packet, Direction) of #xmlel{name = Direction, attrs = Attrs} -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of + case fxml:get_attr_s(<<"xmlns">>, Attrs) of ?NS_CARBONS_2 -> true; ?NS_CARBONS_1 -> true; _ -> false @@ -106,7 +104,7 @@ iq_handler1(From, To, IQ) -> iq_handler(From, _To, #iq{type=set, sub_el = #xmlel{name = Operation, children = []}} = IQ, CC)-> ?DEBUG("carbons IQ received: ~p", [IQ]), - {U, S, R} = jlib:jid_tolower(From), + {U, S, R} = jid:tolower(From), Result = case Operation of <<"enable">>-> ?INFO_MSG("carbons enabled for user ~s@~s/~s", [U,S,R]), @@ -115,7 +113,7 @@ iq_handler(From, _To, #iq{type=set, sub_el = #xmlel{name = Operation, children ?INFO_MSG("carbons disabled for user ~s@~s/~s", [U,S,R]), disable(S, U, R) end, - case Result of + case Result of ok -> ?DEBUG("carbons IQ result: ok", []), IQ#iq{type=result, sub_el=[]}; @@ -127,43 +125,43 @@ iq_handler(From, _To, #iq{type=set, sub_el = #xmlel{name = Operation, children iq_handler(_From, _To, IQ, _CC)-> IQ#iq{type=error, sub_el = [?ERR_NOT_ALLOWED]}. -user_send_packet(From, To, Packet) -> +user_send_packet(Packet, _C2SState, From, To) -> check_and_forward(From, To, Packet, sent). -user_receive_packet(JID, _From, To, Packet) -> +user_receive_packet(Packet, _C2SState, JID, _From, To) -> check_and_forward(JID, To, Packet, received). - -% verifier si le trafic est local -% Modified from original version: + +% Modified from original version: % - registered to the user_send_packet hook, to be called only once even for multicast % - do not support "private" message mode, and do not modify the original packet in any way % - we also replicate "read" notifications check_and_forward(JID, To, Packet, Direction)-> - case is_chat_or_normal_message(Packet) andalso - xml:get_subtag(Packet, <<"private">>) == false andalso - xml:get_subtag(Packet, <<"no-copy">>) == false of + case is_chat_message(Packet) andalso + fxml:get_subtag(Packet, <<"private">>) == false andalso + fxml:get_subtag(Packet, <<"no-copy">>) == false of true -> case is_carbon_copy(Packet) of false -> - send_copies(JID, To, Packet, Direction); + send_copies(JID, To, Packet, Direction), + Packet; true -> - %% stop the hook chain, we don't want mod_logdb to register - %% this message (duplicate) - stop + %% stop the hook chain, we don't want logging modules to duplicates + %% this message + {stop, Packet} end; _ -> - ok + Packet end. remove_connection(User, Server, Resource, _Status)-> disable(Server, User, Resource), ok. - + %%% Internal %% Direction = received | sent <received xmlns='urn:xmpp:carbons:1'/> send_copies(JID, To, Packet, Direction)-> - {U, S, R} = jlib:jid_tolower(JID), + {U, S, R} = jid:tolower(JID), PrioRes = ejabberd_sm:get_user_present_resources(U, S), {_, AvailRs} = lists:unzip(PrioRes), {MaxPrio, MaxRes} = case catch lists:max(PrioRes) of @@ -182,7 +180,7 @@ send_copies(JID, To, Packet, Direction)-> TargetJIDs = case {IsBareTo, R} of {true, MaxRes} -> OrigTo = fun(Res) -> lists:member({MaxPrio, Res}, PrioRes) end, - [ {jlib:make_jid({U, S, CCRes}), CC_Version} + [ {jid:make({U, S, CCRes}), CC_Version} || {CCRes, CC_Version} <- list(U, S), lists:member(CCRes, AvailRs), not OrigTo(CCRes) ]; {true, _} -> @@ -193,16 +191,16 @@ send_copies(JID, To, Packet, Direction)-> %% MaxRes) in order to avoid duplicates. []; {false, _} -> - [ {jlib:make_jid({U, S, CCRes}), CC_Version} + [ {jid:make({U, S, CCRes}), CC_Version} || {CCRes, CC_Version} <- list(U, S), lists:member(CCRes, AvailRs), CCRes /= R ] - %TargetJIDs = lists:delete(JID, [ jlib:make_jid({U, S, CCRes}) || CCRes <- list(U, S) ]), + %TargetJIDs = lists:delete(JID, [ jid:make({U, S, CCRes}) || CCRes <- list(U, S) ]), end, lists:map(fun({Dest,Version}) -> - {_, _, Resource} = jlib:jid_tolower(Dest), + {_, _, Resource} = jid:tolower(Dest), ?DEBUG("Sending: ~p =/= ~p", [R, Resource]), - Sender = jlib:make_jid({U, S, <<>>}), + Sender = jid:make({U, S, <<>>}), %{xmlelement, N, A, C} = Packet, New = build_forward_packet(JID, Packet, Sender, Dest, Direction, Version), ejabberd_router:route(Sender, Dest, New) @@ -210,31 +208,31 @@ send_copies(JID, To, Packet, Direction)-> ok. build_forward_packet(JID, Packet, Sender, Dest, Direction, ?NS_CARBONS_2) -> - #xmlel{name = <<"message">>, + #xmlel{name = <<"message">>, attrs = [{<<"xmlns">>, <<"jabber:client">>}, {<<"type">>, message_type(Packet)}, - {<<"from">>, jlib:jid_to_string(Sender)}, - {<<"to">>, jlib:jid_to_string(Dest)}], - children = [ - #xmlel{name = list_to_binary(atom_to_list(Direction)), + {<<"from">>, jid:to_string(Sender)}, + {<<"to">>, jid:to_string(Dest)}], + children = [ + #xmlel{name = list_to_binary(atom_to_list(Direction)), attrs = [{<<"xmlns">>, ?NS_CARBONS_2}], children = [ - #xmlel{name = <<"forwarded">>, + #xmlel{name = <<"forwarded">>, attrs = [{<<"xmlns">>, ?NS_FORWARD}], children = [ complete_packet(JID, Packet, Direction)]} ]} ]}; build_forward_packet(JID, Packet, Sender, Dest, Direction, ?NS_CARBONS_1) -> - #xmlel{name = <<"message">>, + #xmlel{name = <<"message">>, attrs = [{<<"xmlns">>, <<"jabber:client">>}, {<<"type">>, message_type(Packet)}, - {<<"from">>, jlib:jid_to_string(Sender)}, - {<<"to">>, jlib:jid_to_string(Dest)}], - children = [ - #xmlel{name = list_to_binary(atom_to_list(Direction)), + {<<"from">>, jid:to_string(Sender)}, + {<<"to">>, jid:to_string(Dest)}], + children = [ + #xmlel{name = list_to_binary(atom_to_list(Direction)), attrs = [{<<"xmlns">>, ?NS_CARBONS_1}]}, - #xmlel{name = <<"forwarded">>, + #xmlel{name = <<"forwarded">>, attrs = [{<<"xmlns">>, ?NS_FORWARD}], children = [complete_packet(JID, Packet, Direction)]} ]}. @@ -261,7 +259,7 @@ complete_packet(From, #xmlel{name = <<"message">>, attrs = OrigAttrs} = Packet, Attrs = lists:keystore(<<"xmlns">>, 1, OrigAttrs, {<<"xmlns">>, <<"jabber:client">>}), case proplists:get_value(<<"from">>, Attrs) of undefined -> - Packet#xmlel{attrs = [{<<"from">>, jlib:jid_to_string(From)}|Attrs]}; + Packet#xmlel{attrs = [{<<"from">>, jid:to_string(From)}|Attrs]}; _ -> Packet#xmlel{attrs = Attrs} end; @@ -270,20 +268,26 @@ complete_packet(_From, #xmlel{name = <<"message">>, attrs=OrigAttrs} = Packet, r Packet#xmlel{attrs = Attrs}. message_type(#xmlel{attrs = Attrs}) -> - case xml:get_attr(<<"type">>, Attrs) of + case fxml:get_attr(<<"type">>, Attrs) of {value, Type} -> Type; false -> <<"normal">> end. -is_chat_or_normal_message(#xmlel{name = <<"message">>} = Packet) -> +is_chat_message(#xmlel{name = <<"message">>} = Packet) -> case message_type(Packet) of <<"chat">> -> true; - <<"normal">> -> true; + <<"normal">> -> has_non_empty_body(Packet); _ -> false end; -is_chat_or_normal_message(_Packet) -> false. +is_chat_message(_Packet) -> false. + +has_non_empty_body(Packet) -> + fxml:get_subtag_cdata(Packet, <<"body">>) =/= <<"">>. %% list {resource, cc_version} with carbons enabled for given user and host -list(User, Server)-> +list(User, Server) -> mnesia:dirty_select(?TABLE, [{#carboncopy{us = {User, Server}, resource = '$2', version = '$3'}, [], [{{'$2','$3'}}]}]). + +mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; +mod_opt_type(_) -> [iqdisc]. diff --git a/src/mod_client_state.erl b/src/mod_client_state.erl index fd72c02f..dfbfc028 100644 --- a/src/mod_client_state.erl +++ b/src/mod_client_state.erl @@ -1,11 +1,11 @@ %%%---------------------------------------------------------------------- %%% File : mod_client_state.erl -%%% Author : Holger Weiss +%%% Author : Holger Weiss <holger@zedat.fu-berlin.de> %%% Purpose : Filter stanzas sent to inactive clients (XEP-0352) -%%% Created : 11 Sep 2014 by Holger Weiss +%%% Created : 11 Sep 2014 by Holger Weiss <holger@zedat.fu-berlin.de> %%% %%% -%%% ejabberd, Copyright (C) 2014-2015 ProcessOne +%%% ejabberd, Copyright (C) 2014-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,11 +25,14 @@ -module(mod_client_state). -author('holger@zedat.fu-berlin.de'). +-protocol({xep, 85, '2.1'}). +-protocol({xep, 352, '0.1'}). -behavior(gen_mod). --export([start/2, stop/1, add_stream_feature/2, filter_presence/2, - filter_chat_states/2]). +-export([start/2, stop/1, add_stream_feature/2, + filter_presence/2, filter_chat_states/2, + mod_opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -39,11 +42,11 @@ start(Host, Opts) -> QueuePresence = gen_mod:get_opt(queue_presence, Opts, fun(true) -> true; (false) -> false - end, false), + end, true), DropChatStates = gen_mod:get_opt(drop_chat_states, Opts, fun(true) -> true; (false) -> false - end, false), + end, true), if QueuePresence; DropChatStates -> ejabberd_hooks:add(c2s_post_auth_features, Host, ?MODULE, add_stream_feature, 50), @@ -77,7 +80,7 @@ add_stream_feature(Features, _Host) -> [Feature | Features]. filter_presence(_Action, #xmlel{name = <<"presence">>, attrs = Attrs}) -> - case xml:get_attr(<<"type">>, Attrs) of + case fxml:get_attr(<<"type">>, Attrs) of {value, Type} when Type /= <<"unavailable">> -> ?DEBUG("Got important presence stanza", []), {stop, send}; @@ -88,22 +91,22 @@ filter_presence(_Action, #xmlel{name = <<"presence">>, attrs = Attrs}) -> filter_presence(Action, _Stanza) -> Action. filter_chat_states(_Action, #xmlel{name = <<"message">>} = Stanza) -> - %% All XEP-0085 chat states except for <gone/>: - ChatStates = [<<"active">>, <<"inactive">>, <<"composing">>, <<"paused">>], - Stripped = - lists:foldl(fun(ChatState, AccStanza) -> - xml:remove_subtags(AccStanza, ChatState, - {<<"xmlns">>, ?NS_CHATSTATES}) - end, Stanza, ChatStates), - case Stripped of - #xmlel{children = [#xmlel{name = <<"thread">>}]} -> + case jlib:is_standalone_chat_state(Stanza) of + true -> ?DEBUG("Got standalone chat state notification", []), {stop, drop}; - #xmlel{children = []} -> - ?DEBUG("Got standalone chat state notification", []), - {stop, drop}; - _ -> - ?DEBUG("Got message with chat state notification", []), + false -> + ?DEBUG("Got message stanza", []), {stop, send} end; filter_chat_states(Action, _Stanza) -> Action. + +mod_opt_type(drop_chat_states) -> + fun (true) -> true; + (false) -> false + end; +mod_opt_type(queue_presence) -> + fun (true) -> true; + (false) -> false + end; +mod_opt_type(_) -> [drop_chat_states, queue_presence]. diff --git a/src/mod_configure.erl b/src/mod_configure.erl index 9e6e83e1..5e011704 100644 --- a/src/mod_configure.erl +++ b/src/mod_configure.erl @@ -5,7 +5,7 @@ %%% Created : 19 Jan 2003 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -23,33 +23,29 @@ %%% %%%---------------------------------------------------------------------- -%%% Implements most of XEP-0133: Service Administration Version 1.1 -%%% (2005-08-19) - -module(mod_configure). -author('alexey@process-one.net'). +-protocol({xep, 133, '1.1'}). + -behaviour(gen_mod). -export([start/2, stop/1, get_local_identity/5, get_local_features/5, get_local_items/5, adhoc_local_items/4, adhoc_local_commands/4, get_sm_identity/5, get_sm_features/5, get_sm_items/5, - adhoc_sm_items/4, adhoc_sm_commands/4]). + adhoc_sm_items/4, adhoc_sm_commands/4, mod_opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). -include("jlib.hrl"). - +-include("ejabberd_sm.hrl"). -include("adhoc.hrl"). -define(T(Lang, Text), translate:translate(Lang, Text)). -%% Copied from ejabberd_sm.erl --record(session, {sid, usr, us, priority, info}). - start(Host, _Opts) -> ejabberd_hooks:add(disco_local_items, Host, ?MODULE, get_local_items, 50), @@ -115,7 +111,7 @@ stop(Host) -> -define(NODEJID(To, Name, Node), #xmlel{name = <<"item">>, attrs = - [{<<"jid">>, jlib:jid_to_string(To)}, + [{<<"jid">>, jid:to_string(To)}, {<<"name">>, ?T(Lang, Name)}, {<<"node">>, Node}], children = []}). @@ -293,7 +289,7 @@ adhoc_sm_items(Acc, From, #jid{lserver = LServer} = To, end, Nodes = [#xmlel{name = <<"item">>, attrs = - [{<<"jid">>, jlib:jid_to_string(To)}, + [{<<"jid">>, jid:to_string(To)}, {<<"name">>, ?T(Lang, <<"Configuration">>)}, {<<"node">>, <<"config">>}], children = []}], @@ -354,7 +350,7 @@ adhoc_local_items(Acc, From, Nodes = recursively_get_local_items(PermLev, LServer, <<"">>, Server, Lang), Nodes1 = lists:filter(fun (N) -> - Nd = xml:get_tag_attr_s(<<"node">>, N), + Nd = fxml:get_tag_attr_s(<<"node">>, N), F = get_local_features([], From, To, Nd, Lang), case F of @@ -383,9 +379,9 @@ recursively_get_local_items(PermLev, LServer, Node, {error, _Error} -> [] end, Nodes = lists:flatten(lists:map(fun (N) -> - S = xml:get_tag_attr_s(<<"jid">>, + S = fxml:get_tag_attr_s(<<"jid">>, N), - Nd = xml:get_tag_attr_s(<<"node">>, + Nd = fxml:get_tag_attr_s(<<"node">>, N), if (S /= Server) or (Nd == <<"">>) -> @@ -416,7 +412,7 @@ get_permission_level(JID) -> allow -> PermLev = get_permission_level(From), case get_local_items({PermLev, LServer}, LNode, - jlib:jid_to_string(To), Lang) + jid:to_string(To), Lang) of {result, Res} -> {result, Res}; {error, Error} -> {error, Error} @@ -438,7 +434,7 @@ get_local_items(Acc, From, #jid{lserver = LServer} = To, allow -> PermLev = get_permission_level(From), case get_local_items({PermLev, LServer}, [], - jlib:jid_to_string(To), Lang) + jid:to_string(To), Lang) of {result, Res} -> {result, Items ++ Res}; {error, _Error} -> {result, Items} @@ -986,7 +982,7 @@ get_form(_Host, [<<"running nodes">>, ENode, <<"DB">>], case search_running_node(ENode) of false -> {error, ?ERR_ITEM_NOT_FOUND}; Node -> - case rpc:call(Node, mnesia, system_info, [tables]) of + case ejabberd_cluster:call(Node, mnesia, system_info, [tables]) of {badrpc, _Reason} -> {error, ?ERR_INTERNAL_SERVER_ERROR}; Tables -> @@ -1008,7 +1004,7 @@ get_form(_Host, [<<"running nodes">>, ENode, <<"DB">>], ?T(Lang, <<"Choose storage type of tables">>)}]} | lists:map(fun (Table) -> - case rpc:call(Node, mnesia, + case ejabberd_cluster:call(Node, mnesia, table_info, [Table, storage_type]) @@ -1029,7 +1025,7 @@ get_form(Host, case search_running_node(ENode) of false -> {error, ?ERR_ITEM_NOT_FOUND}; Node -> - case rpc:call(Node, gen_mod, loaded_modules, [Host]) of + case ejabberd_cluster:call(Node, gen_mod, loaded_modules, [Host]) of {badrpc, _Reason} -> {error, ?ERR_INTERNAL_SERVER_ERROR}; Modules -> @@ -1608,7 +1604,7 @@ set_form(_From, Host, case Vals of [<<"1">>] -> Module = jlib:binary_to_atom(Var), - rpc:call(Node, gen_mod, stop_module, + ejabberd_cluster:call(Node, gen_mod, stop_module, [Host, Module]); _ -> ok end @@ -1635,7 +1631,7 @@ set_form(_From, Host, case erl_parse:parse_term(Tokens) of {ok, Modules} -> lists:foreach(fun ({Module, Args}) -> - rpc:call(Node, gen_mod, + ejabberd_cluster:call(Node, gen_mod, start_module, [Host, Module, Args]) end, @@ -1657,7 +1653,7 @@ set_form(_From, _Host, case lists:keysearch(<<"path">>, 1, XData) of false -> {error, ?ERR_BAD_REQUEST}; {value, {_, [String]}} -> - case rpc:call(Node, mnesia, backup, [String]) of + case ejabberd_cluster:call(Node, mnesia, backup, [String]) of {badrpc, _Reason} -> {error, ?ERR_INTERNAL_SERVER_ERROR}; {error, _Reason} -> {error, ?ERR_INTERNAL_SERVER_ERROR}; @@ -1676,7 +1672,7 @@ set_form(_From, _Host, case lists:keysearch(<<"path">>, 1, XData) of false -> {error, ?ERR_BAD_REQUEST}; {value, {_, [String]}} -> - case rpc:call(Node, ejabberd_admin, restore, [String]) + case ejabberd_cluster:call(Node, ejabberd_admin, restore, [String]) of {badrpc, _Reason} -> {error, ?ERR_INTERNAL_SERVER_ERROR}; @@ -1696,7 +1692,7 @@ set_form(_From, _Host, case lists:keysearch(<<"path">>, 1, XData) of false -> {error, ?ERR_BAD_REQUEST}; {value, {_, [String]}} -> - case rpc:call(Node, ejabberd_admin, dump_to_textfile, + case ejabberd_cluster:call(Node, ejabberd_admin, dump_to_textfile, [String]) of {badrpc, _Reason} -> @@ -1716,7 +1712,7 @@ set_form(_From, _Host, case lists:keysearch(<<"path">>, 1, XData) of false -> {error, ?ERR_BAD_REQUEST}; {value, {_, [String]}} -> - rpc:call(Node, jd2ejd, import_file, [String]), + ejabberd_cluster:call(Node, jd2ejd, import_file, [String]), {result, []}; _ -> {error, ?ERR_BAD_REQUEST} end @@ -1730,7 +1726,7 @@ set_form(_From, _Host, case lists:keysearch(<<"path">>, 1, XData) of false -> {error, ?ERR_BAD_REQUEST}; {value, {_, [String]}} -> - rpc:call(Node, jd2ejd, import_dir, [String]), + ejabberd_cluster:call(Node, jd2ejd, import_dir, [String]), {result, []}; _ -> {error, ?ERR_BAD_REQUEST} end @@ -1818,7 +1814,7 @@ set_form(From, Host, ?NS_ADMINL(<<"add-user">>), _Lang, AccountString = get_value(<<"accountjid">>, XData), Password = get_value(<<"password">>, XData), Password = get_value(<<"password-verify">>, XData), - AccountJID = jlib:string_to_jid(AccountString), + AccountJID = jid:from_string(AccountString), User = AccountJID#jid.luser, Server = AccountJID#jid.lserver, true = lists:member(Server, ?MYHOSTS), @@ -1832,7 +1828,7 @@ set_form(From, Host, ?NS_ADMINL(<<"delete-user">>), XData), [_ | _] = AccountStringList, ASL2 = lists:map(fun (AccountString) -> - JID = jlib:string_to_jid(AccountString), + JID = jid:from_string(AccountString), User = JID#jid.luser, Server = JID#jid.lserver, true = Server == Host orelse @@ -1847,7 +1843,7 @@ set_form(From, Host, ?NS_ADMINL(<<"delete-user">>), set_form(From, Host, ?NS_ADMINL(<<"end-user-session">>), Lang, XData) -> AccountString = get_value(<<"accountjid">>, XData), - JID = jlib:string_to_jid(AccountString), + JID = jid:from_string(AccountString), LUser = JID#jid.luser, LServer = JID#jid.lserver, true = LServer == Host orelse @@ -1873,7 +1869,7 @@ set_form(From, Host, ?NS_ADMINL(<<"end-user-session">>), set_form(From, Host, ?NS_ADMINL(<<"get-user-password">>), Lang, XData) -> AccountString = get_value(<<"accountjid">>, XData), - JID = jlib:string_to_jid(AccountString), + JID = jid:from_string(AccountString), User = JID#jid.luser, Server = JID#jid.lserver, true = Server == Host orelse @@ -1893,7 +1889,7 @@ set_form(From, Host, ?NS_ADMINL(<<"change-user-password">>), _Lang, XData) -> AccountString = get_value(<<"accountjid">>, XData), Password = get_value(<<"password">>, XData), - JID = jlib:string_to_jid(AccountString), + JID = jid:from_string(AccountString), User = JID#jid.luser, Server = JID#jid.lserver, true = Server == Host orelse @@ -1904,7 +1900,7 @@ set_form(From, Host, set_form(From, Host, ?NS_ADMINL(<<"get-user-lastlogin">>), Lang, XData) -> AccountString = get_value(<<"accountjid">>, XData), - JID = jlib:string_to_jid(AccountString), + JID = jid:from_string(AccountString), User = JID#jid.luser, Server = JID#jid.lserver, true = Server == Host orelse @@ -1913,7 +1909,6 @@ set_form(From, Host, Server) of [] -> - _US = {User, Server}, case get_last_info(User, Server) of not_found -> ?T(Lang, <<"Never">>); {ok, Timestamp, _Status} -> @@ -1940,7 +1935,7 @@ set_form(From, Host, set_form(From, Host, ?NS_ADMINL(<<"user-stats">>), Lang, XData) -> AccountString = get_value(<<"accountjid">>, XData), - JID = jlib:string_to_jid(AccountString), + JID = jid:from_string(AccountString), User = JID#jid.luser, Server = JID#jid.lserver, true = Server == Host orelse @@ -2023,17 +2018,17 @@ stop_node(From, Host, ENode, Action, XData) -> #xmlel{name = <<"x">>, attrs = [{<<"xmlns">>, - <<"jabber:x:data">>}, + ?NS_XDATA}, {<<"type">>, <<"submit">>}], children = SubEls}, others = [#xmlel{name = <<"x">>, attrs = [{<<"xmlns">>, - <<"jabber:x:data">>}, + ?NS_XDATA}, {<<"type">>, <<"submit">>}], children = SubEls}]}, - To = jlib:make_jid(<<"">>, Host, <<"">>), + To = jid:make(<<"">>, Host, <<"">>), mod_announce:announce_commands(empty, From, To, Request) end, Time = timer:seconds(Delay), @@ -2149,3 +2144,5 @@ set_sm_form(User, Server, <<"config">>, end; set_sm_form(_User, _Server, _Node, _Request, _Fields) -> {error, ?ERR_SERVICE_UNAVAILABLE}. + +mod_opt_type(_) -> []. diff --git a/src/mod_configure2.erl b/src/mod_configure2.erl index b70f8fe5..0acd4a78 100644 --- a/src/mod_configure2.erl +++ b/src/mod_configure2.erl @@ -5,7 +5,7 @@ %%% Created : 26 Oct 2003 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,11 +25,14 @@ -module(mod_configure2). +-behaviour(ejabberd_config). + -author('alexey@process-one.net'). -behaviour(gen_mod). --export([start/2, stop/1, process_local_iq/3]). +-export([start/2, stop/1, process_local_iq/3, + mod_opt_type/1, opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -62,7 +65,7 @@ process_local_iq(From, To, set -> IQ#iq{type = error, sub_el = [SubEl, ?ERR_FEATURE_NOT_IMPLEMENTED]}; - %%case xml:get_tag_attr_s("type", SubEl) of + %%case fxml:get_tag_attr_s("type", SubEl) of %% "cancel" -> %% IQ#iq{type = result, %% sub_el = [{xmlelement, "query", @@ -76,7 +79,7 @@ process_local_iq(From, To, %% _ -> %% Node = %% string:tokens( - %% xml:get_tag_attr_s("node", SubEl), + %% fxml:get_tag_attr_s("node", SubEl), %% "/"), %% case set_form(Node, Lang, XData) of %% {result, Res} -> @@ -183,8 +186,7 @@ process_get(#xmlel{name = <<"last">>, attrs = Attrs}) -> {'EXIT', _Reason} -> {error, ?ERR_INTERNAL_SERVER_ERROR}; Vals -> - {MegaSecs, Secs, _MicroSecs} = now(), - TimeStamp = MegaSecs * 1000000 + Secs, + TimeStamp = p1_time_compat:system_time(seconds), Str = list_to_binary( [[jlib:integer_to_binary(TimeStamp - V), <<" ">>] || V <- Vals]), @@ -195,3 +197,21 @@ process_get(#xmlel{name = <<"last">>, attrs = Attrs}) -> %%process_get({xmlelement, Name, Attrs, SubEls}) -> %% {result, }; process_get(_) -> {error, ?ERR_BAD_REQUEST}. + +mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; +mod_opt_type(_) -> [iqdisc]. + +opt_type(registration_watchers) -> + fun (JIDs) when is_list(JIDs) -> + lists:map(fun (J) -> + #xmlel{name = <<"jid">>, attrs = [], + children = + [{xmlcdata, iolist_to_binary(J)}]} + end, + JIDs) + end; +opt_type(welcome_message) -> + fun ({Subj, Body}) -> + {iolist_to_binary(Subj), iolist_to_binary(Body)} + end; +opt_type(_) -> [registration_watchers, welcome_message]. diff --git a/src/mod_disco.erl b/src/mod_disco.erl index 00b65d23..734e90d3 100644 --- a/src/mod_disco.erl +++ b/src/mod_disco.erl @@ -5,7 +5,7 @@ %%% Created : 1 Jan 2003 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -27,6 +27,9 @@ -author('alexey@process-one.net'). +-protocol({xep, 30, '2.4'}). +-protocol({xep, 157, '1.0'}). + -behaviour(gen_mod). -export([start/2, stop/1, process_local_iq_items/3, @@ -36,7 +39,7 @@ get_sm_identity/5, get_sm_features/5, get_sm_items/5, get_info/5, register_feature/2, unregister_feature/2, register_extra_domain/2, unregister_extra_domain/2, - transform_module_options/1]). + transform_module_options/1, mod_opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -149,7 +152,7 @@ process_local_iq_items(From, To, set -> IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; get -> - Node = xml:get_tag_attr_s(<<"node">>, SubEl), + Node = fxml:get_tag_attr_s(<<"node">>, SubEl), Host = To#jid.lserver, case ejabberd_hooks:run_fold(disco_local_items, Host, empty, [From, To, Node, Lang]) @@ -177,7 +180,7 @@ process_local_iq_info(From, To, IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; get -> Host = To#jid.lserver, - Node = xml:get_tag_attr_s(<<"node">>, SubEl), + Node = fxml:get_tag_attr_s(<<"node">>, SubEl), Identity = ejabberd_hooks:run_fold(disco_local_identity, Host, [], [From, To, Node, Lang]), Info = ejabberd_hooks:run_fold(disco_info, Host, [], @@ -302,7 +305,7 @@ process_sm_iq_items(From, To, case is_presence_subscribed(From, To) of true -> Host = To#jid.lserver, - Node = xml:get_tag_attr_s(<<"node">>, SubEl), + Node = fxml:get_tag_attr_s(<<"node">>, SubEl), case ejabberd_hooks:run_fold(disco_sm_items, Host, empty, [From, To, Node, Lang]) of @@ -375,10 +378,12 @@ process_sm_iq_info(From, To, case is_presence_subscribed(From, To) of true -> Host = To#jid.lserver, - Node = xml:get_tag_attr_s(<<"node">>, SubEl), + Node = fxml:get_tag_attr_s(<<"node">>, SubEl), Identity = ejabberd_hooks:run_fold(disco_sm_identity, Host, [], [From, To, Node, Lang]), + Info = ejabberd_hooks:run_fold(disco_info, Host, [], + [From, To, Node, Lang]), case ejabberd_hooks:run_fold(disco_sm_features, Host, empty, [From, To, Node, Lang]) of @@ -394,7 +399,7 @@ process_sm_iq_info(From, To, [{<<"xmlns">>, ?NS_DISCO_INFO} | ANode], children = - Identity ++ + Identity ++ Info ++ features_to_xml(Features)}]}; {error, Error} -> IQ#iq{type = error, sub_el = [SubEl, Error]} @@ -516,3 +521,18 @@ values_to_xml(Values) -> children = [{xmlcdata, Value}]} end, Values). + +mod_opt_type(extra_domains) -> + fun (Hs) -> [iolist_to_binary(H) || H <- Hs] end; +mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; +mod_opt_type(server_info) -> + fun (L) -> + lists:map(fun (Opts) -> + Mods = proplists:get_value(modules, Opts, all), + Name = proplists:get_value(name, Opts, <<>>), + URLs = proplists:get_value(urls, Opts, []), + {Mods, Name, URLs} + end, + L) + end; +mod_opt_type(_) -> [extra_domains, iqdisc, server_info]. diff --git a/src/mod_echo.erl b/src/mod_echo.erl index 63dc8c81..7184ee4e 100644 --- a/src/mod_echo.erl +++ b/src/mod_echo.erl @@ -5,7 +5,7 @@ %%% Created : 15 Jan 2003 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -35,9 +35,9 @@ -export([start_link/2, start/2, stop/1, do_client_version/3]). -%% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, - handle_info/2, terminate/2, code_change/3]). + handle_info/2, terminate/2, code_change/3, + mod_opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -86,7 +86,7 @@ stop(Host) -> init([Host, Opts]) -> MyHost = gen_mod:get_opt_host(Host, Opts, <<"echo.@HOST@">>), - ejabberd_router:register_route(MyHost), + ejabberd_router:register_route(MyHost, Host), {ok, #state{host = MyHost}}. %%-------------------------------------------------------------------- @@ -167,7 +167,7 @@ code_change(_OldVsn, State, _Extra) -> {ok, State}. %% Finally, the received response is printed in the ejabberd log file. do_client_version(disabled, _From, _To) -> ok; do_client_version(enabled, From, To) -> - ToS = jlib:jid_to_string(To), + ToS = jid:to_string(To), Random_resource = iolist_to_binary(integer_to_list(random:uniform(100000))), From2 = From#jid{resource = Random_resource, @@ -182,7 +182,7 @@ do_client_version(enabled, From, To) -> Els = receive {route, To, From2, IQ} -> #xmlel{name = <<"query">>, children = List} = - xml:get_subtag(IQ, <<"query">>), + fxml:get_subtag(IQ, <<"query">>), List after 5000 -> % Timeout in miliseconds: 5 seconds [] @@ -196,3 +196,6 @@ do_client_version(enabled, From, To) -> Values_string2 = iolist_to_binary(Values_string1), ?INFO_MSG("Information of the client: ~s~s", [ToS, Values_string2]). + +mod_opt_type(host) -> fun iolist_to_binary/1; +mod_opt_type(_) -> [host]. diff --git a/src/mod_fail2ban.erl b/src/mod_fail2ban.erl index 63c09db2..fed10670 100644 --- a/src/mod_fail2ban.erl +++ b/src/mod_fail2ban.erl @@ -1,12 +1,12 @@ %%%------------------------------------------------------------------- %%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> -%%% @copyright (C) 2014, Evgeny Khramtsov %%% @doc %%% %%% @end %%% Created : 15 Aug 2014 by Evgeny Khramtsov <ekhramtsov@process-one.net> %%% -%%% ejabberd, Copyright (C) 2014-2015 ProcessOne +%%% +%%% ejabberd, Copyright (C) 2014-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -31,9 +31,9 @@ %% API -export([start_link/2, start/2, stop/1, c2s_auth_result/4, check_bl_c2s/3]). -%% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). +-export([init/1, handle_call/3, handle_cast/2, + handle_info/2, terminate/2, code_change/3, + mod_opt_type/1]). -include_lib("stdlib/include/ms_transform.hrl"). -include("ejabberd.hrl"). @@ -65,7 +65,7 @@ c2s_auth_result(false, _User, LServer, {Addr, _Port}) -> LServer, ?MODULE, c2s_max_auth_failures, fun(I) when is_integer(I), I > 0 -> I end, ?C2S_MAX_AUTH_FAILURES), - UnbanTS = unban_timestamp(BanLifetime), + UnbanTS = p1_time_compat:system_time(seconds) + BanLifetime, case ets:lookup(failed_auth, Addr) of [{Addr, N, _, _}] -> ets:insert(failed_auth, {Addr, N+1, UnbanTS, MaxFailures}); @@ -79,11 +79,11 @@ c2s_auth_result(true, _User, _Server, _AddrPort) -> check_bl_c2s(_Acc, Addr, Lang) -> case ets:lookup(failed_auth, Addr) of [{Addr, N, TS, MaxFailures}] when N >= MaxFailures -> - case TS > now() of + case TS > p1_time_compat:system_time(seconds) of true -> IP = jlib:ip_to_list(Addr), UnbanDate = format_date( - calendar:now_to_universal_time(TS)), + calendar:now_to_universal_time(seconds_to_now(TS))), LogReason = io_lib:fwrite( "Too many (~p) failed authentications " "from this IP address (~s). The address " @@ -139,7 +139,7 @@ handle_cast(_Msg, State) -> handle_info(clean, State) -> ?DEBUG("cleaning ~p ETS table", [failed_auth]), - Now = now(), + Now = p1_time_compat:system_time(seconds), ets:select_delete( failed_auth, ets:fun2ms(fun({_, _, UnbanTS, _}) -> UnbanTS =< Now end)), @@ -171,11 +171,6 @@ is_whitelisted(Host, Addr) -> none), acl:match_rule(Host, Access, Addr) == allow. -unban_timestamp(BanLifetime) -> - {MegaSecs, MSecs, USecs} = now(), - UnbanSecs = MegaSecs * 1000000 + MSecs + BanLifetime, - {UnbanSecs div 1000000, UnbanSecs rem 1000000, USecs}. - is_loaded_at_other_hosts(Host) -> lists:any( fun(VHost) when VHost == Host -> @@ -184,6 +179,18 @@ is_loaded_at_other_hosts(Host) -> gen_mod:is_loaded(VHost, ?MODULE) end, ?MYHOSTS). +seconds_to_now(Secs) -> + {Secs div 1000000, Secs rem 1000000, 0}. + format_date({{Year, Month, Day}, {Hour, Minute, Second}}) -> io_lib:format("~2..0w:~2..0w:~2..0w ~2..0w.~2..0w.~4..0w", [Hour, Minute, Second, Day, Month, Year]). + +mod_opt_type(access) -> + fun (A) when is_atom(A) -> A end; +mod_opt_type(c2s_auth_ban_lifetime) -> + fun (T) when is_integer(T), T > 0 -> T end; +mod_opt_type(c2s_max_auth_failures) -> + fun (I) when is_integer(I), I > 0 -> I end; +mod_opt_type(_) -> + [access, c2s_auth_ban_lifetime, c2s_max_auth_failures]. diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl new file mode 100644 index 00000000..3b7a09cb --- /dev/null +++ b/src/mod_http_api.erl @@ -0,0 +1,392 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_http_api.erl +%%% Author : Christophe romain <christophe.romain@process-one.net> +%%% Purpose : Implements REST API for ejabberd using JSON data +%%% Created : 15 Sep 2014 by Christophe Romain <christophe.romain@process-one.net> +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2016 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. +%%% +%%%---------------------------------------------------------------------- + +%% Example config: +%% +%% in ejabberd_http listener +%% request_handlers: +%% "/api": mod_http_api +%% +%% Access rights are defined with: +%% commands_admin_access: configure +%% commands: +%% - add_commands: user +%% +%% +%% add_commands allow exporting a class of commands, from +%% open: methods is not risky and can be called by without any access check +%% restricted (default): the same, but will appear only in ejabberdctl list. +%% admin – auth is required with XMLRPC and HTTP API and checked for admin priviledges, works as usual in ejabberdctl. +%% user - can be used through XMLRPC and HTTP API, even by user. Only admin can use the commands for other users. +%% +%% Then to perform an action, send a POST request to the following URL: +%% http://localhost:5280/api/<call_name> + +-module(mod_http_api). + +-author('cromain@process-one.net'). + +-behaviour(gen_mod). + +-export([start/2, stop/1, process/2, mod_opt_type/1]). + +-include("ejabberd.hrl"). +-include("jlib.hrl"). +-include("logger.hrl"). +-include("ejabberd_http.hrl"). + +-define(CT_PLAIN, + {<<"Content-Type">>, <<"text/plain">>}). + +-define(CT_XML, + {<<"Content-Type">>, <<"text/xml; charset=utf-8">>}). + +-define(CT_JSON, + {<<"Content-Type">>, <<"application/json">>}). + +-define(AC_ALLOW_ORIGIN, + {<<"Access-Control-Allow-Origin">>, <<"*">>}). + +-define(AC_ALLOW_METHODS, + {<<"Access-Control-Allow-Methods">>, + <<"GET, POST, OPTIONS">>}). + +-define(AC_ALLOW_HEADERS, + {<<"Access-Control-Allow-Headers">>, + <<"Content-Type">>}). + +-define(AC_MAX_AGE, + {<<"Access-Control-Max-Age">>, <<"86400">>}). + +-define(OPTIONS_HEADER, + [?CT_PLAIN, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_METHODS, + ?AC_ALLOW_HEADERS, ?AC_MAX_AGE]). + +-define(HEADER(CType), + [CType, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_HEADERS]). + +%% ------------------- +%% Module control +%% ------------------- + +start(_Host, _Opts) -> + ok. + +stop(_Host) -> + ok. + + +%% ---------- +%% basic auth +%% ---------- + +check_permissions(#request{auth = HTTPAuth, headers = Headers}, Command) + when HTTPAuth /= undefined -> + case catch binary_to_existing_atom(Command, utf8) of + Call when is_atom(Call) -> + Admin = + case lists:keysearch(<<"X-Admin">>, 1, Headers) of + {value, {_, <<"true">>}} -> true; + _ -> false + end, + Auth = + case HTTPAuth of + {SJID, Pass} -> + case jid:from_string(SJID) of + #jid{user = User, server = Server} -> + case ejabberd_auth:check_password(User, <<"">>, Server, Pass) of + true -> {ok, {User, Server, Pass, Admin}}; + false -> false + end; + _ -> + false + end; + {oauth, Token, _} -> + case ejabberd_oauth:check_token(Command, Token) of + {ok, User, Server} -> + {ok, {User, Server, {oauth, Token}, Admin}}; + false -> + false + end; + _ -> + false + end, + case Auth of + {ok, A} -> {allowed, Call, A}; + _ -> unauthorized_response() + end; + _ -> + unauthorized_response() + end; +check_permissions(_, _Command) -> + unauthorized_response(). + +%% ------------------ +%% command processing +%% ------------------ + +process(_, #request{method = 'POST', data = <<>>}) -> + ?DEBUG("Bad Request: no data", []), + badrequest_response(); +process([Call], #request{method = 'POST', data = Data, ip = IP} = Req) -> + try + Args = case jiffy:decode(Data) of + List when is_list(List) -> List; + {List} when is_list(List) -> List; + Other -> [Other] + end, + log(Call, Args, IP), + case check_permissions(Req, Call) of + {allowed, Cmd, Auth} -> + {Code, Result} = handle(Cmd, Auth, Args), + json_response(Code, jiffy:encode(Result)); + ErrorResponse -> + ErrorResponse + end + catch _:Error -> + ?DEBUG("Bad Request: ~p", [Error]), + badrequest_response() + end; +process([Call], #request{method = 'GET', q = Data, ip = IP} = Req) -> + try + Args = case Data of + [{nokey, <<>>}] -> []; + _ -> Data + end, + log(Call, Args, IP), + case check_permissions(Req, Call) of + {allowed, Cmd, Auth} -> + {Code, Result} = handle(Cmd, Auth, Args), + json_response(Code, jiffy:encode(Result)); + ErrorResponse -> + ErrorResponse + end + catch _:Error -> + ?DEBUG("Bad Request: ~p", [Error]), + badrequest_response() + end; +process([], #request{method = 'OPTIONS', data = <<>>}) -> + {200, ?OPTIONS_HEADER, []}; +process(_Path, Request) -> + ?DEBUG("Bad Request: no handler ~p", [Request]), + badrequest_response(). + +%% ---------------- +%% command handlers +%% ---------------- + +% generic ejabberd command handler +handle(Call, Auth, Args) when is_atom(Call), is_list(Args) -> + case ejabberd_commands:get_command_format(Call, Auth) of + {ArgsSpec, _} when is_list(ArgsSpec) -> + Args2 = [{jlib:binary_to_atom(Key), Value} || {Key, Value} <- Args], + Spec = lists:foldr( + fun ({Key, binary}, Acc) -> + [{Key, <<>>}|Acc]; + ({Key, string}, Acc) -> + [{Key, <<>>}|Acc]; + ({Key, integer}, Acc) -> + [{Key, 0}|Acc]; + ({Key, {list, _}}, Acc) -> + [{Key, []}|Acc]; + ({Key, atom}, Acc) -> + [{Key, undefined}|Acc] + end, [], ArgsSpec), + handle2(Call, Auth, match(Args2, Spec)); + {error, Msg} -> + {400, Msg}; + _Error -> + {400, <<"Error">>} + end. + +handle2(Call, Auth, Args) when is_atom(Call), is_list(Args) -> + {ArgsF, _ResultF} = ejabberd_commands:get_command_format(Call, Auth), + ArgsFormatted = format_args(Args, ArgsF), + case ejabberd_command(Auth, Call, ArgsFormatted, 400) of + 0 -> {200, <<"OK">>}; + 1 -> {500, <<"500 Internal server error">>}; + 400 -> {400, <<"400 Bad Request">>}; + 404 -> {404, <<"404 Not found">>}; + Res -> format_command_result(Call, Auth, Res) + end. + +get_elem_delete(A, L) -> + case proplists:get_all_values(A, L) of + [Value] -> {Value, proplists:delete(A, L)}; + [_, _ | _] -> + %% Crash reporting the error + exit({duplicated_attribute, A, L}); + [] -> + %% Report the error and then force a crash + exit({attribute_not_found, A, L}) + end. + +format_args(Args, ArgsFormat) -> + {ArgsRemaining, R} = lists:foldl(fun ({ArgName, + ArgFormat}, + {Args1, Res}) -> + {ArgValue, Args2} = + get_elem_delete(ArgName, + Args1), + Formatted = format_arg(ArgValue, + ArgFormat), + {Args2, Res ++ [Formatted]} + end, + {Args, []}, ArgsFormat), + case ArgsRemaining of + [] -> R; + L when is_list(L) -> exit({additional_unused_args, L}) + end. + +format_arg({array, Elements}, + {list, {ElementDefName, ElementDefFormat}}) + when is_list(Elements) -> + lists:map(fun ({struct, [{ElementName, ElementValue}]}) when + ElementDefName == ElementName -> + format_arg(ElementValue, ElementDefFormat) + end, + Elements); +format_arg({array, [{struct, Elements}]}, + {list, {ElementDefName, ElementDefFormat}}) + when is_list(Elements) -> + lists:map(fun ({ElementName, ElementValue}) -> + true = ElementDefName == ElementName, + format_arg(ElementValue, ElementDefFormat) + end, + Elements); +format_arg({array, [{struct, Elements}]}, + {tuple, ElementsDef}) + when is_list(Elements) -> + FormattedList = format_args(Elements, ElementsDef), + list_to_tuple(FormattedList); +format_arg({array, Elements}, {list, ElementsDef}) + when is_list(Elements) and is_atom(ElementsDef) -> + [format_arg(Element, ElementsDef) + || Element <- Elements]; +format_arg(Arg, integer) when is_integer(Arg) -> Arg; +format_arg(Arg, binary) when is_list(Arg) -> process_unicode_codepoints(Arg); +format_arg(Arg, binary) when is_binary(Arg) -> Arg; +format_arg(Arg, string) when is_list(Arg) -> process_unicode_codepoints(Arg); +format_arg(Arg, string) when is_binary(Arg) -> Arg; +format_arg(undefined, binary) -> <<>>; +format_arg(undefined, string) -> <<>>; +format_arg(Arg, Format) -> + ?ERROR_MSG("don't know how to format Arg ~p for format ~p", [Arg, Format]), + error. + +process_unicode_codepoints(Str) -> + iolist_to_binary(lists:map(fun(X) when X > 255 -> unicode:characters_to_binary([X]); + (Y) -> Y + end, Str)). + +%% ---------------- +%% internal helpers +%% ---------------- + +match(Args, Spec) -> + [{Key, proplists:get_value(Key, Args, Default)} || {Key, Default} <- Spec]. + +ejabberd_command(Auth, Cmd, Args, Default) -> + case catch ejabberd_commands:execute_command(undefined, Auth, Cmd, Args) of + {'EXIT', _} -> Default; + {error, _} -> Default; + Result -> Result + end. + +format_command_result(Cmd, Auth, Result) -> + {_, ResultFormat} = ejabberd_commands:get_command_format(Cmd, Auth), + case {ResultFormat, Result} of + {{_, rescode}, V} when V == true; V == ok -> + {200, <<"">>}; + {{_, rescode}, _} -> + {500, <<"">>}; + {{_, restuple}, {V1, Text1}} when V1 == true; V1 == ok -> + {200, iolist_to_binary(Text1)}; + {{_, restuple}, {_, Text2}} -> + {500, iolist_to_binary(Text2)}; + {{_, {list, _}}, _V} -> + {_, L} = format_result(Result, ResultFormat), + {200, L}; + {{_, {tuple, _}}, _V} -> + {_, T} = format_result(Result, ResultFormat), + {200, T}; + _ -> + {200, {[format_result(Result, ResultFormat)]}} + end. + +format_result(Atom, {Name, atom}) -> + {jlib:atom_to_binary(Name), jlib:atom_to_binary(Atom)}; + +format_result(Int, {Name, integer}) -> + {jlib:atom_to_binary(Name), Int}; + +format_result(String, {Name, string}) -> + {jlib:atom_to_binary(Name), iolist_to_binary(String)}; + +format_result(Code, {Name, rescode}) -> + {jlib:atom_to_binary(Name), Code == true orelse Code == ok}; + +format_result({Code, Text}, {Name, restuple}) -> + {jlib:atom_to_binary(Name), + {[{<<"res">>, Code == true orelse Code == ok}, + {<<"text">>, iolist_to_binary(Text)}]}}; + +format_result(Els, {Name, {list, {_, {tuple, [{_, atom}, _]}} = Fmt}}) -> + {jlib:atom_to_binary(Name), {[format_result(El, Fmt) || El <- Els]}}; + +format_result(Els, {Name, {list, Def}}) -> + {jlib:atom_to_binary(Name), [element(2, format_result(El, Def)) || El <- Els]}; + +format_result(Tuple, {_Name, {tuple, [{_, atom}, ValFmt]}}) -> + {Name2, Val} = Tuple, + {_, Val2} = format_result(Val, ValFmt), + {jlib:atom_to_binary(Name2), Val2}; + +format_result(Tuple, {Name, {tuple, Def}}) -> + Els = lists:zip(tuple_to_list(Tuple), Def), + {jlib:atom_to_binary(Name), {[format_result(El, ElDef) || {El, ElDef} <- Els]}}; + +format_result(404, {_Name, _}) -> + "not_found". + +unauthorized_response() -> + {401, ?HEADER(?CT_XML), + #xmlel{name = <<"h1">>, attrs = [], + children = [{xmlcdata, <<"401 Unauthorized">>}]}}. + +badrequest_response() -> + {400, ?HEADER(?CT_XML), + #xmlel{name = <<"h1">>, attrs = [], + children = [{xmlcdata, <<"400 Bad Request">>}]}}. +json_response(Code, Body) when is_integer(Code) -> + {Code, ?HEADER(?CT_JSON), Body}. + +log(Call, Args, {Addr, Port}) -> + AddrS = jlib:ip_to_list({Addr, Port}), + ?INFO_MSG("Admin call ~s ~p from ~s:~p", [Call, Args, AddrS, Port]). + +mod_opt_type(access) -> + fun(Access) when is_atom(Access) -> Access end; +mod_opt_type(_) -> [access]. diff --git a/src/mod_http_bind.erl b/src/mod_http_bind.erl index 773ef241..1a07867e 100644 --- a/src/mod_http_bind.erl +++ b/src/mod_http_bind.erl @@ -5,7 +5,7 @@ %%% Created : Tue Feb 20 13:15:52 CET 2007 %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -37,7 +37,7 @@ -behaviour(gen_mod). --export([start/2, stop/1, process/2]). +-export([start/2, stop/1, process/2, mod_opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -64,9 +64,9 @@ process([], #request{method = 'POST', data = <<>>}) -> {400, ?HEADER, #xmlel{name = <<"h1">>, children = [{xmlcdata, <<"400 Bad Request">>}]}}; process([], - #request{method = 'POST', data = Data, ip = IP}) -> + #request{method = 'POST', data = Data, ip = IP, opts = Opts}) -> ?DEBUG("Incoming data: ~s", [Data]), - ejabberd_http_bind:process_request(Data, IP); + ejabberd_http_bind:process_request(Data, IP, Opts); process([], #request{method = 'GET', data = <<>>}) -> {200, ?HEADER, get_human_html_xmlel()}; process([], #request{method = 'OPTIONS', data = <<>>}) -> @@ -78,54 +78,14 @@ process(_Path, _Request) -> {400, ?HEADER, #xmlel{name = <<"h1">>, children = [{xmlcdata, <<"400 Bad Request">>}]}}. -get_human_html_xmlel() -> - Heading = <<"ejabberd ", - (iolist_to_binary(atom_to_list(?MODULE)))/binary>>, - #xmlel{name = <<"html">>, - attrs = - [{<<"xmlns">>, <<"http://www.w3.org/1999/xhtml">>}], - children = - [#xmlel{name = <<"head">>, - children = - [#xmlel{name = <<"title">>, - children = [{xmlcdata, Heading}]}]}, - #xmlel{name = <<"body">>, - children = - [#xmlel{name = <<"h1">>, - children = [{xmlcdata, Heading}]}, - #xmlel{name = <<"p">>, - children = - [{xmlcdata, <<"An implementation of ">>}, - #xmlel{name = <<"a">>, - attrs = - [{<<"href">>, - <<"http://xmpp.org/extensions/xep-0206.html">>}], - children = - [{xmlcdata, - <<"XMPP over BOSH (XEP-0206)">>}]}]}, - #xmlel{name = <<"p">>, - children = - [{xmlcdata, - <<"This web page is only informative. To " - "use HTTP-Bind you need a Jabber/XMPP " - "client that supports it.">>}]}]}]}. - %%%---------------------------------------------------------------------- %%% BEHAVIOUR CALLBACKS %%%---------------------------------------------------------------------- -start(Host, _Opts) -> - setup_database(), - Proc = gen_mod:get_module_proc(Host, ?PROCNAME_MHB), - ChildSpec = {Proc, - {ejabberd_tmp_sup, start_link, - [Proc, ejabberd_http_bind]}, - permanent, infinity, supervisor, [ejabberd_tmp_sup]}, - supervisor:start_child(ejabberd_sup, ChildSpec). - -stop(Host) -> - Proc = gen_mod:get_module_proc(Host, ?PROCNAME_MHB), - supervisor:terminate_child(ejabberd_sup, Proc), - supervisor:delete_child(ejabberd_sup, Proc). +start(_Host, _Opts) -> + setup_database(). + +stop(_Host) -> + ok. setup_database() -> migrate_database(), @@ -142,3 +102,136 @@ migrate_database() -> %% of actually migrating data, let's just destroy the table mnesia:delete_table(http_bind) end. + +mod_opt_type(max_inactivity) -> + fun (I) when is_integer(I), I > 0 -> I end; +mod_opt_type(max_pause) -> + fun (I) when is_integer(I), I > 0 -> I end; +mod_opt_type(_) -> [max_inactivity, max_pause]. + + +%%%---------------------------------------------------------------------- +%%% Help Web Page +%%%---------------------------------------------------------------------- + +get_human_html_xmlel() -> + Heading = <<"ejabberd ", + (iolist_to_binary(atom_to_list(?MODULE)))/binary>>, + #xmlel{name = <<"html">>, + attrs = + [{<<"xmlns">>, <<"http://www.w3.org/1999/xhtml">>}], + children = + [#xmlel{name = <<"head">>, + children = + [#xmlel{name = <<"title">>, + children = [{xmlcdata, Heading}]}, + #xmlel{name = <<"style">>, + children = [{xmlcdata, get_style_cdata()}]}]}, + #xmlel{name = <<"body">>, + children = + [#xmlel{name = <<"div">>, + attrs = [{<<"class">>, <<"container">>}], + children = get_container_children(Heading)}]}]}. + +get_container_children(Heading) -> + [#xmlel{name = <<"div">>, + attrs = [{<<"class">>, <<"section">>}], + children = + [#xmlel{name = <<"div">>, + attrs = [{<<"class">>, <<"block">>}], + children = + [#xmlel{name = <<"a">>, + attrs = [{<<"href">>, <<"https://www.ejabberd.im">>}], + children = + [#xmlel{name = <<"img">>, + attrs = [{<<"height">>, <<"32">>}, + {<<"src">>, get_image_src()}]}]}]}]}, + #xmlel{name = <<"div">>, + attrs = [{<<"class">>, <<"white section">>}], + children = + [#xmlel{name = <<"div">>, + attrs = [{<<"class">>, <<"block">>}], + children = + [#xmlel{name = <<"h1">>, children = [{xmlcdata, Heading}]}, + #xmlel{name = <<"p">>, children = + [{xmlcdata, <<"An implementation of ">>}, + #xmlel{name = <<"a">>, + attrs = [{<<"href">>, <<"http://xmpp.org/extensions/xep-0206.html">>}], + children = [{xmlcdata, <<"XMPP over BOSH (XEP-0206)">>}]}]}, + #xmlel{name = <<"p">>, children = + [{xmlcdata, <<"This web page is only informative. To " + "use HTTP-Bind you need a Jabber/XMPP " + "client that supports it.">>}]}]}]}, + #xmlel{name = <<"div">>, + attrs = [{<<"class">>, <<"section">>}], + children = + [#xmlel{name = <<"div">>, + attrs = [{<<"class">>, <<"block">>}], + children = + [#xmlel{name = <<"a">>, + attrs = [{<<"href">>, <<"https://www.ejabberd.im">>}, + {<<"title">>, <<"ejabberd XMPP server">>}], + children = [{xmlcdata, <<"ejabberd">>}]}, + {xmlcdata, <<" is maintained by ">>}, + #xmlel{name = <<"a">>, + attrs = [{<<"href">>, <<"https://www.process-one.net">>}, + {<<"title">>, <<"ProcessOne - Leader in Instant Messaging and Push Solutions">>}], + children = [{xmlcdata, <<"ProcessOne">>}]} ]}]} + ]. + +get_style_cdata() -> + <<" + body { + margin: 0; + padding: 0; + font-family: sans-serif; + color: #fff; + } + h1 { + font-size: 3em; + color: #444; + } + p { + line-height: 1.5em; + color: #888; + } + a { + color: #fff; + } + a:hover, + a:active { + text-decoration: underline; + } + .container { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: #424A55; + background-image: -webkit-linear-gradient(270deg, rgba(48,52,62,0) 24%, #30353e 100%); + background-image: linear-gradient(-180deg, rgba(48,52,62,0) 24%, #30353e 100%); + } + .section { + padding: 3em; + } + .white.section { + background: #fff; + border-bottom: 4px solid #41AFCA; + } + .white.section a { + text-decoration: none; + color: #41AFCA; + } + .white.section a:hover, + .white.section a:active { + text-decoration: underline; + } + .block { + margin: 0 auto; + max-width: 900px; + width: 100%; + }">>. + +get_image_src() -> + <<"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAABACAYAAACgPErgAAAVtklEQVR42uydeXhU1d2ADzuyCcii4FZBZdEqVYtaUGqlikVwaRWsrWBRKIh8KgXcEAX9VECtoljcURAJICJWQLaKLAEjIHtBSNhC9oQkk2SW+36/52P+iIE5d5kzk4Fn3ud5H/7gIbnD3PPOveeeuVedygD9xLlQMh9yJ0D+GeokJAg1xcEWzOWYj0DBaQm0fXdaMCu8bROgpJVKksQj0+F84OXw/jRd7KVOdbbCFI6jdDNktlAnESVQNwSzOI6yhZBaV1UzAXiB4wj8CBtbqiRJXDIILtgL+zmOb8aoU5Xh0MMiEr5x6iSiAIZYROLo/aoayYMeocjbNlklSeKC9VAjBDM5IcEQfNZFnYq8AM8SkcJV6iSiCGYRkT1LVDWSA08Tkfy9MLaeOsUA2ot9xJHiW2KK+LW4Mvxnijg1/Pd9xQvFGiqJLUOhcT5kE5Etg0/VYI3TBOvbkyxYKURk2dpqDZb2gyEjC1SjUyBQdcQe4mtimujDHWXiD+Lr4g1iXZXkhISgBZBLZIaoU5GR0I2IVDxxkgVrNhH5z+pqDZb2g+FAJjQ4aYMFtBAfEzdils3iP8TkhYkqlMEZFuQQmcHqVGUBTOI40lLhz02TwUoGKxJAI/FxMYPYckB8Smyikvw/vlM/WHqAP4ifiHPF0fBsYyUkg5UM1okAbhV/JL5sE29TSZLBOhHJYCWDVRXgNPFVqpfXxQbJYCWDlQxWMlgRAc4VV5EYrBZ/kQxWMljJYFVLsBomdLCAS8RdJBZ7xMuSwTIcLKCO2EG8VrxdvEfsLV4F1vkJstq9BlidwN8hVleSLLgUuFzsEv6zs9g4EYIFNBAvFi87tm3WZWI7oHZ8glWnkWbb6ortCG/bsT+tDsBpcYrVpeIhzBHAHJni5XGOd5PwvnKJif0DaCh2/Pn7y/linbgFKx/qrjkWpffEHZo3qVhMBWuC2EVVA4ehay6sAYJgBeDocuh9iYqCL+F04DbxLTFNPMzxWGI6WCuBJ8Rfxi5YK34WLKC5BX3FVzj22n8S/fwcX/i9+1wcLraLVbAqr8OqgNOA31nwPLBc3HWCNU1+cbe4WBwjXhbD08A9eOcn8SNxmHij+EuxQ3iwdxP/Jr4pbsU7+2J5ehiCNuI9FkwN78sZol+0xDSYd6OH75R2BcZasALYKwZO0IWd4fd3LIS6g1XHeLBWQV1gsLgJ9wTBWgSBm1ScuBYuAvI4jrx9cOEZyiXzoUUIxhbDPm+fvFlL4PPe5oO14FslvAjnrYeXgYO4p0T8UrzJbLB2HlbCZMqbfwOjgrDD41HLCrE/LK1pKFYNxFS8sVDsK1Y5ctQeRf42HLcy3JMmNjIc6+ss+ETMRUuRH9Zd7SBS9QMwCFiDNzZBYAT4G5dTUcuC7KiClQ3dj8B6osYSAzOgoq2KMatgChF5e5RyQQXcVwL7MMNXsKOTuWAd2FBG9uhyyMMI6XPBd7GZYOUd8pP+UCn8hBEOr4KCbgYG7Ju4Z43YU0VB+LRoLu6ZpgyQAhflw2e4Y7lYS0XADzdZ8ANGsLaX88PgACHvwfLBY4Afo4T2Q9lNMZ68W0lE/J8oB5RDkxDMwDhF+XC4v6tgxZdcKLov+mBhYR4/FD8VRTRu93CE97RYTxkCuFfMxR13qSjIhoE+Tx9qPh9Mb6WqcBRqBuBli5hguQ4WUGsPvGsRKyw/hAaoGJEPy4lI4Uxlw3XHzu/XEVNKHk3QYAmWWDEecmraBqtaCE6HrCYeJpX34Jwc8eYYXp3cjHPSxWbKA9/Da3im+CeY1kBV4gNoUQILiB/6YD0JNYFP4jMoGFINwZqhNHwKLbfB5ji9/kdMBss8vk9MBcs8pasht7GLSIzHOQfFX6kYApyFuw/FFz38jveJitxBqhI3QMsS2IiQMMFaDFOJHyF45fZECdYOqA0sIX5YMPkOk8Eyz96xpoJlnvIZDgduazEfZ+SJVyoNhqO1DWcUim1dfJf2DTxTVgxFY1Ql+kOtivDYSJhgTYX7cIx1EJgmDhSvEbuK12XAiHyYCxThCKsQHmiXCMHa5+5T+BBY74qVX39vcUIhbLBwSkUO7DonBsEqBCtFfBjoHt6+34mjLfg34HO4feL8mw0HqwKsr8XRwA2E9x3xYY693jzHvWfZCAdheAZnWG6+1wecKd4oDhFHicPFvm6WI4SXRBTijOcdzi3+BefkifPAehLoI14Pqcdt/1x3YyNb/BisYUC38PvbU3wivOQhGHWw+sK5ASjAltBB8D8MQe05NXCBBZOBCmxJXwbTa1RnsDbCL4Pgx5ajeeJjYDVXEZgDNSugVwhW44j1sw0GKwjrX4El2kGzHDqVwzvO1x3d2dhMsPbMgjnaRZGroW0OjHcY1VIYeZHNMoZ0nPFP5YDwIJwp5miWinwt3u7w5z3g4lS1sc2c1Tl+Z0eTRXBkLKy1PWp7Ga4EAthSXAhHn4BQa5vX2zUIX1rRBKvU0fluYDaUt3F5xa5rNmzFlsfurs5grYP52JK7AVZcpBxSCrU3wvPOPtgzuxsI1gG3l9/90Cfg6Ihm/ONRBssn3udyceNVftiBLXNnaAZHbxcLNZs6uPXMFJdHCF84OeICluIIfQTzHI3jrI0Q7Kwc4myaxEqDtR2VC9bCCMDvOliZ0NGCCv1pwb7XlEdmQCtgOVp2bYU69aojWB/AVUEIoWcpfNlMeQD4q/0bs/GL6IK1JR3Wd/T4/lxZDJloKTgMrzXzFqyDJfBNT+WB2dD2kO1Eb9AP/zrhAAQ+xhl/d7A6fm0U977qbvPzr3YYwlkqArnQ2f4swb8SZjZXDtkG3SxsWSZ6HRt9RJ+rYP0H/omWdVNUlGyDxpm2b/i4P1VHsPbYnhrl7IQ/NlNRAPzDfqnH0o4eg1UEEy+N8mkl3e1P378e4CFYFnzRR0VBVzgP26BumqyqANQX92JPhm5FOXC2uJXoKBR7KA3AQuzZLzaMcJbwqv2pfXFL5YKV8BFaCvfCH1uqKDgKA0NOgzUAGuRp7664eaUyxD3Qpkz7u0oWxDtY/aBhFhzUD7hBN6goeRVq7IRVNkEY4y1YRx5VBiiHV9ByaK77YOW+pwxQCn9DS/4W2Fy7SgCuEEPYM1ETkXriSsxwRLxQe7RhjyV2VVW4E+ofhj1oSe2rXDAaGhTBAbQM72vo6VDzHQXrX9r7nxeXwqrLYWcNoFa0hj8F7rQiTuAV5cHIFvEM1jvQ3UJH+QJliN9Dj5B2APkWuw/Wrm3QrJ6hZyC20q/C9qfDsgbOg1WYDxe2NbRtdfTfZbXKIbW9x8ns30QISA1xEmbZLDbRzJEdwp6hqgqvwVWWdt9avVy5ZDL8Wj9VstbYwUwpdALKbIP1HxhJRAJlwPfhHWWzATeJafpz9bd/H89grYBRaHn+RmWQn2CdZtsyYWhjd8HaPVQZZKl2HV7AgvEXOw9W1pvKIDNhGFrevbVKACY6vPLWSHN0NUp8ThxnyJfECzRHWSlermaugfvRcuQu5ZLv4F60pN+lDLIZUmyDtRRmklAUPBzPYK2GT3Vf4IX+jZRBPoYniUioHA62dx6sQAVkt1MGSYXeFjoC3ZwFKyRm/14Z5Bu4OKidZwsOqjL4P8eeZSqBAMZgz0JVhQ3wOhEpLoLxZyqXHIAJRKS0CF48UxnkWehnG6wj8BUJxfK3VZQUuAiW/ntRwa+UYR6BP+gH+f6eLoK1CQprKoMchIv82itNGfc5DFY+FLUyvG0NS2E/Ecl8qcrg/w573kuwYP0Re1JVFd7RfvAWbYIU1/vJ2/AhETm6HlJqKIOshA4hKNMGK5RwwZrygYFHqC9zGqygNlgrP4vBjQW7hcCKHKy9tzh/kGrqd8ow+6CVX3tPovShKkw2PKuJx2G4tKEyyF6o4YPvicjhN1UlHD5TcGKCBetG7Nkq/ixA+doPNv9i5YFp2iuE1mJlmBxoEbR7kKqVcMGaOU1FyTpYq7tbg/NgbZ+tDLMLrrcJVi9ViTLtKfsq48E6CK392p0m/e8qTDE8RUQOHobmRoN1AGqWQZomWFMq33FE/C/2vJBgwboee/aKdVzMfXk6U3hLGyyMBysXWga1i5jTBqmchAvWY31VlGRqbyrmn64qka8NVsj4/MZg6IPgNFj7tV9iLfsRVtdSBsmATgEIaoJ/mwqzEwYTkWABbDhLGSQdGpdp12OVPF7lmQO7HU1gJxDAI9izs+oRVlAfrH8rDyzQXoAp3gxv1zJ8UaVTSDtHec9f1bqEmXQPiuUfwjdR/Se8AG0sOEpEUidVmax8n4iU58OKFoaPsJ5xE6x/wwT9pPuTRifdv4K79Nv3QZdK8yb90PLazcogH8HlIQgQkfl3ewjWpwn2yLH92JOmhFgHqwSGE5EyH7x/vjJICtyjX362s6f6Cf6hX9tSOB8Q+SJGfimmQPogZYCXoD9avh1WZYAORcvwfsogpbDeTbBmwW1oKR6mDJIGs4iIvxAeOLPSpGxn/dGYb6rhJRdPod2jn7nGQ7DWaQJSQzxLbC22iqFtxDvE7ThjRTyCNQ1uRkvqYGWQQzBH/yV3+R7zFOhhab8u8smt6iThN1AjB1brd+rnuqhKvAFX6hdz/rgBLqqpDJAHPS2w3ARrIpzth2IiEtgBM+spA2RBB/33unZ9qyrxDNTLg52a11MAS842dDrYRH/XhfxDMLyhh2AViK0jBKum+K5YKObE0CLc8WE8gvUStC7XbtuWrXBeHWWA7tDJp933dq+F8TXVHmgS1M4LFByE/q3VScB826OrwG7IqqsqcQvU3Q+70OJ7SEVJO6iVARsQHAcrTBosR8vrI5UBNsBctBQ+raowD6ag5ev3lQFmw3i0FM9SgiZYOiJ+KHMs4iUkFk+aC5aeLbAULYERygBB+BIt+8ZV3lHfQ8uRRaDqqASmF7Sxf8xV6SR1AjbBi+gphPIuUcb0aQQvwfoWBqDFKoEXuqooWAID0RLyw56OJ7j319WAhZZp/aOMVTegDC0ZfaII1sdKA9CPxOLWeAVrMgy0Hxu9ukS5NvEh+33v6Q6VT4uusCCElpnz4Ie6KgFZAWctgTS0lPjg2osiLDRtFwAfWoKHIfRrj/NCI0oAr8EqgAZ+2IOew7DkWuWBAPQvBT9ayuZo5uUWo8cH6z1FKwTXFkEOWio2Q1ndKIJVILa1XYGeGBSJbeIVrObQKBPS0VJ6ABZ7itY2eLAIO1JmqKqUwzzsWQyB9soFwGliQxUj/HCNs/tiH5hqM2hfxxarCPwPughBI80dEBwHK3wUOMDCjtKjbp5GlAn1vodxgIWeoO6hDGvhmqDtzygXA0/BN3VcPF/yAYfzO3crQRcsE+uxgIfEUqqXVUowGyw9I2EY9uRCxV0uls803AwTsacC3uisqjIEOgMV2GLlAeMgdK7NbW47Aq+Ke8UD4jsw4yyDj9ruIE6yoAxbyo/AjjOVhnehVQFk4ojQIgjeDNSNsGO3FP9mwVYEE8FqAbUOwwoccWAh+HuCVS/C9jUNwZ+BNByx9A0Hd4+ciiOOpELx3WBFumNB/RD0smARjti5FPrVNBCsQvECB9G6SvwaZ/xX/F/xf8S5YojoGRXvYM2C+j7nT8uZC9ZvI42NhdA8AAOALTgi5TkVic/gSRxjFQDzxKFiH7G7eJv4WAhWBqCc49i/Gya2US7hWPzuFf8iTrBgGVCOY4r/5HCupF8AV2wDazrwiDhEfEFcKGYRxkiwwlRAeyAb5+wQPxFHioPEseLn4gEcU7gdLm3q4PubTVw+ay9DTBGfCm/bKPFTcSfOyYe+FyrBQLAQPnfzyHdxSvg1F4hlYrGYHn5d94hNTnC/qxK8Uya2Mx8se0rhCsCHc7aCVXnfG2fB/Ao4hGPSV8O59VQkfgs1N8IiYkrBK8oFW+E5wIdndkx2eVXkeYxgPljh7bs1CCHigr8Ull7u4r26pBgKiBtb7lCCqWCFeVi5AKgtthE7iL8QGykNwGi8s0AJcQ9WmOnwIHEjlAFp5yg7noNmQCoxo2yNizmWoUG8Yon73oOzayiXAG8mZLDC/BkGlIBFTMkvg1v6eFhPc31mzKNVIf59hBJiECy/2FvFCOCcKObBesQjWNV/8aFiPxR0cbNRrcVVxITgauWAR6H+IdiNZxZPinJdyKSEDFaY5dAXyCY2HIJPeyqPvAlXANuJDT74/n4leA6WPUfFXjEKVjMxD/css/m5c7TBMsgmGBGI2VG+9T0UtvdyI7cme+EzjPPhROWACmiJp0/q8lzY/RdlgFUwDCjGGFZYXbDSb3Fx4aGzH77DKBmrIPdCFSUcWyU9B6Pk7ICMbkowFiz9fNFAZRjgGjGIOwLi1UrDBv0R1hJlmCLoFYIMs2Mj8D74m6poAIZbkGtmg/gIbmumHLID5uOK0GzIb294B7skCAssoiYLsh6FdWIk/D7Ycp7LZ7vV2Qejifo9CmYBo+Hb2oY/jQcC+4iKUAnwCmxvrgTjwdLzttjS0L7U3uOjwqYoG3ZqF19WvKVigA/aBGEaUEFUWFsh+CeTa50uCMAUC3LwRioEb1MueRw62a/DsXLE9yH4GxVD/HCjBfPEYlxxdDsUjgPrbCXMprTF/oiPnip9QnnEgnND8KwFu3DHdigfC4vaqhgBNA3CwxZsxB37xFdhUSelQRssM+wVh4iNPb7+RhwLyhHcs8PJM/+OHLv1ziaOw5cNmReoGAL8KgRvBWC/5e4IZh2EBomnqVgQgrYWDBUXApkRlhaUillgLRVfFm8AaiqPAN3ENWJ+2ExxJVjTIHQXBM9UcSQI51twLzBNXMSxQOSIeeEdchNYC469dq6HH+qd4DYpvwA+FPeIeeF/86Ch9+g0C34HTBC/FLeK2eHfkyWmiZ+LT4rXhddqxYUQ1LLgGmCMOF/8QcwKb1tOeFsXhNcu3QyWLhDmg2XPLvFZ8UpRO8iAVuL14nPiTrxRLnZVDjkC54TveJAfdjEcuUzFia/h9FLoDbwifhV+3XmVxsZ6cQ5Yj0LoChVPkI0TO4q9wLoDuFW8TrxAbGr4d9WwoAVwhni6SiCA+mIzsXn4/6S2i3/bIPzvasdw++pW2r6mYi2VIAC1wtvUPLyNdZUGz8EyTyj8s78KnzK+LL4kviPOE9eJeUTP/R5f+xlgnZEIY8OC5pXGRg2VJEkSTbBOXsaoJEmSJIN1EvC0SvJ/7dSxigEAHIDxf5FisAilbNbbWGSi5OZ7BgZPcS/hAZTNwFtYSVltit2iU/c9gxv8u75ffa/wSQ4ruR+ahySHldyZJiHJYSW3pnZIcliJHekrJDmsxMM60JQqIUkJh3WhJX1SKSTpzcN60J1udKQNfdOIaiFJLwxrTwua0Zj61PtjXfqgDtWpEJL04rCetKKBM5GUeVgnGoYkJR1Wka60o0ZIUuJhVWlLzZCk5MMqUyv0r/wCSDD/4sxS1q8AAAAASUVORK5CYII=">>. diff --git a/src/mod_http_fileserver.erl b/src/mod_http_fileserver.erl index 97738355..346dc41c 100644 --- a/src/mod_http_fileserver.erl +++ b/src/mod_http_fileserver.erl @@ -5,7 +5,7 @@ %%% Created : %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -43,8 +43,10 @@ %% request_handlers callbacks -export([process/2]). -%% ejabberd_hooks callbacks --export([reopen_log/1]). +%% utility for other http modules +-export([content_type/3]). + +-export([reopen_log/1, mod_opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -86,8 +88,6 @@ {<<".xpi">>, <<"application/x-xpinstall">>}, {<<".xul">>, <<"application/vnd.mozilla.xul+xml">>}]). --compile(export_all). - %%==================================================================== %% gen_mod callbacks %%==================================================================== @@ -168,10 +168,17 @@ initialize(Host, Opts) -> ?DEFAULT_CONTENT_TYPE), ContentTypes = build_list_content_types( gen_mod:get_opt(content_types, Opts, - fun(L) when is_list(L) -> L end, - []), + fun(L) when is_list(L) -> + lists:map( + fun({K, V}) -> + {iolist_to_binary(K), + iolist_to_binary(V)} + end, L) + end, []), ?DEFAULT_CONTENT_TYPES), - ?INFO_MSG("initialize: ~n ~p", [ContentTypes]),%+++ + ?INFO_MSG("known content types: ~s", + [str:join([[$*, K, " -> ", V] || {K, V} <- ContentTypes], + <<", ">>)]), {DocRoot, AccessLog, AccessLogFD, DirectoryIndices, CustomHeaders, DefaultContentType, ContentTypes}. @@ -448,3 +455,17 @@ ip_to_string(Address) when size(Address) == 4 -> ip_to_string(Address) when size(Address) == 8 -> Parts = lists:map(fun (Int) -> io_lib:format("~.16B", [Int]) end, tuple_to_list(Address)), string:to_lower(lists:flatten(join(Parts, ":"))). + +mod_opt_type(accesslog) -> fun iolist_to_binary/1; +mod_opt_type(content_types) -> + fun (L) when is_list(L) -> L end; +mod_opt_type(custom_headers) -> + fun (L) when is_list(L) -> L end; +mod_opt_type(default_content_type) -> + fun iolist_to_binary/1; +mod_opt_type(directory_indices) -> + fun (L) when is_list(L) -> L end; +mod_opt_type(docroot) -> fun (A) -> A end; +mod_opt_type(_) -> + [accesslog, content_types, custom_headers, + default_content_type, directory_indices, docroot]. diff --git a/src/mod_http_upload.erl b/src/mod_http_upload.erl new file mode 100644 index 00000000..6c029c43 --- /dev/null +++ b/src/mod_http_upload.erl @@ -0,0 +1,1043 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_http_upload.erl +%%% Author : Holger Weiss <holger@zedat.fu-berlin.de> +%%% Purpose : HTTP File Upload (XEP-0363) +%%% Created : 20 Aug 2015 by Holger Weiss <holger@zedat.fu-berlin.de> +%%% +%%% +%%% ejabberd, Copyright (C) 2015-2016 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_http_upload). +-author('holger@zedat.fu-berlin.de'). + +-protocol({xep, 363, '0.1'}). + +-define(GEN_SERVER, gen_server). +-define(SERVICE_REQUEST_TIMEOUT, 5000). % 5 seconds. +-define(SLOT_TIMEOUT, 18000000). % 5 hours. +-define(PROCNAME, ?MODULE). +-define(FORMAT(Error), file:format_error(Error)). +-define(URL_ENC(URL), binary_to_list(ejabberd_http:url_encode(URL))). +-define(ADDR_TO_STR(IP), ejabberd_config:may_hide_data(jlib:ip_to_list(IP))). +-define(STR_TO_INT(Str, B), jlib:binary_to_integer(iolist_to_binary(Str), B)). +-define(DEFAULT_CONTENT_TYPE, <<"application/octet-stream">>). +-define(CONTENT_TYPES, + [{<<".avi">>, <<"video/avi">>}, + {<<".bmp">>, <<"image/bmp">>}, + {<<".bz2">>, <<"application/x-bzip2">>}, + {<<".gif">>, <<"image/gif">>}, + {<<".gz">>, <<"application/x-gzip">>}, + {<<".jpeg">>, <<"image/jpeg">>}, + {<<".jpg">>, <<"image/jpeg">>}, + {<<".mp3">>, <<"audio/mpeg">>}, + {<<".mp4">>, <<"video/mp4">>}, + {<<".mpeg">>, <<"video/mpeg">>}, + {<<".mpg">>, <<"video/mpeg">>}, + {<<".ogg">>, <<"application/ogg">>}, + {<<".pdf">>, <<"application/pdf">>}, + {<<".png">>, <<"image/png">>}, + {<<".rtf">>, <<"application/rtf">>}, + {<<".svg">>, <<"image/svg+xml">>}, + {<<".tiff">>, <<"image/tiff">>}, + {<<".txt">>, <<"text/plain">>}, + {<<".wav">>, <<"audio/wav">>}, + {<<".webp">>, <<"image/webp">>}, + {<<".xz">>, <<"application/x-xz">>}, + {<<".zip">>, <<"application/zip">>}]). + +-behaviour(?GEN_SERVER). +-behaviour(gen_mod). + +%% gen_mod/supervisor callbacks. +-export([start_link/3, + start/2, + stop/1, + mod_opt_type/1]). + +%% gen_server callbacks. +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). + +%% ejabberd_http callback. +-export([process/2]). + +%% ejabberd_hooks callback. +-export([remove_user/2]). + +%% Utility functions. +-export([get_proc_name/2, + expand_home/1, + expand_host/2]). + +-include("ejabberd.hrl"). +-include("ejabberd_http.hrl"). +-include("jlib.hrl"). +-include("logger.hrl"). + +-record(state, + {server_host :: binary(), + host :: binary(), + name :: binary(), + access :: atom(), + max_size :: pos_integer() | infinity, + secret_length :: pos_integer(), + jid_in_url :: sha1 | node, + file_mode :: integer() | undefined, + dir_mode :: integer() | undefined, + docroot :: binary(), + put_url :: binary(), + get_url :: binary(), + service_url :: binary() | undefined, + thumbnail :: boolean(), + slots = #{} :: map()}). + +-record(media_info, + {type :: binary(), + height :: integer(), + width :: integer()}). + +-type state() :: #state{}. +-type slot() :: [binary(), ...]. +-type media_info() :: #media_info{}. + +%%-------------------------------------------------------------------- +%% gen_mod/supervisor callbacks. +%%-------------------------------------------------------------------- + +-spec start_link(binary(), atom(), gen_mod:opts()) + -> {ok, pid()} | ignore | {error, _}. + +start_link(ServerHost, Proc, Opts) -> + ?GEN_SERVER:start_link({local, Proc}, ?MODULE, {ServerHost, Opts}, []). + +-spec start(binary(), gen_mod:opts()) -> {ok, _} | {ok, _, _} | {error, _}. + +start(ServerHost, Opts) -> + case gen_mod:get_opt(rm_on_unregister, Opts, + fun(B) when is_boolean(B) -> B end, + true) of + true -> + ejabberd_hooks:add(remove_user, ServerHost, ?MODULE, + remove_user, 50), + ejabberd_hooks:add(anonymous_purge_hook, ServerHost, ?MODULE, + remove_user, 50); + false -> + ok + end, + Proc = get_proc_name(ServerHost, ?PROCNAME), + Spec = {Proc, + {?MODULE, start_link, [ServerHost, Proc, Opts]}, + permanent, + 3000, + worker, + [?MODULE]}, + supervisor:start_child(ejabberd_sup, Spec). + +-spec stop(binary()) -> ok. + +stop(ServerHost) -> + case gen_mod:get_module_opt(ServerHost, ?MODULE, rm_on_unregister, + fun(B) when is_boolean(B) -> B end, + true) of + true -> + ejabberd_hooks:delete(remove_user, ServerHost, ?MODULE, + remove_user, 50), + ejabberd_hooks:delete(anonymous_purge_hook, ServerHost, ?MODULE, + remove_user, 50); + false -> + ok + end, + Proc = get_proc_name(ServerHost, ?PROCNAME), + supervisor:terminate_child(ejabberd_sup, Proc), + supervisor:delete_child(ejabberd_sup, Proc). + +-spec mod_opt_type(atom()) -> fun((term()) -> term()) | [atom()]. + +mod_opt_type(host) -> + fun iolist_to_binary/1; +mod_opt_type(name) -> + fun iolist_to_binary/1; +mod_opt_type(access) -> + fun(A) when is_atom(A) -> A end; +mod_opt_type(max_size) -> + fun(I) when is_integer(I), I > 0 -> I; + (infinity) -> infinity + end; +mod_opt_type(secret_length) -> + fun(I) when is_integer(I), I >= 8 -> I end; +mod_opt_type(jid_in_url) -> + fun(sha1) -> sha1; + (node) -> node + end; +mod_opt_type(file_mode) -> + fun(Mode) -> ?STR_TO_INT(Mode, 8) end; +mod_opt_type(dir_mode) -> + fun(Mode) -> ?STR_TO_INT(Mode, 8) end; +mod_opt_type(docroot) -> + fun iolist_to_binary/1; +mod_opt_type(put_url) -> + fun(<<"http://", _/binary>> = URL) -> URL; + (<<"https://", _/binary>> = URL) -> URL + end; +mod_opt_type(get_url) -> + fun(<<"http://", _/binary>> = URL) -> URL; + (<<"https://", _/binary>> = URL) -> URL + end; +mod_opt_type(service_url) -> + fun(<<"http://", _/binary>> = URL) -> URL; + (<<"https://", _/binary>> = URL) -> URL + end; +mod_opt_type(custom_headers) -> + fun(Headers) -> + lists:map(fun({K, V}) -> + {iolist_to_binary(K), iolist_to_binary(V)} + end, Headers) + end; +mod_opt_type(rm_on_unregister) -> + fun(B) when is_boolean(B) -> B end; +mod_opt_type(thumbnail) -> + fun(B) when is_boolean(B) -> B end; +mod_opt_type(_) -> + [host, name, access, max_size, secret_length, jid_in_url, file_mode, + dir_mode, docroot, put_url, get_url, service_url, custom_headers, + rm_on_unregister, thumbnail]. + +%%-------------------------------------------------------------------- +%% gen_server callbacks. +%%-------------------------------------------------------------------- + +-spec init({binary(), gen_mod:opts()}) -> {ok, state()}. + +init({ServerHost, Opts}) -> + process_flag(trap_exit, true), + Host = gen_mod:get_opt_host(ServerHost, Opts, <<"upload.@HOST@">>), + Name = gen_mod:get_opt(name, Opts, + fun iolist_to_binary/1, + <<"HTTP File Upload">>), + Access = gen_mod:get_opt(access, Opts, + fun(A) when is_atom(A) -> A end, + local), + MaxSize = gen_mod:get_opt(max_size, Opts, + fun(I) when is_integer(I), I > 0 -> I; + (infinity) -> infinity + end, + 104857600), + SecretLength = gen_mod:get_opt(secret_length, Opts, + fun(I) when is_integer(I), I >= 8 -> I end, + 40), + JIDinURL = gen_mod:get_opt(jid_in_url, Opts, + fun(sha1) -> sha1; + (node) -> node + end, + sha1), + DocRoot = gen_mod:get_opt(docroot, Opts, + fun iolist_to_binary/1, + <<"@HOME@/upload">>), + FileMode = gen_mod:get_opt(file_mode, Opts, + fun(Mode) -> ?STR_TO_INT(Mode, 8) end), + DirMode = gen_mod:get_opt(dir_mode, Opts, + fun(Mode) -> ?STR_TO_INT(Mode, 8) end), + PutURL = gen_mod:get_opt(put_url, Opts, + fun(<<"http://", _/binary>> = URL) -> URL; + (<<"https://", _/binary>> = URL) -> URL + end, + <<"http://@HOST@:5444">>), + GetURL = gen_mod:get_opt(get_url, Opts, + fun(<<"http://", _/binary>> = URL) -> URL; + (<<"https://", _/binary>> = URL) -> URL + end, + PutURL), + ServiceURL = gen_mod:get_opt(service_url, Opts, + fun(<<"http://", _/binary>> = URL) -> URL; + (<<"https://", _/binary>> = URL) -> URL + end), + Thumbnail = gen_mod:get_opt(thumbnail, Opts, + fun(B) when is_boolean(B) -> B end, + true), + DocRoot1 = expand_home(str:strip(DocRoot, right, $/)), + DocRoot2 = expand_host(DocRoot1, ServerHost), + case ServiceURL of + undefined -> + ok; + <<"http://", _/binary>> -> + application:start(inets); + <<"https://", _/binary>> -> + application:start(inets), + application:start(crypto), + application:start(asn1), + application:start(public_key), + application:start(ssl) + end, + case DirMode of + undefined -> + ok; + Mode -> + file:change_mode(DocRoot2, Mode) + end, + case Thumbnail of + true -> + case string:str(os:cmd("identify"), "Magick") of + 0 -> + ?ERROR_MSG("Cannot find 'identify' command, please install " + "ImageMagick or disable thumbnail creation", []); + _ -> + ok + end; + false -> + ok + end, + ejabberd_router:register_route(Host, ServerHost), + {ok, #state{server_host = ServerHost, host = Host, name = Name, + access = Access, max_size = MaxSize, + secret_length = SecretLength, jid_in_url = JIDinURL, + file_mode = FileMode, dir_mode = DirMode, + thumbnail = Thumbnail, + docroot = DocRoot2, + put_url = expand_host(str:strip(PutURL, right, $/), ServerHost), + get_url = expand_host(str:strip(GetURL, right, $/), ServerHost), + service_url = ServiceURL}}. + +-spec handle_call(_, {pid(), _}, state()) + -> {reply, {ok, pos_integer(), binary(), + pos_integer() | undefined, + pos_integer() | undefined}, state()} | + {reply, {error, binary()}, state()} | {noreply, state()}. + +handle_call({use_slot, Slot}, _From, #state{file_mode = FileMode, + dir_mode = DirMode, + get_url = GetPrefix, + thumbnail = Thumbnail, + docroot = DocRoot} = State) -> + case get_slot(Slot, State) of + {ok, {Size, Timer}} -> + timer:cancel(Timer), + NewState = del_slot(Slot, State), + Path = str:join([DocRoot | Slot], <<$/>>), + {reply, {ok, Size, Path, FileMode, DirMode, GetPrefix, Thumbnail}, + NewState}; + error -> + {reply, {error, <<"Invalid slot">>}, State} + end; +handle_call(get_docroot, _From, #state{docroot = DocRoot} = State) -> + {reply, {ok, DocRoot}, State}; +handle_call(Request, From, State) -> + ?ERROR_MSG("Got unexpected request from ~p: ~p", [From, Request]), + {noreply, State}. + +-spec handle_cast(_, state()) -> {noreply, state()}. + +handle_cast(Request, State) -> + ?ERROR_MSG("Got unexpected request: ~p", [Request]), + {noreply, State}. + +-spec handle_info(timeout | _, state()) -> {noreply, state()}. + +handle_info({route, From, To, #xmlel{name = <<"iq">>} = Stanza}, State) -> + Request = jlib:iq_query_info(Stanza), + {Reply, NewState} = case process_iq(From, Request, State) of + R when is_record(R, iq) -> + {R, State}; + {R, S} -> + {R, S}; + not_request -> + {none, State} + end, + if Reply /= none -> + ejabberd_router:route(To, From, jlib:iq_to_xml(Reply)); + true -> + ok + end, + {noreply, NewState}; +handle_info({slot_timed_out, Slot}, State) -> + NewState = del_slot(Slot, State), + {noreply, NewState}; +handle_info(Info, State) -> + ?ERROR_MSG("Got unexpected info: ~p", [Info]), + {noreply, State}. + +-spec terminate(normal | shutdown | {shutdown, _} | _, _) -> ok. + +terminate(Reason, #state{server_host = ServerHost, host = Host}) -> + ?DEBUG("Stopping HTTP upload process for ~s: ~p", [ServerHost, Reason]), + ejabberd_router:unregister_route(Host), + ok. + +-spec code_change({down, _} | _, state(), _) -> {ok, state()}. + +code_change(_OldVsn, #state{server_host = ServerHost} = State, _Extra) -> + ?DEBUG("Updating HTTP upload process for ~s", [ServerHost]), + {ok, State}. + +%%-------------------------------------------------------------------- +%% ejabberd_http callback. +%%-------------------------------------------------------------------- + +-spec process([binary()], #request{}) + -> {pos_integer(), [{binary(), binary()}], binary()}. + +process(LocalPath, #request{method = Method, host = Host, ip = IP}) + when length(LocalPath) < 3, + Method == 'PUT' orelse + Method == 'GET' orelse + Method == 'HEAD' -> + ?DEBUG("Rejecting ~s request from ~s for ~s: Too few path components", + [Method, ?ADDR_TO_STR(IP), Host]), + http_response(Host, 404); +process(_LocalPath, #request{method = 'PUT', host = Host, ip = IP, + data = Data} = Request) -> + {Proc, Slot} = parse_http_request(Request), + case catch gen_server:call(Proc, {use_slot, Slot}) of + {ok, Size, Path, FileMode, DirMode, GetPrefix, Thumbnail} + when byte_size(Data) == Size -> + ?DEBUG("Storing file from ~s for ~s: ~s", + [?ADDR_TO_STR(IP), Host, Path]), + case store_file(Path, Data, FileMode, DirMode, + GetPrefix, Slot, Thumbnail) of + ok -> + http_response(Host, 201); + {ok, Headers, OutData} -> + http_response(Host, 201, Headers, OutData); + {error, Error} -> + ?ERROR_MSG("Cannot store file ~s from ~s for ~s: ~p", + [Path, ?ADDR_TO_STR(IP), Host, ?FORMAT(Error)]), + http_response(Host, 500) + end; + {ok, Size, Path, _FileMode, _DirMode, _GetPrefix, _Thumbnail} -> + ?INFO_MSG("Rejecting file ~s from ~s for ~s: Size is ~B, not ~B", + [Path, ?ADDR_TO_STR(IP), Host, byte_size(Data), Size]), + http_response(Host, 413); + {error, Error} -> + ?INFO_MSG("Rejecting file from ~s for ~s: ~p", + [?ADDR_TO_STR(IP), Host, Error]), + http_response(Host, 403); + Error -> + ?ERROR_MSG("Cannot handle PUT request from ~s for ~s: ~p", + [?ADDR_TO_STR(IP), Host, Error]), + http_response(Host, 500) + end; +process(_LocalPath, #request{method = Method, host = Host, ip = IP} = Request) + when Method == 'GET'; + Method == 'HEAD' -> + {Proc, [_UserDir, _RandDir, FileName] = Slot} = parse_http_request(Request), + case catch gen_server:call(Proc, get_docroot) of + {ok, DocRoot} -> + Path = str:join([DocRoot | Slot], <<$/>>), + case file:read_file(Path) of + {ok, Data} -> + ?INFO_MSG("Serving ~s to ~s", [Path, ?ADDR_TO_STR(IP)]), + ContentType = guess_content_type(FileName), + Headers1 = case ContentType of + <<"image/", _SubType/binary>> -> []; + <<"text/", _SubType/binary>> -> []; + _ -> + [{<<"Content-Disposition">>, + <<"attachment; filename=", + $", FileName/binary, $">>}] + end, + Headers2 = [{<<"Content-Type">>, ContentType} | Headers1], + http_response(Host, 200, Headers2, Data); + {error, eacces} -> + ?INFO_MSG("Cannot serve ~s to ~s: Permission denied", + [Path, ?ADDR_TO_STR(IP)]), + http_response(Host, 403); + {error, enoent} -> + ?INFO_MSG("Cannot serve ~s to ~s: No such file", + [Path, ?ADDR_TO_STR(IP)]), + http_response(Host, 404); + {error, eisdir} -> + ?INFO_MSG("Cannot serve ~s to ~s: Is a directory", + [Path, ?ADDR_TO_STR(IP)]), + http_response(Host, 404); + {error, Error} -> + ?INFO_MSG("Cannot serve ~s to ~s: ~s", + [Path, ?ADDR_TO_STR(IP), ?FORMAT(Error)]), + http_response(Host, 500) + end; + Error -> + ?ERROR_MSG("Cannot handle ~s request from ~s for ~s: ~p", + [Method, ?ADDR_TO_STR(IP), Host, Error]), + http_response(Host, 500) + end; +process(_LocalPath, #request{method = 'OPTIONS', host = Host, ip = IP}) -> + ?DEBUG("Responding to OPTIONS request from ~s for ~s", + [?ADDR_TO_STR(IP), Host]), + http_response(Host, 200); +process(_LocalPath, #request{method = Method, host = Host, ip = IP}) -> + ?DEBUG("Rejecting ~s request from ~s for ~s", + [Method, ?ADDR_TO_STR(IP), Host]), + http_response(Host, 405, [{<<"Allow">>, <<"OPTIONS, HEAD, GET, PUT">>}]). + +%%-------------------------------------------------------------------- +%% Exported utility functions. +%%-------------------------------------------------------------------- + +-spec get_proc_name(binary(), atom()) -> atom(). + +get_proc_name(ServerHost, ModuleName) -> + PutURL = gen_mod:get_module_opt(ServerHost, ?MODULE, put_url, + fun(<<"http://", _/binary>> = URL) -> URL; + (<<"https://", _/binary>> = URL) -> URL; + (_) -> <<"http://@HOST@">> + end, + <<"http://@HOST@">>), + {ok, {_Scheme, _UserInfo, Host, _Port, Path, _Query}} = + http_uri:parse(binary_to_list(expand_host(PutURL, ServerHost))), + ProcPrefix = list_to_binary(string:strip(Host ++ Path, right, $/)), + gen_mod:get_module_proc(ProcPrefix, ModuleName). + +-spec expand_home(binary()) -> binary(). + +expand_home(Subject) -> + {ok, [[Home]]} = init:get_argument(home), + Parts = binary:split(Subject, <<"@HOME@">>, [global]), + str:join(Parts, list_to_binary(Home)). + +-spec expand_host(binary(), binary()) -> binary(). + +expand_host(Subject, Host) -> + Parts = binary:split(Subject, <<"@HOST@">>, [global]), + str:join(Parts, Host). + +%%-------------------------------------------------------------------- +%% Internal functions. +%%-------------------------------------------------------------------- + +%% XMPP request handling. + +-spec process_iq(jid(), iq_request() | reply | invalid, state()) + -> {iq_reply(), state()} | iq_reply() | not_request. + +process_iq(_From, + #iq{type = get, xmlns = ?NS_DISCO_INFO, lang = Lang} = IQ, + #state{server_host = ServerHost, name = Name}) -> + AddInfo = ejabberd_hooks:run_fold(disco_info, ServerHost, [], + [ServerHost, ?MODULE, <<"">>, <<"">>]), + IQ#iq{type = result, + sub_el = [#xmlel{name = <<"query">>, + attrs = [{<<"xmlns">>, ?NS_DISCO_INFO}], + children = iq_disco_info(ServerHost, Lang, Name) + ++ AddInfo}]}; +process_iq(From, + #iq{type = get, xmlns = XMLNS, lang = Lang, sub_el = SubEl} = IQ, + #state{server_host = ServerHost, access = Access} = State) + when XMLNS == ?NS_HTTP_UPLOAD; + XMLNS == ?NS_HTTP_UPLOAD_OLD -> + case acl:match_rule(ServerHost, Access, From) of + allow -> + case parse_request(SubEl, Lang) of + {ok, File, Size, ContentType} -> + case create_slot(State, From, File, Size, ContentType, + Lang) of + {ok, Slot} -> + {ok, Timer} = timer:send_after(?SLOT_TIMEOUT, + {slot_timed_out, + Slot}), + NewState = add_slot(Slot, Size, Timer, State), + SlotEl = slot_el(Slot, State, XMLNS), + {IQ#iq{type = result, sub_el = [SlotEl]}, NewState}; + {ok, PutURL, GetURL} -> + SlotEl = slot_el(PutURL, GetURL, XMLNS), + IQ#iq{type = result, sub_el = [SlotEl]}; + {error, Error} -> + IQ#iq{type = error, sub_el = [SubEl, Error]} + end; + {error, Error} -> + ?DEBUG("Cannot parse request from ~s", + [jid:to_string(From)]), + IQ#iq{type = error, sub_el = [SubEl, Error]} + end; + deny -> + ?DEBUG("Denying HTTP upload slot request from ~s", + [jid:to_string(From)]), + IQ#iq{type = error, sub_el = [SubEl, ?ERR_FORBIDDEN]} + end; +process_iq(_From, #iq{sub_el = SubEl} = IQ, _State) -> + IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; +process_iq(_From, reply, _State) -> + not_request; +process_iq(_From, invalid, _State) -> + not_request. + +-spec parse_request(xmlel(), binary()) + -> {ok, binary(), pos_integer(), binary()} | {error, xmlel()}. + +parse_request(#xmlel{name = <<"request">>, attrs = Attrs} = Request, Lang) -> + case fxml:get_attr(<<"xmlns">>, Attrs) of + {value, XMLNS} when XMLNS == ?NS_HTTP_UPLOAD; + XMLNS == ?NS_HTTP_UPLOAD_OLD -> + case {fxml:get_subtag_cdata(Request, <<"filename">>), + fxml:get_subtag_cdata(Request, <<"size">>), + fxml:get_subtag_cdata(Request, <<"content-type">>)} of + {File, SizeStr, ContentType} when byte_size(File) > 0 -> + case catch jlib:binary_to_integer(SizeStr) of + Size when is_integer(Size), Size > 0 -> + {ok, File, Size, yield_content_type(ContentType)}; + _ -> + Text = <<"Please specify file size.">>, + {error, ?ERRT_BAD_REQUEST(Lang, Text)} + end; + _ -> + Text = <<"Please specify file name.">>, + {error, ?ERRT_BAD_REQUEST(Lang, Text)} + end; + _ -> + {error, ?ERR_BAD_REQUEST} + end; +parse_request(_El, _Lang) -> {error, ?ERR_BAD_REQUEST}. + +-spec create_slot(state(), jid(), binary(), pos_integer(), binary(), binary()) + -> {ok, slot()} | {ok, binary(), binary()} | {error, xmlel()}. + +create_slot(#state{service_url = undefined, max_size = MaxSize}, + JID, File, Size, _ContentType, Lang) when MaxSize /= infinity, + Size > MaxSize -> + Text = <<"File larger than ", (jlib:integer_to_binary(MaxSize))/binary, + " Bytes.">>, + ?INFO_MSG("Rejecting file ~s from ~s (too large: ~B bytes)", + [File, jid:to_string(JID), Size]), + {error, ?ERRT_NOT_ACCEPTABLE(Lang, Text)}; +create_slot(#state{service_url = undefined, + jid_in_url = JIDinURL, + secret_length = SecretLength, + server_host = ServerHost, + docroot = DocRoot}, + JID, File, Size, _ContentType, Lang) -> + UserStr = make_user_string(JID, JIDinURL), + UserDir = <<DocRoot/binary, $/, UserStr/binary>>, + case ejabberd_hooks:run_fold(http_upload_slot_request, ServerHost, allow, + [JID, UserDir, Size, Lang]) of + allow -> + RandStr = make_rand_string(SecretLength), + FileStr = make_file_string(File), + ?INFO_MSG("Got HTTP upload slot for ~s (file: ~s)", + [jid:to_string(JID), File]), + {ok, [UserStr, RandStr, FileStr]}; + deny -> + {error, ?ERR_SERVICE_UNAVAILABLE}; + #xmlel{} = Error -> + {error, Error} + end; +create_slot(#state{service_url = ServiceURL}, + #jid{luser = U, lserver = S} = JID, File, Size, ContentType, + _Lang) -> + Options = [{body_format, binary}, {full_result, false}], + HttpOptions = [{timeout, ?SERVICE_REQUEST_TIMEOUT}], + SizeStr = jlib:integer_to_binary(Size), + GetRequest = binary_to_list(ServiceURL) ++ + "?jid=" ++ ?URL_ENC(jid:to_string({U, S, <<"">>})) ++ + "&name=" ++ ?URL_ENC(File) ++ + "&size=" ++ ?URL_ENC(SizeStr) ++ + "&content_type=" ++ ?URL_ENC(ContentType), + case httpc:request(get, {GetRequest, []}, HttpOptions, Options) of + {ok, {Code, Body}} when Code >= 200, Code =< 299 -> + case binary:split(Body, <<$\n>>, [global, trim]) of + [<<"http", _/binary>> = PutURL, + <<"http", _/binary>> = GetURL] -> + ?INFO_MSG("Got HTTP upload slot for ~s (file: ~s)", + [jid:to_string(JID), File]), + {ok, PutURL, GetURL}; + Lines -> + ?ERROR_MSG("Can't parse data received for ~s from <~s>: ~p", + [jid:to_string(JID), ServiceURL, Lines]), + {error, ?ERR_SERVICE_UNAVAILABLE} + end; + {ok, {402, _Body}} -> + ?INFO_MSG("Got status code 402 for ~s from <~s>", + [jid:to_string(JID), ServiceURL]), + {error, ?ERR_RESOURCE_CONSTRAINT}; + {ok, {403, _Body}} -> + ?INFO_MSG("Got status code 403 for ~s from <~s>", + [jid:to_string(JID), ServiceURL]), + {error, ?ERR_NOT_ALLOWED}; + {ok, {413, _Body}} -> + ?INFO_MSG("Got status code 413 for ~s from <~s>", + [jid:to_string(JID), ServiceURL]), + {error, ?ERR_NOT_ACCEPTABLE}; + {ok, {Code, _Body}} -> + ?ERROR_MSG("Got unexpected status code for ~s from <~s>: ~B", + [jid:to_string(JID), ServiceURL, Code]), + {error, ?ERR_SERVICE_UNAVAILABLE}; + {error, Reason} -> + ?ERROR_MSG("Error requesting upload slot for ~s from <~s>: ~p", + [jid:to_string(JID), ServiceURL, Reason]), + {error, ?ERR_SERVICE_UNAVAILABLE} + end. + +-spec add_slot(slot(), pos_integer(), timer:tref(), state()) -> state(). + +add_slot(Slot, Size, Timer, #state{slots = Slots} = State) -> + NewSlots = maps:put(Slot, {Size, Timer}, Slots), + State#state{slots = NewSlots}. + +-spec get_slot(slot(), state()) -> {ok, {pos_integer(), timer:tref()}} | error. + +get_slot(Slot, #state{slots = Slots}) -> + maps:find(Slot, Slots). + +-spec del_slot(slot(), state()) -> state(). + +del_slot(Slot, #state{slots = Slots} = State) -> + NewSlots = maps:remove(Slot, Slots), + State#state{slots = NewSlots}. + +-spec slot_el(slot() | binary(), state() | binary(), binary()) -> xmlel(). + +slot_el(Slot, #state{put_url = PutPrefix, get_url = GetPrefix}, XMLNS) -> + PutURL = str:join([PutPrefix | Slot], <<$/>>), + GetURL = str:join([GetPrefix | Slot], <<$/>>), + slot_el(PutURL, GetURL, XMLNS); +slot_el(PutURL, GetURL, XMLNS) -> + #xmlel{name = <<"slot">>, + attrs = [{<<"xmlns">>, XMLNS}], + children = [#xmlel{name = <<"put">>, + children = [{xmlcdata, PutURL}]}, + #xmlel{name = <<"get">>, + children = [{xmlcdata, GetURL}]}]}. + +-spec make_user_string(jid(), sha1 | node) -> binary(). + +make_user_string(#jid{luser = U, lserver = S}, sha1) -> + p1_sha:sha(<<U/binary, $@, S/binary>>); +make_user_string(#jid{luser = U}, node) -> + re:replace(U, <<"[^a-zA-Z0-9_.-]">>, <<$_>>, [global, {return, binary}]). + +-spec make_file_string(binary()) -> binary(). + +make_file_string(File) -> + re:replace(File, <<"[^a-zA-Z0-9_.-]">>, <<$_>>, [global, {return, binary}]). + +-spec make_rand_string(non_neg_integer()) -> binary(). + +make_rand_string(Length) -> + list_to_binary(make_rand_string([], Length)). + +-spec make_rand_string(string(), non_neg_integer()) -> string(). + +make_rand_string(S, 0) -> S; +make_rand_string(S, N) -> make_rand_string([make_rand_char() | S], N - 1). + +-spec make_rand_char() -> char(). + +make_rand_char() -> + map_int_to_char(crypto:rand_uniform(0, 62)). + +-spec map_int_to_char(0..61) -> char(). + +map_int_to_char(N) when N =< 9 -> N + 48; % Digit. +map_int_to_char(N) when N =< 35 -> N + 55; % Upper-case character. +map_int_to_char(N) when N =< 61 -> N + 61. % Lower-case character. + +-spec yield_content_type(binary()) -> binary(). + +yield_content_type(<<"">>) -> ?DEFAULT_CONTENT_TYPE; +yield_content_type(Type) -> Type. + +-spec iq_disco_info(binary(), binary(), binary()) -> [xmlel()]. + +iq_disco_info(Host, Lang, Name) -> + Form = case gen_mod:get_module_opt(Host, ?MODULE, max_size, + fun(I) when is_integer(I), I > 0 -> I; + (infinity) -> infinity + end, + 104857600) of + infinity -> + []; + MaxSize -> + MaxSizeStr = jlib:integer_to_binary(MaxSize), + Fields = [#xmlel{name = <<"field">>, + attrs = [{<<"type">>, <<"hidden">>}, + {<<"var">>, <<"FORM_TYPE">>}], + children = [#xmlel{name = <<"value">>, + children = + [{xmlcdata, + ?NS_HTTP_UPLOAD}]}]}, + #xmlel{name = <<"field">>, + attrs = [{<<"var">>, <<"max-file-size">>}], + children = [#xmlel{name = <<"value">>, + children = + [{xmlcdata, + MaxSizeStr}]}]}], + [#xmlel{name = <<"x">>, + attrs = [{<<"xmlns">>, ?NS_XDATA}, + {<<"type">>, <<"result">>}], + children = Fields}] + end, + [#xmlel{name = <<"identity">>, + attrs = [{<<"category">>, <<"store">>}, + {<<"type">>, <<"file">>}, + {<<"name">>, translate:translate(Lang, Name)}]}, + #xmlel{name = <<"feature">>, + attrs = [{<<"var">>, ?NS_HTTP_UPLOAD}]}, + #xmlel{name = <<"feature">>, + attrs = [{<<"var">>, ?NS_HTTP_UPLOAD_OLD}]} | Form]. + +%% HTTP request handling. + +-spec parse_http_request(#request{}) -> {atom(), slot()}. + +parse_http_request(#request{host = Host, path = Path}) -> + PrefixLength = length(Path) - 3, + {ProcURL, Slot} = if PrefixLength > 0 -> + Prefix = lists:sublist(Path, PrefixLength), + {str:join([Host | Prefix], $/), + lists:nthtail(PrefixLength, Path)}; + true -> + {Host, Path} + end, + {gen_mod:get_module_proc(ProcURL, ?PROCNAME), Slot}. + +-spec store_file(binary(), binary(), + integer() | undefined, + integer() | undefined, + binary(), slot(), boolean()) + -> ok | {ok, [{binary(), binary()}], binary()} | {error, term()}. + +store_file(Path, Data, FileMode, DirMode, GetPrefix, Slot, Thumbnail) -> + case do_store_file(Path, Data, FileMode, DirMode) of + ok when Thumbnail -> + case identify(Path) of + {ok, MediaInfo} -> + case convert(Path, MediaInfo) of + {ok, OutPath} -> + [UserDir, RandDir | _] = Slot, + FileName = filename:basename(OutPath), + URL = str:join([GetPrefix, UserDir, + RandDir, FileName], <<$/>>), + ThumbEl = thumb_el(OutPath, URL), + {ok, + [{<<"Content-Type">>, + <<"text/xml; charset=utf-8">>}], + fxml:element_to_binary(ThumbEl)}; + pass -> + ok + end; + pass -> + ok + end; + ok -> + ok; + Err -> + Err + end. + +-spec do_store_file(file:filename_all(), binary(), + integer() | undefined, + integer() | undefined) + -> ok | {error, term()}. + +do_store_file(Path, Data, FileMode, DirMode) -> + try + ok = filelib:ensure_dir(Path), + {ok, Io} = file:open(Path, [write, exclusive, raw]), + Ok = file:write(Io, Data), + ok = file:close(Io), + if is_integer(FileMode) -> + ok = file:change_mode(Path, FileMode); + FileMode == undefined -> + ok + end, + if is_integer(DirMode) -> + RandDir = filename:dirname(Path), + UserDir = filename:dirname(RandDir), + ok = file:change_mode(RandDir, DirMode), + ok = file:change_mode(UserDir, DirMode); + DirMode == undefined -> + ok + end, + ok = Ok % Raise an exception if file:write/2 failed. + catch + _:{badmatch, {error, Error}} -> + {error, Error}; + _:Error -> + {error, Error} + end. + +-spec guess_content_type(binary()) -> binary(). + +guess_content_type(FileName) -> + mod_http_fileserver:content_type(FileName, + ?DEFAULT_CONTENT_TYPE, + ?CONTENT_TYPES). + +-spec http_response(binary(), 100..599) + -> {pos_integer(), [{binary(), binary()}], binary()}. + +http_response(Host, Code) -> + http_response(Host, Code, []). + +-spec http_response(binary(), 100..599, [{binary(), binary()}]) + -> {pos_integer(), [{binary(), binary()}], binary()}. + +http_response(Host, Code, ExtraHeaders) -> + Message = <<(code_to_message(Code))/binary, $\n>>, + http_response(Host, Code, ExtraHeaders, Message). + +-spec http_response(binary(), 100..599, [{binary(), binary()}], binary()) + -> {pos_integer(), [{binary(), binary()}], binary()}. + +http_response(Host, Code, ExtraHeaders, Body) -> + ServerHeader = {<<"Server">>, <<"ejabberd ", (?VERSION)/binary>>}, + CustomHeaders = + gen_mod:get_module_opt(Host, ?MODULE, custom_headers, + fun(Headers) -> + lists:map(fun({K, V}) -> + {iolist_to_binary(K), + iolist_to_binary(V)} + end, Headers) + end, + []), + Headers = case proplists:is_defined(<<"Content-Type">>, ExtraHeaders) of + true -> + [ServerHeader | ExtraHeaders]; + false -> + [ServerHeader, {<<"Content-Type">>, <<"text/plain">>} | + ExtraHeaders] + end ++ CustomHeaders, + {Code, Headers, Body}. + +-spec code_to_message(100..599) -> binary(). + +code_to_message(201) -> <<"Upload successful.">>; +code_to_message(403) -> <<"Forbidden.">>; +code_to_message(404) -> <<"Not found.">>; +code_to_message(405) -> <<"Method not allowed.">>; +code_to_message(413) -> <<"File size doesn't match requested size.">>; +code_to_message(500) -> <<"Internal server error.">>; +code_to_message(_Code) -> <<"">>. + +%%-------------------------------------------------------------------- +%% Image manipulation stuff. +%%-------------------------------------------------------------------- + +-spec identify(binary()) -> {ok, media_info()} | pass. + +identify(Path) -> + Cmd = io_lib:format("identify -format 'ok %m %h %w' ~s", [Path]), + Res = string:strip(os:cmd(Cmd), right, $\n), + case string:tokens(Res, " ") of + ["ok", T, H, W] -> + {ok, #media_info{type = list_to_binary(string:to_lower(T)), + height = list_to_integer(H), + width = list_to_integer(W)}}; + _ -> + ?DEBUG("Cannot identify type of ~s: ~s", [Path, Res]), + pass + end. + +-spec convert(binary(), media_info()) -> {ok, binary()} | pass. + +convert(Path, #media_info{type = T, width = W, height = H}) -> + if W * H >= 25000000 -> + ?DEBUG("The image ~s is more than 25 Mpix", [Path]), + pass; + W =< 300, H =< 300 -> + {ok, Path}; + T == <<"gif">>; T == <<"jpeg">>; T == <<"png">>; T == <<"webp">> -> + Dir = filename:dirname(Path), + FileName = <<(randoms:get_string())/binary, $., T/binary>>, + OutPath = filename:join(Dir, FileName), + Cmd = io_lib:format("convert -resize 300 ~s ~s", [Path, OutPath]), + case os:cmd(Cmd) of + "" -> + {ok, OutPath}; + Err -> + ?ERROR_MSG("Failed to convert ~s to ~s: ~s", + [Path, OutPath, string:strip(Err, right, $\n)]), + pass + end; + true -> + ?DEBUG("Won't call 'convert' for unknown type ~s", [T]), + pass + end. + +-spec thumb_el(binary(), binary()) -> xmlel(). + +thumb_el(Path, URI) -> + ContentType = guess_content_type(Path), + case identify(Path) of + {ok, #media_info{height = H, width = W}} -> + #xmlel{name = <<"thumbnail">>, + attrs = [{<<"xmlns">>, ?NS_THUMBS_1}, + {<<"media-type">>, ContentType}, + {<<"uri">>, URI}, + {<<"height">>, jlib:integer_to_binary(H)}, + {<<"width">>, jlib:integer_to_binary(W)}]}; + pass -> + #xmlel{name = <<"thumbnail">>, + attrs = [{<<"xmlns">>, ?NS_THUMBS_1}, + {<<"uri">>, URI}, + {<<"media-type">>, ContentType}]} + end. + +%%-------------------------------------------------------------------- +%% Remove user. +%%-------------------------------------------------------------------- + +-spec remove_user(binary(), binary()) -> ok. + +remove_user(User, Server) -> + ServerHost = jid:nameprep(Server), + DocRoot = gen_mod:get_module_opt(ServerHost, ?MODULE, docroot, + fun iolist_to_binary/1, + <<"@HOME@/upload">>), + JIDinURL = gen_mod:get_module_opt(ServerHost, ?MODULE, jid_in_url, + fun(sha1) -> sha1; + (node) -> node + end, + sha1), + DocRoot1 = expand_host(expand_home(DocRoot), ServerHost), + UserStr = make_user_string(jid:make(User, Server, <<"">>), JIDinURL), + UserDir = str:join([DocRoot1, UserStr], <<$/>>), + case del_tree(UserDir) of + ok -> + ?INFO_MSG("Removed HTTP upload directory of ~s@~s", [User, Server]); + {error, enoent} -> + ?DEBUG("Found no HTTP upload directory of ~s@~s", [User, Server]); + {error, Error} -> + ?ERROR_MSG("Cannot remove HTTP upload directory of ~s@~s: ~p", + [User, Server, ?FORMAT(Error)]) + end, + ok. + +-spec del_tree(file:filename_all()) -> ok | {error, term()}. + +del_tree(Dir) when is_binary(Dir) -> + del_tree(binary_to_list(Dir)); +del_tree(Dir) -> + try + {ok, Entries} = file:list_dir(Dir), + lists:foreach(fun(Path) -> + case filelib:is_dir(Path) of + true -> + ok = del_tree(Path); + false -> + ok = file:delete(Path) + end + end, [Dir ++ "/" ++ Entry || Entry <- Entries]), + ok = file:del_dir(Dir) + catch + _:{badmatch, {error, Error}} -> + {error, Error}; + _:Error -> + {error, Error} + end. diff --git a/src/mod_http_upload_quota.erl b/src/mod_http_upload_quota.erl new file mode 100644 index 00000000..a5ae0c3c --- /dev/null +++ b/src/mod_http_upload_quota.erl @@ -0,0 +1,372 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_http_upload_quota.erl +%%% Author : Holger Weiss <holger@zedat.fu-berlin.de> +%%% Purpose : Quota management for HTTP File Upload (XEP-0363) +%%% Created : 15 Oct 2015 by Holger Weiss <holger@zedat.fu-berlin.de> +%%% +%%% +%%% ejabberd, Copyright (C) 2015-2016 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_http_upload_quota). +-author('holger@zedat.fu-berlin.de'). + +-define(GEN_SERVER, gen_server). +-define(PROCNAME, ?MODULE). +-define(TIMEOUT, timer:hours(24)). +-define(INITIAL_TIMEOUT, timer:minutes(10)). +-define(FORMAT(Error), file:format_error(Error)). + +-behaviour(?GEN_SERVER). +-behaviour(gen_mod). + +%% gen_mod/supervisor callbacks. +-export([start_link/3, + start/2, + stop/1, + mod_opt_type/1]). + +%% gen_server callbacks. +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). + +%% ejabberd_hooks callback. +-export([handle_slot_request/5]). + +-include("jlib.hrl"). +-include("logger.hrl"). +-include_lib("kernel/include/file.hrl"). + +-record(state, + {server_host :: binary(), + access_soft_quota :: atom(), + access_hard_quota :: atom(), + max_days :: pos_integer() | infinity, + docroot :: binary(), + disk_usage = #{} :: map(), + timers :: [timer:tref()]}). + +-type state() :: #state{}. + +%%-------------------------------------------------------------------- +%% gen_mod/supervisor callbacks. +%%-------------------------------------------------------------------- + +-spec start_link(binary(), atom(), gen_mod:opts()) + -> {ok, pid()} | ignore | {error, _}. + +start_link(ServerHost, Proc, Opts) -> + ?GEN_SERVER:start_link({local, Proc}, ?MODULE, {ServerHost, Opts}, []). + +-spec start(binary(), gen_mod:opts()) -> {ok, _} | {ok, _, _} | {error, _}. + +start(ServerHost, Opts) -> + Proc = mod_http_upload:get_proc_name(ServerHost, ?PROCNAME), + Spec = {Proc, + {?MODULE, start_link, [ServerHost, Proc, Opts]}, + permanent, + 3000, + worker, + [?MODULE]}, + supervisor:start_child(ejabberd_sup, Spec). + +-spec stop(binary()) -> ok. + +stop(ServerHost) -> + Proc = mod_http_upload:get_proc_name(ServerHost, ?PROCNAME), + supervisor:terminate_child(ejabberd_sup, Proc), + supervisor:delete_child(ejabberd_sup, Proc). + +-spec mod_opt_type(atom()) -> fun((term()) -> term()) | [atom()]. + +mod_opt_type(access_soft_quota) -> + fun(A) when is_atom(A) -> A end; +mod_opt_type(access_hard_quota) -> + fun(A) when is_atom(A) -> A end; +mod_opt_type(max_days) -> + fun(I) when is_integer(I), I > 0 -> I; + (infinity) -> infinity + end; +mod_opt_type(_) -> + [access_soft_quota, access_hard_quota, max_days]. + +%%-------------------------------------------------------------------- +%% gen_server callbacks. +%%-------------------------------------------------------------------- + +-spec init({binary(), gen_mod:opts()}) -> {ok, state()}. + +init({ServerHost, Opts}) -> + process_flag(trap_exit, true), + AccessSoftQuota = gen_mod:get_opt(access_soft_quota, Opts, + fun(A) when is_atom(A) -> A end, + soft_upload_quota), + AccessHardQuota = gen_mod:get_opt(access_hard_quota, Opts, + fun(A) when is_atom(A) -> A end, + hard_upload_quota), + MaxDays = gen_mod:get_opt(max_days, Opts, + fun(I) when is_integer(I), I > 0 -> I; + (infinity) -> infinity + end, + infinity), + DocRoot1 = gen_mod:get_module_opt(ServerHost, mod_http_upload, docroot, + fun iolist_to_binary/1, + <<"@HOME@/upload">>), + DocRoot2 = mod_http_upload:expand_home(str:strip(DocRoot1, right, $/)), + DocRoot3 = mod_http_upload:expand_host(DocRoot2, ServerHost), + Timers = if MaxDays == infinity -> []; + true -> + {ok, T1} = timer:send_after(?INITIAL_TIMEOUT, sweep), + {ok, T2} = timer:send_interval(?TIMEOUT, sweep), + [T1, T2] + end, + ejabberd_hooks:add(http_upload_slot_request, ServerHost, ?MODULE, + handle_slot_request, 50), + {ok, #state{server_host = ServerHost, + access_soft_quota = AccessSoftQuota, + access_hard_quota = AccessHardQuota, + max_days = MaxDays, + docroot = DocRoot3, + timers = Timers}}. + +-spec handle_call(_, {pid(), _}, state()) -> {noreply, state()}. + +handle_call(Request, From, State) -> + ?ERROR_MSG("Got unexpected request from ~p: ~p", [From, Request]), + {noreply, State}. + +-spec handle_cast(_, state()) -> {noreply, state()}. + +handle_cast({handle_slot_request, #jid{user = U, server = S} = JID, Path, Size}, + #state{server_host = ServerHost, + access_soft_quota = AccessSoftQuota, + access_hard_quota = AccessHardQuota, + disk_usage = DiskUsage} = State) -> + HardQuota = case acl:match_rule(ServerHost, AccessHardQuota, JID) of + Hard when is_integer(Hard), Hard > 0 -> + Hard * 1024 * 1024; + _ -> + 0 + end, + SoftQuota = case acl:match_rule(ServerHost, AccessSoftQuota, JID) of + Soft when is_integer(Soft), Soft > 0 -> + Soft * 1024 * 1024; + _ -> + 0 + end, + OldSize = case maps:find({U, S}, DiskUsage) of + {ok, Value} -> + Value; + error -> + undefined + end, + NewSize = case {HardQuota, SoftQuota} of + {0, 0} -> + ?DEBUG("No quota specified for ~s", + [jid:to_string(JID)]), + undefined; + {0, _} -> + ?WARNING_MSG("No hard quota specified for ~s", + [jid:to_string(JID)]), + enforce_quota(Path, Size, OldSize, SoftQuota, SoftQuota); + {_, 0} -> + ?WARNING_MSG("No soft quota specified for ~s", + [jid:to_string(JID)]), + enforce_quota(Path, Size, OldSize, HardQuota, HardQuota); + _ when SoftQuota > HardQuota -> + ?WARNING_MSG("Bad quota for ~s (soft: ~p, hard: ~p)", + [jid:to_string(JID), + SoftQuota, HardQuota]), + enforce_quota(Path, Size, OldSize, SoftQuota, SoftQuota); + _ -> + ?DEBUG("Enforcing quota for ~s", + [jid:to_string(JID)]), + enforce_quota(Path, Size, OldSize, SoftQuota, HardQuota) + end, + NewDiskUsage = if is_integer(NewSize) -> + maps:put({U, S}, NewSize, DiskUsage); + true -> + DiskUsage + end, + {noreply, State#state{disk_usage = NewDiskUsage}}; +handle_cast(Request, State) -> + ?ERROR_MSG("Got unexpected request: ~p", [Request]), + {noreply, State}. + +-spec handle_info(_, state()) -> {noreply, state()}. + +handle_info(sweep, #state{server_host = ServerHost, + docroot = DocRoot, + max_days = MaxDays} = State) + when is_integer(MaxDays), MaxDays > 0 -> + ?DEBUG("Got 'sweep' message for ~s", [ServerHost]), + case file:list_dir(DocRoot) of + {ok, Entries} -> + BackThen = secs_since_epoch() - (MaxDays * 86400), + DocRootS = binary_to_list(DocRoot), + PathNames = lists:map(fun(Entry) -> + DocRootS ++ "/" ++ Entry + end, Entries), + UserDirs = lists:filter(fun filelib:is_dir/1, PathNames), + lists:foreach(fun(UserDir) -> + delete_old_files(UserDir, BackThen) + end, UserDirs); + {error, Error} -> + ?ERROR_MSG("Cannot open document root ~s: ~s", + [DocRoot, ?FORMAT(Error)]) + end, + {noreply, State}; +handle_info(Info, State) -> + ?ERROR_MSG("Got unexpected info: ~p", [Info]), + {noreply, State}. + +-spec terminate(normal | shutdown | {shutdown, _} | _, _) -> ok. + +terminate(Reason, #state{server_host = ServerHost, timers = Timers}) -> + ?DEBUG("Stopping upload quota process for ~s: ~p", [ServerHost, Reason]), + ejabberd_hooks:delete(http_upload_slot_request, ServerHost, ?MODULE, + handle_slot_request, 50), + lists:foreach(fun(Timer) -> timer:cancel(Timer) end, Timers). + +-spec code_change({down, _} | _, state(), _) -> {ok, state()}. + +code_change(_OldVsn, #state{server_host = ServerHost} = State, _Extra) -> + ?DEBUG("Updating upload quota process for ~s", [ServerHost]), + {ok, State}. + +%%-------------------------------------------------------------------- +%% ejabberd_hooks callback. +%%-------------------------------------------------------------------- + +-spec handle_slot_request(term(), jid(), binary(), non_neg_integer(), binary()) + -> term(). + +handle_slot_request(allow, #jid{lserver = ServerHost} = JID, Path, Size, + _Lang) -> + Proc = mod_http_upload:get_proc_name(ServerHost, ?PROCNAME), + ?GEN_SERVER:cast(Proc, {handle_slot_request, JID, Path, Size}), + allow; +handle_slot_request(Acc, _JID, _Path, _Size, _Lang) -> Acc. + +%%-------------------------------------------------------------------- +%% Internal functions. +%%-------------------------------------------------------------------- + +-spec enforce_quota(file:filename_all(), non_neg_integer(), + non_neg_integer() | undefined, non_neg_integer(), + non_neg_integer()) + -> non_neg_integer(). + +enforce_quota(_UserDir, SlotSize, OldSize, _MinSize, MaxSize) + when is_integer(OldSize), OldSize + SlotSize =< MaxSize -> + OldSize + SlotSize; +enforce_quota(UserDir, SlotSize, _OldSize, MinSize, MaxSize) -> + Files = lists:sort(fun({_PathA, _SizeA, TimeA}, {_PathB, _SizeB, TimeB}) -> + TimeA > TimeB + end, gather_file_info(UserDir)), + {DelFiles, OldSize, NewSize} = + lists:foldl(fun({_Path, Size, _Time}, {[], AccSize, AccSize}) + when AccSize + Size + SlotSize =< MinSize -> + {[], AccSize + Size, AccSize + Size}; + ({Path, Size, _Time}, {[], AccSize, AccSize}) -> + {[Path], AccSize + Size, AccSize}; + ({Path, Size, _Time}, {AccFiles, AccSize, NewSize}) -> + {[Path | AccFiles], AccSize + Size, NewSize} + end, {[], 0, 0}, Files), + if OldSize + SlotSize > MaxSize -> + lists:foreach(fun(File) -> del_file_and_dir(File) end, DelFiles), + file:del_dir(UserDir), % In case it's empty, now. + NewSize + SlotSize; + true -> + OldSize + SlotSize + end. + +-spec delete_old_files(file:filename_all(), integer()) -> ok. + +delete_old_files(UserDir, CutOff) -> + FileInfo = gather_file_info(UserDir), + case [Path || {Path, _Size, Time} <- FileInfo, Time < CutOff] of + [] -> + ok; + OldFiles -> + lists:foreach(fun(File) -> del_file_and_dir(File) end, OldFiles), + file:del_dir(UserDir) % In case it's empty, now. + end. + +-spec gather_file_info(file:filename_all()) + -> [{binary(), non_neg_integer(), non_neg_integer()}]. + +gather_file_info(Dir) when is_binary(Dir) -> + gather_file_info(binary_to_list(Dir)); +gather_file_info(Dir) -> + case file:list_dir(Dir) of + {ok, Entries} -> + lists:foldl(fun(Entry, Acc) -> + Path = Dir ++ "/" ++ Entry, + case file:read_file_info(Path, + [{time, posix}]) of + {ok, #file_info{type = directory}} -> + gather_file_info(Path) ++ Acc; + {ok, #file_info{type = regular, + mtime = Time, + size = Size}} -> + [{Path, Size, Time} | Acc]; + {ok, _Info} -> + ?DEBUG("Won't stat(2) non-regular file ~s", + [Path]), + Acc; + {error, Error} -> + ?ERROR_MSG("Cannot stat(2) ~s: ~s", + [Path, ?FORMAT(Error)]), + Acc + end + end, [], Entries); + {error, enoent} -> + ?DEBUG("Directory ~s doesn't exist", [Dir]), + []; + {error, Error} -> + ?ERROR_MSG("Cannot open directory ~s: ~s", [Dir, ?FORMAT(Error)]), + [] + end. + +-spec del_file_and_dir(file:name_all()) -> ok. + +del_file_and_dir(File) -> + case file:delete(File) of + ok -> + ?INFO_MSG("Removed ~s", [File]), + Dir = filename:dirname(File), + case file:del_dir(Dir) of + ok -> + ?DEBUG("Removed ~s", [Dir]); + {error, Error} -> + ?DEBUG("Cannot remove ~s: ~s", [Dir, ?FORMAT(Error)]) + end; + {error, Error} -> + ?WARNING_MSG("Cannot remove ~s: ~s", [File, ?FORMAT(Error)]) + end. + +-spec secs_since_epoch() -> non_neg_integer(). + +secs_since_epoch() -> + {MegaSecs, Secs, _MicroSecs} = os:timestamp(), + MegaSecs * 1000000 + Secs. diff --git a/src/mod_ip_blacklist.erl b/src/mod_ip_blacklist.erl index 6225343c..a6f9f619 100644 --- a/src/mod_ip_blacklist.erl +++ b/src/mod_ip_blacklist.erl @@ -7,7 +7,7 @@ %%% {mod_ip_blacklist, []} %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -36,8 +36,7 @@ -export([update_bl_c2s/0]). -%% Hooks: --export([is_ip_in_c2s_blacklist/3]). +-export([is_ip_in_c2s_blacklist/3, mod_opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -129,3 +128,5 @@ is_ip_in_c2s_blacklist(_Val, _IP, _Lang) -> false. %% - For now, we do not kick user already logged on a given IP after %% we update the blacklist. + +mod_opt_type(_) -> []. diff --git a/src/mod_irc.erl b/src/mod_irc.erl index e96fc4dc..d5cd0135 100644 --- a/src/mod_irc.erl +++ b/src/mod_irc.erl @@ -5,7 +5,7 @@ %%% Created : 15 Feb 2003 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -35,9 +35,9 @@ -export([start_link/2, start/2, stop/1, export/1, import/1, import/3, closed_connection/3, get_connection_params/3]). -%% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, - handle_info/2, terminate/2, code_change/3]). + handle_info/2, terminate/2, code_change/3, + mod_opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -46,12 +46,16 @@ -include("adhoc.hrl"). --define(DEFAULT_IRC_ENCODING, <<"iso8859-1">>). +-define(DEFAULT_IRC_ENCODING, <<"iso8859-15">>). -define(DEFAULT_IRC_PORT, 6667). +-define(DEFAULT_REALNAME, <<"WebIRC-User">>). + +-define(DEFAULT_WEBIRC_PASSWORD, <<"">>). + -define(POSSIBLE_ENCODINGS, - [<<"koi8-r">>, <<"iso8859-1">>, <<"iso8859-2">>, + [<<"koi8-r">>, <<"iso8859-15">>, <<"iso8859-1">>, <<"iso8859-2">>, <<"utf-8">>, <<"utf-8+latin-1">>]). -type conn_param() :: {binary(), binary(), inet:port_number(), binary()} | @@ -112,7 +116,7 @@ stop(Host) -> %% Description: Initiates the server %%-------------------------------------------------------------------- init([Host, Opts]) -> - ejabberd:start_app(p1_iconv), + ejabberd:start_app(iconv), MyHost = gen_mod:get_opt_host(Host, Opts, <<"irc.@HOST@">>), case gen_mod:db_type(Host, Opts) of @@ -129,7 +133,7 @@ init([Host, Opts]) -> catch ets:new(irc_connection, [named_table, public, {keypos, #irc_connection.jid_server_host}]), - ejabberd_router:register_route(MyHost), + ejabberd_router:register_route(MyHost, Host), {ok, #state{host = MyHost, server_host = Host, access = Access}}. @@ -212,7 +216,7 @@ do_route(Host, ServerHost, Access, From, To, Packet) -> allow -> do_route1(Host, ServerHost, From, To, Packet); _ -> #xmlel{attrs = Attrs} = Packet, - Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), + Lang = fxml:get_attr_s(<<"xml:lang">>, Attrs), ErrText = <<"Access denied by service policy">>, Err = jlib:make_error_reply(Packet, ?ERRT_FORBIDDEN(Lang, ErrText)), @@ -230,7 +234,7 @@ do_route1(Host, ServerHost, From, To, Packet) -> #iq{type = get, xmlns = (?NS_DISCO_INFO) = XMLNS, sub_el = SubEl, lang = Lang} = IQ -> - Node = xml:get_tag_attr_s(<<"node">>, SubEl), + Node = fxml:get_tag_attr_s(<<"node">>, SubEl), Info = ejabberd_hooks:run_fold(disco_info, ServerHost, [], [ServerHost, ?MODULE, @@ -258,7 +262,7 @@ do_route1(Host, ServerHost, From, To, Packet) -> #iq{type = get, xmlns = (?NS_DISCO_ITEMS) = XMLNS, sub_el = SubEl, lang = Lang} = IQ -> - Node = xml:get_tag_attr_s(<<"node">>, SubEl), + Node = fxml:get_tag_attr_s(<<"node">>, SubEl), case Node of <<>> -> ResIQ = IQ#iq{type = result, @@ -379,11 +383,15 @@ do_route1(Host, ServerHost, From, To, Packet) -> %% username part of the JID). _ -> Username end, + Ident = extract_ident(Packet), + RemoteAddr = extract_ip_address(Packet), + RealName = get_realname(ServerHost), + WebircPassword = get_webirc_password(ServerHost), {ok, Pid} = mod_irc_connection:start(From, Host, ServerHost, Server, ConnectionUsername, Encoding, Port, - Password, ?MODULE), + Password, Ident, RemoteAddr, RealName, WebircPassword, ?MODULE), ets:insert(irc_connection, #irc_connection{jid_server_host = {From, Server, Host}, @@ -466,7 +474,7 @@ iq_get_vcard(Lang) -> [{xmlcdata, <<(translate:translate(Lang, <<"ejabberd IRC module">>))/binary, - "\nCopyright (c) 2003-2015 ProcessOne">>}]}]. + "\nCopyright (c) 2003-2016 ProcessOne">>}]}]. command_items(ServerHost, Host, Lang) -> lists:map(fun ({Node, Name, _Function}) -> @@ -508,7 +516,7 @@ find_xdata_el1([]) -> false; find_xdata_el1([#xmlel{name = Name, attrs = Attrs, children = SubEls} | Els]) -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of + case fxml:get_attr_s(<<"xmlns">>, Attrs) of ?NS_XDATA -> #xmlel{name = Name, attrs = Attrs, children = SubEls}; _ -> find_xdata_el1(Els) @@ -527,7 +535,7 @@ process_irc_register(ServerHost, Host, From, _To, IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ACCEPTABLE]}; #xmlel{attrs = Attrs} -> - case xml:get_attr_s(<<"type">>, Attrs) of + case fxml:get_attr_s(<<"type">>, Attrs) of <<"cancel">> -> IQ#iq{type = result, sub_el = @@ -541,7 +549,7 @@ process_irc_register(ServerHost, Host, From, _To, IQ#iq{type = error, sub_el = [SubEl, ?ERR_BAD_REQUEST]}; _ -> - Node = str:tokens(xml:get_tag_attr_s(<<"node">>, + Node = str:tokens(fxml:get_tag_attr_s(<<"node">>, SubEl), <<"/">>), case set_form(ServerHost, Host, From, Node, Lang, @@ -563,7 +571,7 @@ process_irc_register(ServerHost, Host, From, _To, end end; get -> - Node = str:tokens(xml:get_tag_attr_s(<<"node">>, SubEl), + Node = str:tokens(fxml:get_tag_attr_s(<<"node">>, SubEl), <<"/">>), case get_form(ServerHost, Host, From, Node, Lang) of {result, Res} -> @@ -578,7 +586,7 @@ process_irc_register(ServerHost, Host, From, _To, end. get_data(ServerHost, Host, From) -> - LServer = jlib:nameprep(ServerHost), + LServer = jid:nameprep(ServerHost), get_data(LServer, Host, From, gen_mod:db_type(LServer, ?MODULE)). @@ -604,7 +612,7 @@ get_data(LServer, Host, From, riak) -> end; get_data(LServer, Host, From, odbc) -> SJID = - ejabberd_odbc:escape(jlib:jid_to_string(jlib:jid_tolower(jlib:jid_remove_resource(From)))), + ejabberd_odbc:escape(jid:to_string(jid:tolower(jid:remove_resource(From)))), SHost = ejabberd_odbc:escape(Host), case catch ejabberd_odbc:sql_query(LServer, [<<"select data from irc_custom where jid='">>, @@ -722,12 +730,12 @@ get_form(_ServerHost, _Host, _, _, _Lang) -> {error, ?ERR_SERVICE_UNAVAILABLE}. set_data(ServerHost, Host, From, Data) -> - LServer = jlib:nameprep(ServerHost), + LServer = jid:nameprep(ServerHost), set_data(LServer, Host, From, data_to_binary(From, Data), gen_mod:db_type(LServer, ?MODULE)). set_data(_LServer, Host, From, Data, mnesia) -> - {LUser, LServer, _} = jlib:jid_tolower(From), + {LUser, LServer, _} = jid:tolower(From), US = {LUser, LServer}, F = fun () -> mnesia:write(#irc_custom{us_host = {US, Host}, @@ -735,14 +743,14 @@ set_data(_LServer, Host, From, Data, mnesia) -> end, mnesia:transaction(F); set_data(LServer, Host, From, Data, riak) -> - {LUser, LServer, _} = jlib:jid_tolower(From), + {LUser, LServer, _} = jid:tolower(From), US = {LUser, LServer}, {atomic, ejabberd_riak:put(#irc_custom{us_host = {US, Host}, data = Data}, irc_custom_schema())}; set_data(LServer, Host, From, Data, odbc) -> SJID = - ejabberd_odbc:escape(jlib:jid_to_string(jlib:jid_tolower(jlib:jid_remove_resource(From)))), + ejabberd_odbc:escape(jid:to_string(jid:tolower(jid:remove_resource(From)))), SHost = ejabberd_odbc:escape(Host), SData = ejabberd_odbc:encode_term(Data), F = fun () -> @@ -784,8 +792,6 @@ set_form(ServerHost, Host, From, [], _Lang, XData) -> set_form(_ServerHost, _Host, _, _, _Lang, _XData) -> {error, ?ERR_SERVICE_UNAVAILABLE}. -%% Host = "irc.example.com" -%% ServerHost = "example.com" get_connection_params(Host, From, IRCServer) -> [_ | HostTail] = str:tokens(Host, <<".">>), ServerHost = str:join(HostTail, <<".">>), @@ -801,6 +807,12 @@ get_default_encoding(ServerHost) -> [ServerHost, Result]), Result. +get_realname(ServerHost) -> + gen_mod:get_module_opt(ServerHost, ?MODULE, realname, fun iolist_to_binary/1, ?DEFAULT_REALNAME). + +get_webirc_password(ServerHost) -> + gen_mod:get_module_opt(ServerHost, ?MODULE, webirc_password, fun iolist_to_binary/1, ?DEFAULT_WEBIRC_PASSWORD). + get_connection_params(Host, ServerHost, From, IRCServer) -> #jid{user = User, server = _Server} = From, @@ -923,7 +935,7 @@ adhoc_join(From, To, <<"invite">>, attrs = [{<<"from">>, - jlib:jid_to_string(From)}], + jid:to_string(From)}], children = [#xmlel{name = @@ -954,7 +966,7 @@ adhoc_join(From, To, Lang, <<"Join the IRC channel in this Jabber ID: ~s">>), [RoomJID]))}]}]}, - ejabberd_router:route(jlib:string_to_jid(RoomJID), From, + ejabberd_router:route(jid:from_string(RoomJID), From, Invite), adhoc:produce_response(Request, #adhoc_response{status = @@ -1250,7 +1262,7 @@ data_to_binary(JID, Data) -> ?ERROR_MSG("failed to convert " "parameter ~p for user ~s", [Param, - jlib:jid_to_string(JID)]); + jid:to_string(JID)]); true -> ?ERROR_MSG("failed to convert " "parameter ~p", @@ -1297,7 +1309,7 @@ update_table() -> fun(#irc_custom{us_host = {_, H}}) -> H end, fun(#irc_custom{us_host = {{U, S}, H}, data = Data} = R) -> - JID = jlib:make_jid(U, S, <<"">>), + JID = jid:make(U, S, <<"">>), R#irc_custom{us_host = {{iolist_to_binary(U), iolist_to_binary(S)}, iolist_to_binary(H)}, @@ -1315,8 +1327,8 @@ export(_Server) -> case str:suffix(Host, IRCHost) of true -> SJID = ejabberd_odbc:escape( - jlib:jid_to_string( - jlib:make_jid(U, S, <<"">>))), + jid:to_string( + jid:make(U, S, <<"">>))), SIRCHost = ejabberd_odbc:escape(IRCHost), SData = ejabberd_odbc:encode_term(Data), [[<<"delete from irc_custom where jid='">>, SJID, @@ -1333,7 +1345,7 @@ export(_Server) -> import(_LServer) -> [{<<"select jid, host, data from irc_custom;">>, fun([SJID, IRCHost, SData]) -> - #jid{luser = U, lserver = S} = jlib:string_to_jid(SJID), + #jid{luser = U, lserver = S} = jid:from_string(SJID), Data = ejabberd_odbc:decode_term(SData), #irc_custom{us_host = {{U, S}, IRCHost}, data = Data} @@ -1345,3 +1357,38 @@ import(_LServer, riak, #irc_custom{} = R) -> ejabberd_riak:put(R, irc_custom_schema()); import(_, _, _) -> pass. + +mod_opt_type(access) -> + fun (A) when is_atom(A) -> A end; +mod_opt_type(db_type) -> fun gen_mod:v_db/1; +mod_opt_type(default_encoding) -> + fun iolist_to_binary/1; +mod_opt_type(host) -> fun iolist_to_binary/1; +mod_opt_type(_) -> + [access, db_type, default_encoding, host]. + +extract_ident(Packet) -> + case fxml:get_subtag(Packet, <<"headers">>) of + {xmlel, _Name, _Attrs, Headers} -> + extract_header(<<"X-Irc-Ident">>, Headers); + _ -> + "chatmovil" + end. + +extract_ip_address(Packet) -> + case fxml:get_subtag(Packet, <<"headers">>) of + {xmlel, _Name, _Attrs, Headers} -> + extract_header(<<"X-Forwarded-For">>, Headers); + _ -> + "127.0.0.1" + end. + +extract_header(HeaderName, [{xmlel, _Name, _Attrs, [{xmlcdata, Value}]} | Tail]) -> + case fxml:get_attr(<<"name">>, _Attrs) of + {value, HeaderName} -> + binary_to_list(Value); + _ -> + extract_header(HeaderName, Tail) + end; +extract_header(_HeaderName, _Headers) -> + false. diff --git a/src/mod_irc_connection.erl b/src/mod_irc_connection.erl index cc21b0f1..098c8c28 100644 --- a/src/mod_irc_connection.erl +++ b/src/mod_irc_connection.erl @@ -5,7 +5,7 @@ %%% Created : 15 Feb 2003 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -30,7 +30,7 @@ -behaviour(gen_fsm). %% External exports --export([start_link/8, start/9, route_chan/4, +-export([start_link/12, start/13, route_chan/4, route_nick/3]). %% gen_fsm callbacks @@ -55,9 +55,13 @@ user = #jid{} :: jid(), host = <<"">> :: binary(), server = <<"">> :: binary(), + remoteAddr = <<"">> :: binary(), + ident = <<"">> :: binary(), + realname = <<"">> :: binary(), nick = <<"">> :: binary(), channels = dict:new() :: ?TDICT, nickchannel :: binary(), + webirc_password :: binary(), mod = mod_irc :: atom(), inbuf = <<"">> :: binary(), outbuf = <<"">> :: binary()}). @@ -78,18 +82,18 @@ -endif. start(From, Host, ServerHost, Server, Username, - Encoding, Port, Password, Mod) -> + Encoding, Port, Password, Ident, RemoteAddr, RealName, WebircPassword, Mod) -> Supervisor = gen_mod:get_module_proc(ServerHost, ejabberd_mod_irc_sup), supervisor:start_child(Supervisor, [From, Host, Server, Username, Encoding, Port, - Password, Mod]). + Password, Ident, RemoteAddr, RealName, WebircPassword, Mod]). start_link(From, Host, Server, Username, Encoding, Port, - Password, Mod) -> + Password, Ident, RemoteAddr, RealName, WebircPassword, Mod) -> gen_fsm:start_link(?MODULE, [From, Host, Server, Username, Encoding, Port, Password, - Mod], + Ident, RemoteAddr, RealName, WebircPassword, Mod], ?FSMOPTS). %%%---------------------------------------------------------------------- @@ -104,13 +108,14 @@ start_link(From, Host, Server, Username, Encoding, Port, %% {stop, StopReason} %%---------------------------------------------------------------------- init([From, Host, Server, Username, Encoding, Port, - Password, Mod]) -> + Password, Ident, RemoteAddr, RealName, WebircPassword, Mod]) -> gen_fsm:send_event(self(), init), {ok, open_socket, #state{queue = queue:new(), mod = Mod, encoding = Encoding, port = Port, password = Password, user = From, nick = Username, host = Host, - server = Server}}. + server = Server, ident = Ident, realname = RealName, + remoteAddr = RemoteAddr, webirc_password = WebircPassword }}. %%---------------------------------------------------------------------- %% Func: StateName/2 @@ -122,6 +127,10 @@ open_socket(init, StateData) -> Addr = StateData#state.server, Port = StateData#state.port, ?DEBUG("Connecting with IPv6 to ~s:~p", [Addr, Port]), + From = StateData#state.user, + #jid{user = JidUser, server = JidServer, resource = JidResource} = From, + UserIP = ejabberd_sm:get_user_ip(JidUser, JidServer, JidResource), + UserIPStr = inet_parse:ntoa(element(1, UserIP)), Connect6 = gen_tcp:connect(binary_to_list(Addr), Port, [inet6, binary, {packet, 0}]), Connect = case Connect6 of @@ -136,6 +145,8 @@ open_socket(init, StateData) -> case Connect of {ok, Socket} -> NewStateData = StateData#state{socket = Socket}, + send_text(NewStateData, + io_lib:format("WEBIRC ~s ~s ~s ~s\r\n", [StateData#state.webirc_password, JidResource, UserIPStr, UserIPStr])), if StateData#state.password /= <<"">> -> send_text(NewStateData, io_lib:format("PASS ~s\r\n", @@ -146,9 +157,10 @@ open_socket(init, StateData) -> io_lib:format("NICK ~s\r\n", [StateData#state.nick])), send_text(NewStateData, io_lib:format("USER ~s ~s ~s :~s\r\n", - [StateData#state.nick, StateData#state.nick, + [StateData#state.ident, + StateData#state.nick, StateData#state.host, - StateData#state.nick])), + StateData#state.realname])), {next_state, wait_for_registration, NewStateData}; {error, Reason} -> ?DEBUG("connect return ~p~n", [Reason]), @@ -221,7 +233,7 @@ get_password_from_presence(#xmlel{name = <<"presence">>, case lists:filter(fun (El) -> case El of #xmlel{name = <<"x">>, attrs = Attrs} -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of + case fxml:get_attr_s(<<"xmlns">>, Attrs) of ?NS_MUC -> true; _ -> false end; @@ -231,9 +243,9 @@ get_password_from_presence(#xmlel{name = <<"presence">>, Els) of [ElXMUC | _] -> - case xml:get_subtag(ElXMUC, <<"password">>) of + case fxml:get_subtag(ElXMUC, <<"password">>) of #xmlel{name = <<"password">>} = PasswordTag -> - {true, xml:get_tag_cdata(PasswordTag)}; + {true, fxml:get_tag_cdata(PasswordTag)}; _ -> false end; _ -> false @@ -249,7 +261,7 @@ handle_info({route_chan, Channel, Resource, #xmlel{name = <<"presence">>, attrs = Attrs} = Presence}, StateName, StateData) -> - NewStateData = case xml:get_attr_s(<<"type">>, Attrs) of + NewStateData = case fxml:get_attr_s(<<"type">>, Attrs) of <<"unavailable">> -> send_stanza_unavailable(Channel, StateData), S1 = (?SEND((io_lib:format("PART #~s\r\n", @@ -300,20 +312,20 @@ handle_info({route_chan, Channel, Resource, handle_info({route_chan, Channel, Resource, #xmlel{name = <<"message">>, attrs = Attrs} = El}, StateName, StateData) -> - NewStateData = case xml:get_attr_s(<<"type">>, Attrs) of + NewStateData = case fxml:get_attr_s(<<"type">>, Attrs) of <<"groupchat">> -> - case xml:get_path_s(El, [{elem, <<"subject">>}, cdata]) + case fxml:get_path_s(El, [{elem, <<"subject">>}, cdata]) of <<"">> -> ejabberd_router:route( - jlib:make_jid( + jid:make( iolist_to_binary([Channel, <<"%">>, StateData#state.server]), StateData#state.host, StateData#state.nick), StateData#state.user, El), - Body = xml:get_path_s(El, + Body = fxml:get_path_s(El, [{elem, <<"body">>}, cdata]), case Body of @@ -374,7 +386,7 @@ handle_info({route_chan, Channel, Resource, when Type == <<"chat">>; Type == <<"">>; Type == <<"normal">> -> - Body = xml:get_path_s(El, [{elem, <<"body">>}, cdata]), + Body = fxml:get_path_s(El, [{elem, <<"body">>}, cdata]), case Body of <<"/quote ", Rest/binary>> -> ?SEND(<<Rest/binary, "\r\n">>); @@ -430,7 +442,7 @@ handle_info({route_chan, Channel, Resource, #xmlel{name = <<"iq">>} = El}, StateName, StateData) -> From = StateData#state.user, - To = jlib:make_jid(iolist_to_binary([Channel, <<"%">>, + To = jid:make(iolist_to_binary([Channel, <<"%">>, StateData#state.server]), StateData#state.host, StateData#state.nick), _ = case jlib:iq_query_info(El) of @@ -469,9 +481,9 @@ handle_info({route_chan, _Channel, _Resource, _Packet}, handle_info({route_nick, Nick, #xmlel{name = <<"message">>, attrs = Attrs} = El}, StateName, StateData) -> - NewStateData = case xml:get_attr_s(<<"type">>, Attrs) of + NewStateData = case fxml:get_attr_s(<<"type">>, Attrs) of <<"chat">> -> - Body = xml:get_path_s(El, [{elem, <<"body">>}, cdata]), + Body = fxml:get_path_s(El, [{elem, <<"body">>}, cdata]), case Body of <<"/quote ", Rest/binary>> -> ?SEND(<<Rest/binary, "\r\n">>); @@ -716,7 +728,7 @@ terminate(_Reason, _StateName, FullStateData) -> send_stanza(Chan, StateData, Stanza) -> ejabberd_router:route( - jlib:make_jid( + jid:make( iolist_to_binary([Chan, <<"%">>, StateData#state.server]), @@ -766,13 +778,13 @@ bounce_messages(Reason) -> receive {send_element, El} -> #xmlel{attrs = Attrs} = El, - case xml:get_attr_s(<<"type">>, Attrs) of + case fxml:get_attr_s(<<"type">>, Attrs) of <<"error">> -> ok; _ -> Err = jlib:make_error_reply(El, <<"502">>, Reason), - From = jlib:string_to_jid(xml:get_attr_s(<<"from">>, + From = jid:from_string(fxml:get_attr_s(<<"from">>, Attrs)), - To = jlib:string_to_jid(xml:get_attr_s(<<"to">>, + To = jid:from_string(fxml:get_attr_s(<<"to">>, Attrs)), ejabberd_router:route(To, From, Err) end, @@ -830,7 +842,7 @@ process_channel_list_user(StateData, Chan, User) -> {U2, <<"admin">>, <<"moderator">>}; _ -> {User1, <<"member">>, <<"participant">>} end, - ejabberd_router:route(jlib:make_jid(iolist_to_binary([Chan, + ejabberd_router:route(jid:make(iolist_to_binary([Chan, <<"%">>, StateData#state.server]), StateData#state.host, User2), @@ -860,7 +872,7 @@ process_channel_topic(StateData, Chan, String) -> Msg = ejabberd_regexp:replace(String, <<".*332[^:]*:">>, <<"">>), Msg1 = filter_message(Msg), - ejabberd_router:route(jlib:make_jid(iolist_to_binary([Chan, + ejabberd_router:route(jid:make(iolist_to_binary([Chan, <<"%">>, StateData#state.server]), StateData#state.host, <<"">>), @@ -889,7 +901,7 @@ process_channel_topic_who(StateData, Chan, String) -> _ -> String end, Msg2 = filter_message(Msg1), - ejabberd_router:route(jlib:make_jid(iolist_to_binary([Chan, + ejabberd_router:route(jid:make(iolist_to_binary([Chan, <<"%">>, StateData#state.server]), StateData#state.host, <<"">>), @@ -921,7 +933,7 @@ process_nick_in_use(StateData, String) -> % Shouldn't happen with a well behaved server StateData; Chan -> - ejabberd_router:route(jlib:make_jid(iolist_to_binary([Chan, + ejabberd_router:route(jid:make(iolist_to_binary([Chan, <<"%">>, StateData#state.server]), StateData#state.host, @@ -938,7 +950,7 @@ process_num_error(StateData, String) -> <<"continue">>), lists:foreach(fun (Chan) -> ejabberd_router:route( - jlib:make_jid( + jid:make( iolist_to_binary( [Chan, <<"%">>, @@ -956,7 +968,7 @@ process_num_error(StateData, String) -> StateData. process_endofwhois(StateData, _String, Nick) -> - ejabberd_router:route(jlib:make_jid(iolist_to_binary([Nick, + ejabberd_router:route(jid:make(iolist_to_binary([Nick, <<"!">>, StateData#state.server]), StateData#state.host, <<"">>), @@ -973,7 +985,7 @@ process_whois311(StateData, String, Nick, Ident, Irchost) -> Fullname = ejabberd_regexp:replace(String, <<".*311[^:]*:">>, <<"">>), - ejabberd_router:route(jlib:make_jid(iolist_to_binary([Nick, + ejabberd_router:route(jid:make(iolist_to_binary([Nick, <<"!">>, StateData#state.server]), StateData#state.host, <<"">>), @@ -997,7 +1009,7 @@ process_whois311(StateData, String, Nick, Ident, process_whois312(StateData, String, Nick, Ircserver) -> Ircserverdesc = ejabberd_regexp:replace(String, <<".*312[^:]*:">>, <<"">>), - ejabberd_router:route(jlib:make_jid(iolist_to_binary([Nick, + ejabberd_router:route(jid:make(iolist_to_binary([Nick, <<"!">>, StateData#state.server]), StateData#state.host, <<"">>), @@ -1019,7 +1031,7 @@ process_whois312(StateData, String, Nick, Ircserver) -> process_whois319(StateData, String, Nick) -> Chanlist = ejabberd_regexp:replace(String, <<".*319[^:]*:">>, <<"">>), - ejabberd_router:route(jlib:make_jid(iolist_to_binary( + ejabberd_router:route(jid:make(iolist_to_binary( [Nick, <<"!">>, StateData#state.server]), @@ -1047,7 +1059,7 @@ process_chanprivmsg(StateData, Chan, From, String) -> _ -> Msg end, Msg2 = filter_message(Msg1), - ejabberd_router:route(jlib:make_jid(iolist_to_binary( + ejabberd_router:route(jid:make(iolist_to_binary( [Chan, <<"%">>, StateData#state.server]), @@ -1069,7 +1081,7 @@ process_channotice(StateData, Chan, From, String) -> _ -> <<"/me NOTICE: ", Msg/binary>> end, Msg2 = filter_message(Msg1), - ejabberd_router:route(jlib:make_jid(iolist_to_binary( + ejabberd_router:route(jid:make(iolist_to_binary( [Chan, <<"%">>, StateData#state.server]), @@ -1091,7 +1103,7 @@ process_privmsg(StateData, _Nick, From, String) -> _ -> Msg end, Msg2 = filter_message(Msg1), - ejabberd_router:route(jlib:make_jid(iolist_to_binary( + ejabberd_router:route(jid:make(iolist_to_binary( [FromUser, <<"!">>, StateData#state.server]), @@ -1113,7 +1125,7 @@ process_notice(StateData, _Nick, From, String) -> _ -> <<"/me NOTICE: ", Msg/binary>> end, Msg2 = filter_message(Msg1), - ejabberd_router:route(jlib:make_jid(iolist_to_binary( + ejabberd_router:route(jid:make(iolist_to_binary( [FromUser, <<"!">>, StateData#state.server]), @@ -1141,14 +1153,14 @@ process_userinfo(StateData, _Nick, From) -> send_text(StateData, io_lib:format("NOTICE ~s :\001USERINFO xmpp:~s\001\r\n", [FromUser, - jlib:jid_to_string(StateData#state.user)])). + jid:to_string(StateData#state.user)])). process_topic(StateData, Chan, From, String) -> [FromUser | _] = str:tokens(From, <<"!">>), Msg = ejabberd_regexp:replace(String, <<".*TOPIC[^:]*:">>, <<"">>), Msg1 = filter_message(Msg), - ejabberd_router:route(jlib:make_jid(iolist_to_binary( + ejabberd_router:route(jid:make(iolist_to_binary( [Chan, <<"%">>, StateData#state.server]), @@ -1170,7 +1182,7 @@ process_part(StateData, Chan, From, String) -> Msg = ejabberd_regexp:replace(String, <<".*PART[^:]*:">>, <<"">>), Msg1 = filter_message(Msg), - ejabberd_router:route(jlib:make_jid(iolist_to_binary( + ejabberd_router:route(jid:make(iolist_to_binary( [Chan, <<"%">>, StateData#state.server]), @@ -1212,7 +1224,7 @@ process_quit(StateData, From, String) -> dict:map(fun (Chan, Ps) -> case (?SETS):is_member(FromUser, Ps) of true -> - ejabberd_router:route(jlib:make_jid(iolist_to_binary( + ejabberd_router:route(jid:make(iolist_to_binary( [Chan, <<"%">>, StateData#state.server]), @@ -1261,7 +1273,7 @@ process_quit(StateData, From, String) -> process_join(StateData, Channel, From, _String) -> [FromUser | FromIdent] = str:tokens(From, <<"!">>), [Chan | _] = binary:split(Channel, <<":#">>), - ejabberd_router:route(jlib:make_jid(iolist_to_binary( + ejabberd_router:route(jid:make(iolist_to_binary( [Chan, <<"%">>, StateData#state.server]), @@ -1294,7 +1306,7 @@ process_join(StateData, Channel, From, _String) -> process_mode_o(StateData, Chan, _From, Nick, Affiliation, Role) -> - ejabberd_router:route(jlib:make_jid(iolist_to_binary( + ejabberd_router:route(jid:make(iolist_to_binary( [Chan, <<"%">>, StateData#state.server]), @@ -1318,7 +1330,7 @@ process_kick(StateData, Chan, From, Nick, String) -> Msg = lists:last(str:tokens(String, <<":">>)), Msg2 = <<Nick/binary, " kicked by ", From/binary, " (", (filter_message(Msg))/binary, ")">>, - ejabberd_router:route(jlib:make_jid(iolist_to_binary( + ejabberd_router:route(jid:make(iolist_to_binary( [Chan, <<"%">>, StateData#state.server]), @@ -1329,7 +1341,7 @@ process_kick(StateData, Chan, From, Nick, String) -> children = [#xmlel{name = <<"body">>, attrs = [], children = [{xmlcdata, Msg2}]}]}), - ejabberd_router:route(jlib:make_jid(iolist_to_binary( + ejabberd_router:route(jid:make(iolist_to_binary( [Chan, <<"%">>, StateData#state.server]), @@ -1361,7 +1373,7 @@ process_nick(StateData, From, NewNick) -> NewChans = dict:map(fun (Chan, Ps) -> case (?SETS):is_member(FromUser, Ps) of true -> - ejabberd_router:route(jlib:make_jid( + ejabberd_router:route(jid:make( iolist_to_binary( [Chan, <<"%">>, @@ -1408,7 +1420,7 @@ process_nick(StateData, From, NewNick) -> children = []}]}]}), - ejabberd_router:route(jlib:make_jid( + ejabberd_router:route(jid:make( iolist_to_binary( [Chan, <<"%">>, @@ -1456,7 +1468,7 @@ process_nick(StateData, From, NewNick) -> process_error(StateData, String) -> lists:foreach(fun (Chan) -> - ejabberd_router:route(jlib:make_jid( + ejabberd_router:route(jid:make( iolist_to_binary( [Chan, <<"%">>, @@ -1523,14 +1535,14 @@ iq_admin(StateData, Channel, From, To, end. process_iq_admin(StateData, Channel, set, SubEl) -> - case xml:get_subtag(SubEl, <<"item">>) of + case fxml:get_subtag(SubEl, <<"item">>) of false -> {error, ?ERR_BAD_REQUEST}; ItemEl -> - Nick = xml:get_tag_attr_s(<<"nick">>, ItemEl), - Affiliation = xml:get_tag_attr_s(<<"affiliation">>, + Nick = fxml:get_tag_attr_s(<<"nick">>, ItemEl), + Affiliation = fxml:get_tag_attr_s(<<"affiliation">>, ItemEl), - Role = xml:get_tag_attr_s(<<"role">>, ItemEl), - Reason = xml:get_path_s(ItemEl, + Role = fxml:get_tag_attr_s(<<"role">>, ItemEl), + Reason = fxml:get_path_s(ItemEl, [{elem, <<"reason">>}, cdata]), process_admin(StateData, Channel, Nick, Affiliation, Role, Reason) diff --git a/src/mod_last.erl b/src/mod_last.erl index e079a2d3..33f88e02 100644 --- a/src/mod_last.erl +++ b/src/mod_last.erl @@ -5,7 +5,7 @@ %%% Created : 24 Oct 2003 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,14 +25,19 @@ -module(mod_last). +-behaviour(ejabberd_config). + -author('alexey@process-one.net'). +-protocol({xep, 12, '2.0'}). + -behaviour(gen_mod). -export([start/2, stop/1, process_local_iq/3, export/1, - process_sm_iq/3, on_presence_update/4, import/1, import/3, - store_last_info/4, get_last_info/2, remove_user/2, - transform_options/1]). + process_sm_iq/3, on_presence_update/4, import/1, + import/3, store_last_info/4, get_last_info/2, + remove_user/2, transform_options/1, mod_opt_type/1, + opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -107,7 +112,7 @@ get_node_uptime() -> undefined -> trunc(element(1, erlang:statistics(wall_clock)) / 1000); Now -> - now_to_seconds(now()) - Now + p1_time_compat:system_time(seconds) - Now end. now_to_seconds({MegaSecs, Secs, _MicroSecs}) -> @@ -180,18 +185,12 @@ get_last(LUser, LServer, riak) -> Err end; get_last(LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - case catch odbc_queries:get_last(LServer, Username) of - {selected, [<<"seconds">>, <<"state">>], []} -> - not_found; - {selected, [<<"seconds">>, <<"state">>], - [[STimeStamp, Status]]} -> - case catch jlib:binary_to_integer(STimeStamp) of - TimeStamp when is_integer(TimeStamp) -> - {ok, TimeStamp, Status}; - Reason -> {error, {invalid_timestamp, Reason}} - end; - Reason -> {error, {invalid_result, Reason}} + case catch odbc_queries:get_last(LServer, LUser) of + {selected, []} -> + not_found; + {selected, [{TimeStamp, Status}]} -> + {ok, TimeStamp, Status}; + Reason -> {error, {invalid_result, Reason}} end. get_last_iq(IQ, SubEl, LUser, LServer) -> @@ -205,7 +204,7 @@ get_last_iq(IQ, SubEl, LUser, LServer) -> IQ#iq{type = error, sub_el = [SubEl, ?ERR_SERVICE_UNAVAILABLE]}; {ok, TimeStamp, Status} -> - TimeStamp2 = now_to_seconds(now()), + TimeStamp2 = p1_time_compat:system_time(seconds), Sec = TimeStamp2 - TimeStamp, IQ#iq{type = result, sub_el = @@ -227,12 +226,12 @@ get_last_iq(IQ, SubEl, LUser, LServer) -> end. on_presence_update(User, Server, _Resource, Status) -> - TimeStamp = now_to_seconds(now()), + TimeStamp = p1_time_compat:system_time(seconds), store_last_info(User, Server, TimeStamp, Status). store_last_info(User, Server, TimeStamp, Status) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), DBType = gen_mod:db_type(LServer, ?MODULE), store_last_info(LUser, LServer, TimeStamp, Status, DBType). @@ -255,12 +254,7 @@ store_last_info(LUser, LServer, TimeStamp, Status, last_activity_schema())}; store_last_info(LUser, LServer, TimeStamp, Status, odbc) -> - Username = ejabberd_odbc:escape(LUser), - Seconds = - ejabberd_odbc:escape(iolist_to_binary(integer_to_list(TimeStamp))), - State = ejabberd_odbc:escape(Status), - odbc_queries:set_last_t(LServer, Username, Seconds, - State). + odbc_queries:set_last_t(LServer, LUser, TimeStamp, Status). %% @spec (LUser::string(), LServer::string()) -> %% {ok, TimeStamp::integer(), Status::string()} | not_found @@ -271,8 +265,8 @@ get_last_info(LUser, LServer) -> end. remove_user(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), DBType = gen_mod:db_type(LServer, ?MODULE), remove_user(LUser, LServer, DBType). @@ -281,8 +275,7 @@ remove_user(LUser, LServer, mnesia) -> F = fun () -> mnesia:delete({last_activity, US}) end, mnesia:transaction(F); remove_user(LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - odbc_queries:del_last(LServer, Username); + odbc_queries:del_last(LServer, LUser); remove_user(LUser, LServer, riak) -> {atomic, ejabberd_riak:delete(last_activity, {LUser, LServer})}. @@ -349,3 +342,11 @@ transform_options({node_start, {_, _, _} = Now}, Opts) -> [{node_start, now_to_seconds(Now)}|Opts]; transform_options(Opt, Opts) -> [Opt|Opts]. + +mod_opt_type(db_type) -> fun gen_mod:v_db/1; +mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; +mod_opt_type(_) -> [db_type, iqdisc]. + +opt_type(node_start) -> + fun (S) when is_integer(S), S >= 0 -> S end; +opt_type(_) -> [node_start]. diff --git a/src/mod_mam.erl b/src/mod_mam.erl new file mode 100644 index 00000000..38642c0c --- /dev/null +++ b/src/mod_mam.erl @@ -0,0 +1,1421 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeniy Khramtsov <ekhramtsov@process-one.net> +%%% @doc +%%% Message Archive Management (XEP-0313) +%%% @end +%%% Created : 4 Jul 2013 by Evgeniy Khramtsov <ekhramtsov@process-one.net> +%%% +%%% +%%% ejabberd, Copyright (C) 2013-2016 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_mam). + +-protocol({xep, 313, '0.4'}). +-protocol({xep, 334, '0.2'}). + +-behaviour(gen_mod). + +%% API +-export([start/2, stop/1]). + +-export([user_send_packet/4, user_receive_packet/5, + process_iq_v0_2/3, process_iq_v0_3/3, disco_sm_features/5, + remove_user/2, remove_user/3, mod_opt_type/1, muc_process_iq/4, + muc_filter_message/5, message_is_archived/5, delete_old_messages/2, + get_commands_spec/0]). + +-include_lib("stdlib/include/ms_transform.hrl"). +-include("jlib.hrl"). +-include("logger.hrl"). +-include("mod_muc_room.hrl"). +-include("ejabberd_commands.hrl"). + +-define(DEF_PAGE_SIZE, 50). +-define(MAX_PAGE_SIZE, 250). + +-define(BIN_GREATER_THAN(A, B), + ((A > B andalso byte_size(A) == byte_size(B)) + orelse byte_size(A) > byte_size(B))). +-define(BIN_LESS_THAN(A, B), + ((A < B andalso byte_size(A) == byte_size(B)) + orelse byte_size(A) < byte_size(B))). + +-record(archive_msg, + {us = {<<"">>, <<"">>} :: {binary(), binary()} | '$2', + id = <<>> :: binary() | '_', + timestamp = p1_time_compat:timestamp() :: erlang:timestamp() | '_' | '$1', + peer = {<<"">>, <<"">>, <<"">>} :: ljid() | '_' | '$3' | undefined, + bare_peer = {<<"">>, <<"">>, <<"">>} :: ljid() | '_' | '$3', + packet = #xmlel{} :: xmlel() | '_', + nick = <<"">> :: binary(), + type = chat :: chat | groupchat}). + +-record(archive_prefs, + {us = {<<"">>, <<"">>} :: {binary(), binary()}, + default = never :: never | always | roster, + always = [] :: [ljid()], + never = [] :: [ljid()]}). + +%%%=================================================================== +%%% API +%%%=================================================================== +start(Host, Opts) -> + IQDisc = gen_mod:get_opt(iqdisc, Opts, fun gen_iq_handler:check_type/1, + one_queue), + DBType = gen_mod:db_type(Host, Opts), + init_db(DBType, Host), + init_cache(DBType, Opts), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, + ?NS_MAM_TMP, ?MODULE, process_iq_v0_2, IQDisc), + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, + ?NS_MAM_TMP, ?MODULE, process_iq_v0_2, IQDisc), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, + ?NS_MAM_0, ?MODULE, process_iq_v0_3, IQDisc), + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, + ?NS_MAM_0, ?MODULE, process_iq_v0_3, IQDisc), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, + ?NS_MAM_1, ?MODULE, process_iq_v0_3, IQDisc), + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, + ?NS_MAM_1, ?MODULE, process_iq_v0_3, IQDisc), + ejabberd_hooks:add(user_receive_packet, Host, ?MODULE, + user_receive_packet, 500), + ejabberd_hooks:add(user_send_packet, Host, ?MODULE, + user_send_packet, 500), + ejabberd_hooks:add(muc_filter_message, Host, ?MODULE, + muc_filter_message, 50), + ejabberd_hooks:add(muc_process_iq, Host, ?MODULE, + muc_process_iq, 50), + ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, + disco_sm_features, 50), + ejabberd_hooks:add(remove_user, Host, ?MODULE, + remove_user, 50), + ejabberd_hooks:add(anonymous_purge_hook, Host, ?MODULE, + remove_user, 50), + case gen_mod:get_opt(assume_mam_usage, Opts, + fun(if_enabled) -> if_enabled; + (on_request) -> on_request; + (never) -> never + end, never) of + never -> + ok; + _ -> + ejabberd_hooks:add(message_is_archived, Host, ?MODULE, + message_is_archived, 50) + end, + ejabberd_commands:register_commands(get_commands_spec()), + ok. + +init_db(mnesia, _Host) -> + mnesia:create_table(archive_msg, + [{disc_only_copies, [node()]}, + {type, bag}, + {attributes, record_info(fields, archive_msg)}]), + mnesia:create_table(archive_prefs, + [{disc_only_copies, [node()]}, + {attributes, record_info(fields, archive_prefs)}]); +init_db(_, _) -> + ok. + +init_cache(_DBType, Opts) -> + MaxSize = gen_mod:get_opt(cache_size, Opts, + fun(I) when is_integer(I), I>0 -> I end, + 1000), + LifeTime = gen_mod:get_opt(cache_life_time, Opts, + fun(I) when is_integer(I), I>0 -> I end, + timer:hours(1) div 1000), + cache_tab:new(archive_prefs, [{max_size, MaxSize}, + {life_time, LifeTime}]). + +stop(Host) -> + ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, + user_send_packet, 500), + ejabberd_hooks:delete(user_receive_packet, Host, ?MODULE, + user_receive_packet, 500), + ejabberd_hooks:delete(muc_filter_message, Host, ?MODULE, + muc_filter_message, 50), + ejabberd_hooks:delete(muc_process_iq, Host, ?MODULE, + muc_process_iq, 50), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_MAM_TMP), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MAM_TMP), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_MAM_0), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MAM_0), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_MAM_1), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MAM_1), + ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, + disco_sm_features, 50), + ejabberd_hooks:delete(remove_user, Host, ?MODULE, + remove_user, 50), + ejabberd_hooks:delete(anonymous_purge_hook, Host, + ?MODULE, remove_user, 50), + case gen_mod:get_module_opt(Host, ?MODULE, assume_mam_usage, + fun(if_enabled) -> if_enabled; + (on_request) -> on_request; + (never) -> never + end, never) of + never -> + ok; + _ -> + ejabberd_hooks:delete(message_is_archived, Host, ?MODULE, + message_is_archived, 50) + end, + ejabberd_commands:unregister_commands(get_commands_spec()), + ok. + +remove_user(User, Server) -> + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + remove_user(LUser, LServer, + gen_mod:db_type(LServer, ?MODULE)). + +remove_user(LUser, LServer, mnesia) -> + US = {LUser, LServer}, + F = fun () -> + mnesia:delete({archive_msg, US}), + mnesia:delete({archive_prefs, US}) + end, + mnesia:transaction(F); +remove_user(LUser, LServer, odbc) -> + SUser = ejabberd_odbc:escape(LUser), + ejabberd_odbc:sql_query( + LServer, + [<<"delete from archive where username='">>, SUser, <<"';">>]), + ejabberd_odbc:sql_query( + LServer, + [<<"delete from archive_prefs where username='">>, SUser, <<"';">>]). + +user_receive_packet(Pkt, C2SState, JID, Peer, To) -> + LUser = JID#jid.luser, + LServer = JID#jid.lserver, + IsBareCopy = is_bare_copy(JID, To), + case should_archive(Pkt, LServer) of + true when not IsBareCopy -> + NewPkt = strip_my_archived_tag(Pkt, LServer), + case store_msg(C2SState, NewPkt, LUser, LServer, Peer, recv) of + {ok, ID} -> + Archived = #xmlel{name = <<"archived">>, + attrs = [{<<"by">>, LServer}, + {<<"xmlns">>, ?NS_MAM_TMP}, + {<<"id">>, ID}]}, + StanzaID = #xmlel{name = <<"stanza-id">>, + attrs = [{<<"by">>, LServer}, + {<<"xmlns">>, ?NS_SID_0}, + {<<"id">>, ID}]}, + NewEls = [Archived, StanzaID|NewPkt#xmlel.children], + NewPkt#xmlel{children = NewEls}; + _ -> + NewPkt + end; + _ -> + Pkt + end. + +user_send_packet(Pkt, C2SState, JID, Peer) -> + LUser = JID#jid.luser, + LServer = JID#jid.lserver, + case should_archive(Pkt, LServer) of + true -> + NewPkt = strip_my_archived_tag(Pkt, LServer), + store_msg(C2SState, jlib:replace_from_to(JID, Peer, NewPkt), + LUser, LServer, Peer, send), + NewPkt; + false -> + Pkt + end. + +muc_filter_message(Pkt, #state{config = Config} = MUCState, + RoomJID, From, FromNick) -> + if Config#config.mam -> + LServer = RoomJID#jid.lserver, + NewPkt = strip_my_archived_tag(Pkt, LServer), + StorePkt = strip_x_jid_tags(NewPkt), + case store_muc(MUCState, StorePkt, RoomJID, From, FromNick) of + {ok, ID} -> + Archived = #xmlel{name = <<"archived">>, + attrs = [{<<"by">>, LServer}, + {<<"xmlns">>, ?NS_MAM_TMP}, + {<<"id">>, ID}]}, + StanzaID = #xmlel{name = <<"stanza-id">>, + attrs = [{<<"by">>, LServer}, + {<<"xmlns">>, ?NS_SID_0}, + {<<"id">>, ID}]}, + NewEls = [Archived, StanzaID|NewPkt#xmlel.children], + NewPkt#xmlel{children = NewEls}; + _ -> + NewPkt + end; + true -> + Pkt + end. + +% Query archive v0.2 +process_iq_v0_2(#jid{lserver = LServer} = From, + #jid{lserver = LServer} = To, + #iq{type = get, sub_el = #xmlel{name = <<"query">>} = SubEl} = IQ) -> + Fs = parse_query_v0_2(SubEl), + process_iq(LServer, From, To, IQ, SubEl, Fs, chat); +process_iq_v0_2(From, To, IQ) -> + process_iq(From, To, IQ). + +% Query archive v0.3 +process_iq_v0_3(#jid{lserver = LServer} = From, + #jid{lserver = LServer} = To, + #iq{type = set, sub_el = #xmlel{name = <<"query">>} = SubEl} = IQ) -> + process_iq(LServer, From, To, IQ, SubEl, get_xdata_fields(SubEl), chat); +process_iq_v0_3(#jid{lserver = LServer}, + #jid{lserver = LServer}, + #iq{type = get, sub_el = #xmlel{name = <<"query">>}} = IQ) -> + process_iq(LServer, IQ); +process_iq_v0_3(From, To, IQ) -> + process_iq(From, To, IQ). + +muc_process_iq(#iq{type = set, + sub_el = #xmlel{name = <<"query">>, + attrs = Attrs} = SubEl} = IQ, + MUCState, From, To) -> + case fxml:get_attr_s(<<"xmlns">>, Attrs) of + NS when NS == ?NS_MAM_0; NS == ?NS_MAM_1 -> + muc_process_iq(IQ, MUCState, From, To, get_xdata_fields(SubEl)); + _ -> + IQ + end; +muc_process_iq(#iq{type = get, + sub_el = #xmlel{name = <<"query">>, + attrs = Attrs} = SubEl} = IQ, + MUCState, From, To) -> + case fxml:get_attr_s(<<"xmlns">>, Attrs) of + ?NS_MAM_TMP -> + muc_process_iq(IQ, MUCState, From, To, parse_query_v0_2(SubEl)); + NS when NS == ?NS_MAM_0; NS == ?NS_MAM_1 -> + LServer = MUCState#state.server_host, + process_iq(LServer, IQ); + _ -> + IQ + end; +muc_process_iq(IQ, _MUCState, _From, _To) -> + IQ. + +get_xdata_fields(SubEl) -> + case {fxml:get_subtag_with_xmlns(SubEl, <<"x">>, ?NS_XDATA), + fxml:get_subtag_with_xmlns(SubEl, <<"set">>, ?NS_RSM)} of + {#xmlel{} = XData, false} -> + jlib:parse_xdata_submit(XData); + {#xmlel{} = XData, #xmlel{}} -> + [{<<"set">>, SubEl} | jlib:parse_xdata_submit(XData)]; + {false, #xmlel{}} -> + [{<<"set">>, SubEl}]; + {false, false} -> + [] + end. + +disco_sm_features(empty, From, To, Node, Lang) -> + disco_sm_features({result, []}, From, To, Node, Lang); +disco_sm_features({result, OtherFeatures}, + #jid{luser = U, lserver = S}, + #jid{luser = U, lserver = S}, <<>>, _Lang) -> + {result, [?NS_MAM_TMP, ?NS_MAM_0, ?NS_MAM_1 | OtherFeatures]}; +disco_sm_features(Acc, _From, _To, _Node, _Lang) -> + Acc. + +message_is_archived(true, _C2SState, _Peer, _JID, _Pkt) -> + true; +message_is_archived(false, C2SState, Peer, + #jid{luser = LUser, lserver = LServer}, Pkt) -> + Res = case gen_mod:get_module_opt(LServer, ?MODULE, assume_mam_usage, + fun(if_enabled) -> if_enabled; + (on_request) -> on_request; + (never) -> never + end, never) of + if_enabled -> + get_prefs(LUser, LServer); + on_request -> + DBType = gen_mod:db_type(LServer, ?MODULE), + cache_tab:lookup(archive_prefs, {LUser, LServer}, + fun() -> + get_prefs(LUser, LServer, DBType) + end); + never -> + error + end, + case Res of + {ok, Prefs} -> + should_archive(strip_my_archived_tag(Pkt, LServer), LServer) + andalso should_archive_peer(C2SState, Prefs, Peer); + error -> + false + end. + +delete_old_messages(TypeBin, Days) when TypeBin == <<"chat">>; + TypeBin == <<"groupchat">>; + TypeBin == <<"all">> -> + Diff = Days * 24 * 60 * 60 * 1000000, + TimeStamp = usec_to_now(p1_time_compat:system_time(micro_seconds) - Diff), + Type = jlib:binary_to_atom(TypeBin), + {Results, _} = + lists:foldl(fun(Host, {Results, MnesiaDone}) -> + case {gen_mod:db_type(Host, ?MODULE), MnesiaDone} of + {mnesia, true} -> + {Results, true}; + {mnesia, false} -> + Res = delete_old_messages(TimeStamp, Type, + global, mnesia), + {[Res|Results], true}; + {DBType, _} -> + Res = delete_old_messages(TimeStamp, Type, + Host, DBType), + {[Res|Results], MnesiaDone} + end + end, {[], false}, ?MYHOSTS), + case lists:filter(fun(Res) -> Res /= ok end, Results) of + [] -> ok; + [NotOk|_] -> NotOk + end; +delete_old_messages(_TypeBin, _Days) -> + unsupported_type. + +delete_old_messages(TimeStamp, Type, global, mnesia) -> + MS = ets:fun2ms(fun(#archive_msg{timestamp = MsgTS, + type = MsgType} = Msg) + when MsgTS < TimeStamp, + MsgType == Type orelse Type == all -> + Msg + end), + OldMsgs = mnesia:dirty_select(archive_msg, MS), + lists:foreach(fun(Rec) -> + ok = mnesia:dirty_delete_object(Rec) + end, OldMsgs); +delete_old_messages(_TimeStamp, _Type, _Host, _DBType) -> + %% TODO + not_implemented. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== + +process_iq(LServer, #iq{sub_el = #xmlel{attrs = Attrs}} = IQ) -> + NS = case fxml:get_attr_s(<<"xmlns">>, Attrs) of + ?NS_MAM_0 -> + ?NS_MAM_0; + _ -> + ?NS_MAM_1 + end, + CommonFields = [#xmlel{name = <<"field">>, + attrs = [{<<"type">>, <<"hidden">>}, + {<<"var">>, <<"FORM_TYPE">>}], + children = [#xmlel{name = <<"value">>, + children = [{xmlcdata, NS}]}]}, + #xmlel{name = <<"field">>, + attrs = [{<<"type">>, <<"jid-single">>}, + {<<"var">>, <<"with">>}]}, + #xmlel{name = <<"field">>, + attrs = [{<<"type">>, <<"text-single">>}, + {<<"var">>, <<"start">>}]}, + #xmlel{name = <<"field">>, + attrs = [{<<"type">>, <<"text-single">>}, + {<<"var">>, <<"end">>}]}], + Fields = case gen_mod:db_type(LServer, ?MODULE) of + odbc -> + WithText = #xmlel{name = <<"field">>, + attrs = [{<<"type">>, <<"text-single">>}, + {<<"var">>, <<"withtext">>}]}, + [WithText|CommonFields]; + _ -> + CommonFields + end, + Form = #xmlel{name = <<"x">>, + attrs = [{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"form">>}], + children = Fields}, + IQ#iq{type = result, + sub_el = [#xmlel{name = <<"query">>, + attrs = [{<<"xmlns">>, NS}], + children = [Form]}]}. + +% Preference setting (both v0.2 & v0.3) +process_iq(#jid{luser = LUser, lserver = LServer}, + #jid{lserver = LServer}, + #iq{type = set, sub_el = #xmlel{name = <<"prefs">>} = SubEl} = IQ) -> + try {case fxml:get_tag_attr_s(<<"default">>, SubEl) of + <<"always">> -> always; + <<"never">> -> never; + <<"roster">> -> roster + end, + lists:foldl( + fun(#xmlel{name = <<"always">>, children = Els}, {A, N}) -> + {get_jids(Els) ++ A, N}; + (#xmlel{name = <<"never">>, children = Els}, {A, N}) -> + {A, get_jids(Els) ++ N}; + (_, {A, N}) -> + {A, N} + end, {[], []}, SubEl#xmlel.children)} of + {Default, {Always0, Never0}} -> + Always = lists:usort(Always0), + Never = lists:usort(Never0), + case write_prefs(LUser, LServer, LServer, Default, Always, Never) of + ok -> + NewPrefs = prefs_el(Default, Always, Never, IQ#iq.xmlns), + IQ#iq{type = result, sub_el = [NewPrefs]}; + _Err -> + IQ#iq{type = error, + sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]} + end + catch _:_ -> + IQ#iq{type = error, sub_el = [SubEl, ?ERR_BAD_REQUEST]} + end; +process_iq(#jid{luser = LUser, lserver = LServer}, + #jid{lserver = LServer}, + #iq{type = get, sub_el = #xmlel{name = <<"prefs">>}} = IQ) -> + Prefs = get_prefs(LUser, LServer), + PrefsEl = prefs_el(Prefs#archive_prefs.default, + Prefs#archive_prefs.always, + Prefs#archive_prefs.never, + IQ#iq.xmlns), + IQ#iq{type = result, sub_el = [PrefsEl]}; +process_iq(_, _, #iq{sub_el = SubEl} = IQ) -> + IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}. + +process_iq(LServer, #jid{luser = LUser} = From, To, IQ, SubEl, Fs, MsgType) -> + case MsgType of + chat -> + maybe_activate_mam(LUser, LServer); + {groupchat, _Role, _MUCState} -> + ok + end, + case catch lists:foldl( + fun({<<"start">>, [Data|_]}, {_, End, With, RSM}) -> + {{_, _, _} = jlib:datetime_string_to_timestamp(Data), + End, With, RSM}; + ({<<"end">>, [Data|_]}, {Start, _, With, RSM}) -> + {Start, + {_, _, _} = jlib:datetime_string_to_timestamp(Data), + With, RSM}; + ({<<"with">>, [Data|_]}, {Start, End, _, RSM}) -> + {Start, End, jid:tolower(jid:from_string(Data)), RSM}; + ({<<"withtext">>, [Data|_]}, {Start, End, _, RSM}) -> + {Start, End, {text, Data}, RSM}; + ({<<"set">>, El}, {Start, End, With, _}) -> + {Start, End, With, jlib:rsm_decode(El)}; + (_, Acc) -> + Acc + end, {none, [], none, none}, Fs) of + {'EXIT', _} -> + IQ#iq{type = error, sub_el = [SubEl, ?ERR_BAD_REQUEST]}; + {_Start, _End, _With, #rsm_in{index = Index}} when is_integer(Index) -> + IQ#iq{type = error, sub_el = [SubEl, ?ERR_FEATURE_NOT_IMPLEMENTED]}; + {Start, End, With, RSM} -> + NS = fxml:get_tag_attr_s(<<"xmlns">>, SubEl), + select_and_send(LServer, From, To, Start, End, + With, limit_max(RSM, NS), IQ, MsgType) + end. + +muc_process_iq(#iq{lang = Lang, sub_el = SubEl} = IQ, MUCState, From, To, Fs) -> + case may_enter_room(From, MUCState) of + true -> + LServer = MUCState#state.server_host, + Role = mod_muc_room:get_role(From, MUCState), + process_iq(LServer, From, To, IQ, SubEl, Fs, + {groupchat, Role, MUCState}); + false -> + Text = <<"Only members may query archives of this room">>, + Error = ?ERRT_FORBIDDEN(Lang, Text), + IQ#iq{type = error, sub_el = [SubEl, Error]} + end. + +parse_query_v0_2(Query) -> + lists:flatmap( + fun (#xmlel{name = <<"start">>} = El) -> + [{<<"start">>, [fxml:get_tag_cdata(El)]}]; + (#xmlel{name = <<"end">>} = El) -> + [{<<"end">>, [fxml:get_tag_cdata(El)]}]; + (#xmlel{name = <<"with">>} = El) -> + [{<<"with">>, [fxml:get_tag_cdata(El)]}]; + (#xmlel{name = <<"withtext">>} = El) -> + [{<<"withtext">>, [fxml:get_tag_cdata(El)]}]; + (#xmlel{name = <<"set">>}) -> + [{<<"set">>, Query}]; + (_) -> + [] + end, Query#xmlel.children). + +should_archive(#xmlel{name = <<"message">>} = Pkt, LServer) -> + case fxml:get_attr_s(<<"type">>, Pkt#xmlel.attrs) of + <<"error">> -> + false; + <<"groupchat">> -> + false; + _ -> + case is_resent(Pkt, LServer) of + true -> + false; + false -> + case check_store_hint(Pkt) of + store -> + true; + no_store -> + false; + none -> + case fxml:get_subtag_cdata(Pkt, <<"body">>) of + <<>> -> + %% Empty body + false; + _ -> + true + end + end + end + end; +should_archive(#xmlel{}, _LServer) -> + false. + +strip_my_archived_tag(Pkt, LServer) -> + NewEls = lists:filter( + fun(#xmlel{name = Tag, attrs = Attrs}) + when Tag == <<"archived">>; Tag == <<"stanza-id">> -> + case catch jid:nameprep( + fxml:get_attr_s( + <<"by">>, Attrs)) of + LServer -> + false; + _ -> + true + end; + (_) -> + true + end, Pkt#xmlel.children), + Pkt#xmlel{children = NewEls}. + +strip_x_jid_tags(Pkt) -> + NewEls = lists:filter( + fun(#xmlel{name = <<"x">>} = XEl) -> + not lists:any(fun(ItemEl) -> + fxml:get_tag_attr(<<"jid">>, ItemEl) + /= false + end, fxml:get_subtags(XEl, <<"item">>)); + (_) -> + true + end, Pkt#xmlel.children), + Pkt#xmlel{children = NewEls}. + +should_archive_peer(C2SState, + #archive_prefs{default = Default, + always = Always, + never = Never}, + Peer) -> + LPeer = jid:tolower(Peer), + case lists:member(LPeer, Always) of + true -> + true; + false -> + case lists:member(LPeer, Never) of + true -> + false; + false -> + case Default of + always -> true; + never -> false; + roster -> + case ejabberd_c2s:get_subscription( + LPeer, C2SState) of + both -> true; + from -> true; + to -> true; + _ -> false + end + end + end + end. + +should_archive_muc(Pkt) -> + case fxml:get_attr_s(<<"type">>, Pkt#xmlel.attrs) of + <<"groupchat">> -> + case check_store_hint(Pkt) of + store -> + true; + no_store -> + false; + none -> + case fxml:get_subtag_cdata(Pkt, <<"body">>) of + <<>> -> + case fxml:get_subtag_cdata(Pkt, <<"subject">>) of + <<>> -> + false; + _ -> + true + end; + _ -> + true + end + end; + _ -> + false + end. + +check_store_hint(Pkt) -> + case has_store_hint(Pkt) of + true -> + store; + false -> + case has_no_store_hint(Pkt) of + true -> + no_store; + false -> + none + end + end. + +has_store_hint(Message) -> + fxml:get_subtag_with_xmlns(Message, <<"store">>, ?NS_HINTS) + /= false. + +has_no_store_hint(Message) -> + fxml:get_subtag_with_xmlns(Message, <<"no-store">>, ?NS_HINTS) + /= false orelse + fxml:get_subtag_with_xmlns(Message, <<"no-storage">>, ?NS_HINTS) + /= false orelse + fxml:get_subtag_with_xmlns(Message, <<"no-permanent-store">>, ?NS_HINTS) + /= false orelse + fxml:get_subtag_with_xmlns(Message, <<"no-permanent-storage">>, ?NS_HINTS) + /= false. + +is_resent(Pkt, LServer) -> + case fxml:get_subtag_with_xmlns(Pkt, <<"stanza-id">>, ?NS_SID_0) of + #xmlel{attrs = Attrs} -> + case fxml:get_attr(<<"by">>, Attrs) of + {value, LServer} -> + true; + _ -> + false + end; + false -> + false + end. + +may_enter_room(From, + #state{config = #config{members_only = false}} = MUCState) -> + mod_muc_room:get_affiliation(From, MUCState) /= outcast; +may_enter_room(From, MUCState) -> + mod_muc_room:is_occupant_or_admin(From, MUCState). + +store_msg(C2SState, Pkt, LUser, LServer, Peer, Dir) -> + Prefs = get_prefs(LUser, LServer), + case should_archive_peer(C2SState, Prefs, Peer) of + true -> + US = {LUser, LServer}, + store(Pkt, LServer, US, chat, Peer, <<"">>, Dir, + gen_mod:db_type(LServer, ?MODULE)); + false -> + pass + end. + +store_muc(MUCState, Pkt, RoomJID, Peer, Nick) -> + case should_archive_muc(Pkt) of + true -> + LServer = MUCState#state.server_host, + {U, S, _} = jid:tolower(RoomJID), + store(Pkt, LServer, {U, S}, groupchat, Peer, Nick, recv, + gen_mod:db_type(LServer, ?MODULE)); + false -> + pass + end. + +store(Pkt, _, {LUser, LServer}, Type, Peer, Nick, _Dir, mnesia) -> + LPeer = {PUser, PServer, _} = jid:tolower(Peer), + TS = p1_time_compat:timestamp(), + ID = jlib:integer_to_binary(now_to_usec(TS)), + case mnesia:dirty_write( + #archive_msg{us = {LUser, LServer}, + id = ID, + timestamp = TS, + peer = LPeer, + bare_peer = {PUser, PServer, <<>>}, + type = Type, + nick = Nick, + packet = Pkt}) of + ok -> + {ok, ID}; + Err -> + Err + end; +store(Pkt, LServer, {LUser, LHost}, Type, Peer, Nick, _Dir, odbc) -> + TSinteger = p1_time_compat:system_time(micro_seconds), + ID = TS = jlib:integer_to_binary(TSinteger), + SUser = case Type of + chat -> LUser; + groupchat -> jid:to_string({LUser, LHost, <<>>}) + end, + BarePeer = jid:to_string( + jid:tolower( + jid:remove_resource(Peer))), + LPeer = jid:to_string( + jid:tolower(Peer)), + XML = fxml:element_to_binary(Pkt), + Body = fxml:get_subtag_cdata(Pkt, <<"body">>), + case ejabberd_odbc:sql_query( + LServer, + [<<"insert into archive (username, timestamp, " + "peer, bare_peer, xml, txt, kind, nick) values (">>, + <<"'">>, ejabberd_odbc:escape(SUser), <<"', ">>, + <<"'">>, TS, <<"', ">>, + <<"'">>, ejabberd_odbc:escape(LPeer), <<"', ">>, + <<"'">>, ejabberd_odbc:escape(BarePeer), <<"', ">>, + <<"'">>, ejabberd_odbc:escape(XML), <<"', ">>, + <<"'">>, ejabberd_odbc:escape(Body), <<"', ">>, + <<"'">>, jlib:atom_to_binary(Type), <<"', ">>, + <<"'">>, ejabberd_odbc:escape(Nick), <<"');">>]) of + {updated, _} -> + {ok, ID}; + Err -> + Err + end. + +write_prefs(LUser, LServer, Host, Default, Always, Never) -> + DBType = case gen_mod:db_type(Host, ?MODULE) of + odbc -> {odbc, Host}; + DB -> DB + end, + Prefs = #archive_prefs{us = {LUser, LServer}, + default = Default, + always = Always, + never = Never}, + cache_tab:dirty_insert( + archive_prefs, {LUser, LServer}, Prefs, + fun() -> write_prefs(LUser, LServer, Prefs, DBType) end). + +write_prefs(_LUser, _LServer, Prefs, mnesia) -> + mnesia:dirty_write(Prefs); +write_prefs(LUser, _LServer, #archive_prefs{default = Default, + never = Never, + always = Always}, + {odbc, Host}) -> + SUser = ejabberd_odbc:escape(LUser), + SDefault = erlang:atom_to_binary(Default, utf8), + SAlways = ejabberd_odbc:encode_term(Always), + SNever = ejabberd_odbc:encode_term(Never), + case update(Host, <<"archive_prefs">>, + [<<"username">>, <<"def">>, <<"always">>, <<"never">>], + [SUser, SDefault, SAlways, SNever], + [<<"username='">>, SUser, <<"'">>]) of + {updated, _} -> + ok; + Err -> + Err + end. + +get_prefs(LUser, LServer) -> + DBType = gen_mod:db_type(LServer, ?MODULE), + Res = cache_tab:lookup(archive_prefs, {LUser, LServer}, + fun() -> get_prefs(LUser, LServer, + DBType) + end), + case Res of + {ok, Prefs} -> + Prefs; + error -> + ActivateOpt = gen_mod:get_module_opt( + LServer, ?MODULE, request_activates_archiving, + fun(B) when is_boolean(B) -> B end, false), + case ActivateOpt of + true -> + #archive_prefs{us = {LUser, LServer}, default = never}; + false -> + Default = gen_mod:get_module_opt( + LServer, ?MODULE, default, + fun(always) -> always; + (never) -> never; + (roster) -> roster + end, never), + #archive_prefs{us = {LUser, LServer}, default = Default} + end + end. + +get_prefs(LUser, LServer, mnesia) -> + case mnesia:dirty_read(archive_prefs, {LUser, LServer}) of + [Prefs] -> + {ok, Prefs}; + _ -> + error + end; +get_prefs(LUser, LServer, odbc) -> + case ejabberd_odbc:sql_query( + LServer, + [<<"select def, always, never from archive_prefs ">>, + <<"where username='">>, + ejabberd_odbc:escape(LUser), <<"';">>]) of + {selected, _, [[SDefault, SAlways, SNever]]} -> + Default = erlang:binary_to_existing_atom(SDefault, utf8), + Always = ejabberd_odbc:decode_term(SAlways), + Never = ejabberd_odbc:decode_term(SNever), + {ok, #archive_prefs{us = {LUser, LServer}, + default = Default, + always = Always, + never = Never}}; + _ -> + error + end. + +prefs_el(Default, Always, Never, NS) -> + Default1 = jlib:atom_to_binary(Default), + JFun = fun(L) -> + [#xmlel{name = <<"jid">>, + children = [{xmlcdata, jid:to_string(J)}]} + || J <- L] + end, + Always1 = #xmlel{name = <<"always">>, + children = JFun(Always)}, + Never1 = #xmlel{name = <<"never">>, + children = JFun(Never)}, + #xmlel{name = <<"prefs">>, + attrs = [{<<"xmlns">>, NS}, + {<<"default">>, Default1}], + children = [Always1, Never1]}. + +maybe_activate_mam(LUser, LServer) -> + ActivateOpt = gen_mod:get_module_opt(LServer, ?MODULE, + request_activates_archiving, + fun(B) when is_boolean(B) -> B end, + false), + case ActivateOpt of + true -> + Res = cache_tab:lookup(archive_prefs, {LUser, LServer}, + fun() -> + get_prefs(LUser, LServer, + gen_mod:db_type(LServer, + ?MODULE)) + end), + case Res of + {ok, _Prefs} -> + ok; + error -> + Default = gen_mod:get_module_opt(LServer, ?MODULE, default, + fun(always) -> always; + (never) -> never; + (roster) -> roster + end, never), + write_prefs(LUser, LServer, LServer, Default, [], []) + end; + false -> + ok + end. + +select_and_send(LServer, From, To, Start, End, With, RSM, IQ, MsgType) -> + DBType = case gen_mod:db_type(LServer, ?MODULE) of + odbc -> {odbc, LServer}; + DB -> DB + end, + select_and_send(LServer, From, To, Start, End, With, RSM, IQ, + MsgType, DBType). + +select_and_send(LServer, From, To, Start, End, With, RSM, IQ, MsgType, DBType) -> + {Msgs, IsComplete, Count} = select_and_start(LServer, From, To, Start, End, + With, RSM, MsgType, DBType), + SortedMsgs = lists:keysort(2, Msgs), + send(From, To, SortedMsgs, RSM, Count, IsComplete, IQ). + +select_and_start(LServer, From, To, Start, End, With, RSM, MsgType, DBType) -> + case MsgType of + chat -> + select(LServer, From, From, Start, End, With, RSM, MsgType, DBType); + {groupchat, _Role, _MUCState} -> + select(LServer, From, To, Start, End, With, RSM, MsgType, DBType) + end. + +select(_LServer, JidRequestor, JidArchive, Start, End, _With, RSM, + {groupchat, _Role, #state{config = #config{mam = false}, + history = History}} = MsgType, + _DBType) -> + #lqueue{len = L, queue = Q} = History, + {Msgs0, _} = + lists:mapfoldl( + fun({Nick, Pkt, _HaveSubject, UTCDateTime, _Size}, I) -> + Now = datetime_to_now(UTCDateTime, I), + TS = now_to_usec(Now), + case match_interval(Now, Start, End) and + match_rsm(Now, RSM) of + true -> + {[{jlib:integer_to_binary(TS), TS, + msg_to_el(#archive_msg{ + type = groupchat, + timestamp = Now, + peer = undefined, + nick = Nick, + packet = Pkt}, + MsgType, JidRequestor, JidArchive)}], + I+1}; + false -> + {[], I+1} + end + end, 0, queue:to_list(Q)), + Msgs = lists:flatten(Msgs0), + case RSM of + #rsm_in{max = Max, direction = before} -> + {NewMsgs, IsComplete} = filter_by_max(lists:reverse(Msgs), Max), + {NewMsgs, IsComplete, L}; + #rsm_in{max = Max} -> + {NewMsgs, IsComplete} = filter_by_max(Msgs, Max), + {NewMsgs, IsComplete, L}; + _ -> + {Msgs, true, L} + end; +select(_LServer, JidRequestor, + #jid{luser = LUser, lserver = LServer} = JidArchive, + Start, End, With, RSM, MsgType, mnesia) -> + MS = make_matchspec(LUser, LServer, Start, End, With), + Msgs = mnesia:dirty_select(archive_msg, MS), + SortedMsgs = lists:keysort(#archive_msg.timestamp, Msgs), + {FilteredMsgs, IsComplete} = filter_by_rsm(SortedMsgs, RSM), + Count = length(Msgs), + {lists:map( + fun(Msg) -> + {Msg#archive_msg.id, + jlib:binary_to_integer(Msg#archive_msg.id), + msg_to_el(Msg, MsgType, JidRequestor, JidArchive)} + end, FilteredMsgs), IsComplete, Count}; +select(LServer, JidRequestor, #jid{luser = LUser} = JidArchive, + Start, End, With, RSM, MsgType, {odbc, Host}) -> + User = case MsgType of + chat -> LUser; + {groupchat, _Role, _MUCState} -> jid:to_string(JidArchive) + end, + {Query, CountQuery} = make_sql_query(User, LServer, + Start, End, With, RSM), + % TODO from XEP-0313 v0.2: "To conserve resources, a server MAY place a + % reasonable limit on how many stanzas may be pushed to a client in one + % request. If a query returns a number of stanzas greater than this limit + % and the client did not specify a limit using RSM then the server should + % return a policy-violation error to the client." We currently don't do this + % for v0.2 requests, but we do limit #rsm_in.max for v0.3 and newer. + case {ejabberd_odbc:sql_query(Host, Query), + ejabberd_odbc:sql_query(Host, CountQuery)} of + {{selected, _, Res}, {selected, _, [[Count]]}} -> + {Max, Direction} = case RSM of + #rsm_in{max = M, direction = D} -> {M, D}; + _ -> {undefined, undefined} + end, + {Res1, IsComplete} = + if Max >= 0 andalso Max /= undefined andalso length(Res) > Max -> + if Direction == before -> + {lists:nthtail(1, Res), false}; + true -> + {lists:sublist(Res, Max), false} + end; + true -> + {Res, true} + end, + {lists:flatmap( + fun([TS, XML, PeerBin, Kind, Nick]) -> + try + #xmlel{} = El = fxml_stream:parse_element(XML), + Now = usec_to_now(jlib:binary_to_integer(TS)), + PeerJid = jid:tolower(jid:from_string(PeerBin)), + T = case Kind of + <<"">> -> chat; + null -> chat; + _ -> jlib:binary_to_atom(Kind) + end, + [{TS, jlib:binary_to_integer(TS), + msg_to_el(#archive_msg{timestamp = Now, + packet = El, + type = T, + nick = Nick, + peer = PeerJid}, + MsgType, JidRequestor, JidArchive)}] + catch _:Err -> + ?ERROR_MSG("failed to parse data from SQL: ~p. " + "The data was: " + "timestamp = ~s, xml = ~s, " + "peer = ~s, kind = ~s, nick = ~s", + [Err, TS, XML, PeerBin, Kind, Nick]), + [] + end + end, Res1), IsComplete, jlib:binary_to_integer(Count)}; + _ -> + {[], false, 0} + end. + +msg_to_el(#archive_msg{timestamp = TS, packet = Pkt1, nick = Nick, peer = Peer}, + MsgType, JidRequestor, #jid{lserver = LServer} = JidArchive) -> + Pkt2 = maybe_update_from_to(Pkt1, JidRequestor, JidArchive, Peer, MsgType, + Nick), + Pkt3 = #xmlel{name = <<"forwarded">>, + attrs = [{<<"xmlns">>, ?NS_FORWARD}], + children = [fxml:replace_tag_attr( + <<"xmlns">>, <<"jabber:client">>, Pkt2)]}, + jlib:add_delay_info(Pkt3, LServer, TS). + +maybe_update_from_to(#xmlel{children = Els} = Pkt, JidRequestor, JidArchive, + Peer, {groupchat, Role, + #state{config = #config{anonymous = Anon}}}, + Nick) -> + ExposeJID = case {Peer, JidRequestor} of + {undefined, _JidRequestor} -> + false; + {{U, S, _R}, #jid{luser = U, lserver = S}} -> + true; + {_Peer, _JidRequestor} when not Anon; Role == moderator -> + true; + {_Peer, _JidRequestor} -> + false + end, + Items = case ExposeJID of + true -> + [#xmlel{name = <<"x">>, + attrs = [{<<"xmlns">>, ?NS_MUC_USER}], + children = + [#xmlel{name = <<"item">>, + attrs = [{<<"jid">>, + jid:to_string(Peer)}]}]}]; + false -> + [] + end, + Pkt1 = Pkt#xmlel{children = Items ++ Els}, + Pkt2 = jlib:replace_from(jid:replace_resource(JidArchive, Nick), Pkt1), + jlib:remove_attr(<<"to">>, Pkt2); +maybe_update_from_to(Pkt, _JidRequestor, _JidArchive, _Peer, chat, _Nick) -> + Pkt. + +is_bare_copy(#jid{luser = U, lserver = S, lresource = R}, To) -> + PrioRes = ejabberd_sm:get_user_present_resources(U, S), + MaxRes = case catch lists:max(PrioRes) of + {_Prio, Res} when is_binary(Res) -> + Res; + _ -> + undefined + end, + IsBareTo = case To of + #jid{lresource = <<"">>} -> + true; + #jid{lresource = LRes} -> + %% Unavailable resources are handled like bare JIDs. + lists:keyfind(LRes, 2, PrioRes) =:= false + end, + case {IsBareTo, R} of + {true, MaxRes} -> + ?DEBUG("Recipient of message to bare JID has top priority: ~s@~s/~s", + [U, S, R]), + false; + {true, _R} -> + %% The message was sent to our bare JID, and we currently have + %% multiple resources with the same highest priority, so the session + %% manager routes the message to each of them. We store the message + %% only from the resource where R equals MaxRes. + ?DEBUG("Additional recipient of message to bare JID: ~s@~s/~s", + [U, S, R]), + true; + {false, _R} -> + false + end. + +send(From, To, Msgs, RSM, Count, IsComplete, #iq{sub_el = SubEl} = IQ) -> + QID = fxml:get_tag_attr_s(<<"queryid">>, SubEl), + NS = fxml:get_tag_attr_s(<<"xmlns">>, SubEl), + QIDAttr = if QID /= <<>> -> + [{<<"queryid">>, QID}]; + true -> + [] + end, + CompleteAttr = if NS == ?NS_MAM_TMP -> + []; + NS == ?NS_MAM_0; NS == ?NS_MAM_1 -> + [{<<"complete">>, jlib:atom_to_binary(IsComplete)}] + end, + Els = lists:map( + fun({ID, _IDInt, El}) -> + #xmlel{name = <<"message">>, + children = [#xmlel{name = <<"result">>, + attrs = [{<<"xmlns">>, NS}, + {<<"id">>, ID}|QIDAttr], + children = [El]}]} + end, Msgs), + RSMOut = make_rsm_out(Msgs, RSM, Count, QIDAttr ++ CompleteAttr, NS), + if NS == ?NS_MAM_TMP; NS == ?NS_MAM_1 -> + lists:foreach( + fun(El) -> + ejabberd_router:route(To, From, El) + end, Els), + IQ#iq{type = result, sub_el = RSMOut}; + NS == ?NS_MAM_0 -> + ejabberd_router:route( + To, From, jlib:iq_to_xml(IQ#iq{type = result, sub_el = []})), + lists:foreach( + fun(El) -> + ejabberd_router:route(To, From, El) + end, Els), + ejabberd_router:route( + To, From, #xmlel{name = <<"message">>, + children = RSMOut}), + ignore + end. + + +make_rsm_out([], _, Count, Attrs, NS) -> + Tag = if NS == ?NS_MAM_TMP -> <<"query">>; + true -> <<"fin">> + end, + [#xmlel{name = Tag, attrs = [{<<"xmlns">>, NS}|Attrs], + children = jlib:rsm_encode(#rsm_out{count = Count})}]; +make_rsm_out([{FirstID, _, _}|_] = Msgs, _, Count, Attrs, NS) -> + {LastID, _, _} = lists:last(Msgs), + Tag = if NS == ?NS_MAM_TMP -> <<"query">>; + true -> <<"fin">> + end, + [#xmlel{name = Tag, attrs = [{<<"xmlns">>, NS}|Attrs], + children = jlib:rsm_encode( + #rsm_out{first = FirstID, count = Count, + last = LastID})}]. + +filter_by_rsm(Msgs, none) -> + {Msgs, true}; +filter_by_rsm(_Msgs, #rsm_in{max = Max}) when Max < 0 -> + {[], true}; +filter_by_rsm(Msgs, #rsm_in{max = Max, direction = Direction, id = ID}) -> + NewMsgs = case Direction of + aft when ID /= <<"">> -> + lists:filter( + fun(#archive_msg{id = I}) -> + ?BIN_GREATER_THAN(I, ID) + end, Msgs); + before when ID /= <<"">> -> + lists:foldl( + fun(#archive_msg{id = I} = Msg, Acc) + when ?BIN_LESS_THAN(I, ID) -> + [Msg|Acc]; + (_, Acc) -> + Acc + end, [], Msgs); + before when ID == <<"">> -> + lists:reverse(Msgs); + _ -> + Msgs + end, + filter_by_max(NewMsgs, Max). + +filter_by_max(Msgs, undefined) -> + {Msgs, true}; +filter_by_max(Msgs, Len) when is_integer(Len), Len >= 0 -> + {lists:sublist(Msgs, Len), length(Msgs) =< Len}; +filter_by_max(_Msgs, _Junk) -> + {[], true}. + +limit_max(RSM, ?NS_MAM_TMP) -> + RSM; % XEP-0313 v0.2 doesn't require clients to support RSM. +limit_max(#rsm_in{max = Max} = RSM, _NS) when not is_integer(Max) -> + RSM#rsm_in{max = ?DEF_PAGE_SIZE}; +limit_max(#rsm_in{max = Max} = RSM, _NS) when Max > ?MAX_PAGE_SIZE -> + RSM#rsm_in{max = ?MAX_PAGE_SIZE}; +limit_max(RSM, _NS) -> + RSM. + +match_interval(Now, Start, End) -> + (Now >= Start) and (Now =< End). + +match_rsm(Now, #rsm_in{id = ID, direction = aft}) when ID /= <<"">> -> + Now1 = (catch usec_to_now(jlib:binary_to_integer(ID))), + Now > Now1; +match_rsm(Now, #rsm_in{id = ID, direction = before}) when ID /= <<"">> -> + Now1 = (catch usec_to_now(jlib:binary_to_integer(ID))), + Now < Now1; +match_rsm(_Now, _) -> + true. + +make_matchspec(LUser, LServer, Start, End, {_, _, <<>>} = With) -> + ets:fun2ms( + fun(#archive_msg{timestamp = TS, + us = US, + bare_peer = BPeer} = Msg) + when Start =< TS, End >= TS, + US == {LUser, LServer}, + BPeer == With -> + Msg + end); +make_matchspec(LUser, LServer, Start, End, {_, _, _} = With) -> + ets:fun2ms( + fun(#archive_msg{timestamp = TS, + us = US, + peer = Peer} = Msg) + when Start =< TS, End >= TS, + US == {LUser, LServer}, + Peer == With -> + Msg + end); +make_matchspec(LUser, LServer, Start, End, none) -> + ets:fun2ms( + fun(#archive_msg{timestamp = TS, + us = US, + peer = Peer} = Msg) + when Start =< TS, End >= TS, + US == {LUser, LServer} -> + Msg + end). + +make_sql_query(User, _LServer, Start, End, With, RSM) -> + {Max, Direction, ID} = case RSM of + #rsm_in{} -> + {RSM#rsm_in.max, + RSM#rsm_in.direction, + RSM#rsm_in.id}; + none -> + {none, none, <<>>} + end, + LimitClause = if is_integer(Max), Max >= 0 -> + [<<" limit ">>, jlib:integer_to_binary(Max+1)]; + true -> + [] + end, + WithClause = case With of + {text, <<>>} -> + []; + {text, Txt} -> + [<<" and match (txt) against ('">>, + ejabberd_odbc:escape(Txt), <<"')">>]; + {_, _, <<>>} -> + [<<" and bare_peer='">>, + ejabberd_odbc:escape(jid:to_string(With)), + <<"'">>]; + {_, _, _} -> + [<<" and peer='">>, + ejabberd_odbc:escape(jid:to_string(With)), + <<"'">>]; + none -> + [] + end, + PageClause = case catch jlib:binary_to_integer(ID) of + I when is_integer(I), I >= 0 -> + case Direction of + before -> + [<<" AND timestamp < ">>, ID]; + aft -> + [<<" AND timestamp > ">>, ID]; + _ -> + [] + end; + _ -> + [] + end, + StartClause = case Start of + {_, _, _} -> + [<<" and timestamp >= ">>, + jlib:integer_to_binary(now_to_usec(Start))]; + _ -> + [] + end, + EndClause = case End of + {_, _, _} -> + [<<" and timestamp <= ">>, + jlib:integer_to_binary(now_to_usec(End))]; + _ -> + [] + end, + SUser = ejabberd_odbc:escape(User), + + Query = [<<"SELECT timestamp, xml, peer, kind, nick" + " FROM archive WHERE username='">>, + SUser, <<"'">>, WithClause, StartClause, EndClause, + PageClause], + + QueryPage = + case Direction of + before -> + % ID can be empty because of + % XEP-0059: Result Set Management + % 2.5 Requesting the Last Page in a Result Set + [<<"SELECT timestamp, xml, peer, kind, nick FROM (">>, Query, + <<" ORDER BY timestamp DESC ">>, + LimitClause, <<") AS t ORDER BY timestamp ASC;">>]; + _ -> + [Query, <<" ORDER BY timestamp ASC ">>, + LimitClause, <<";">>] + end, + {QueryPage, + [<<"SELECT COUNT(*) FROM archive WHERE username='">>, + SUser, <<"'">>, WithClause, StartClause, EndClause, <<";">>]}. + +now_to_usec({MSec, Sec, USec}) -> + (MSec*1000000 + Sec)*1000000 + USec. + +usec_to_now(Int) -> + Secs = Int div 1000000, + USec = Int rem 1000000, + MSec = Secs div 1000000, + Sec = Secs rem 1000000, + {MSec, Sec, USec}. + +datetime_to_now(DateTime, USecs) -> + Seconds = calendar:datetime_to_gregorian_seconds(DateTime) - + calendar:datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}}), + {Seconds div 1000000, Seconds rem 1000000, USecs}. + +get_jids(Els) -> + lists:flatmap( + fun(#xmlel{name = <<"jid">>} = El) -> + J = jid:from_string(fxml:get_tag_cdata(El)), + [jid:tolower(jid:remove_resource(J)), + jid:tolower(J)]; + (_) -> + [] + end, Els). + +update(LServer, Table, Fields, Vals, Where) -> + UPairs = lists:zipwith(fun (A, B) -> + <<A/binary, "='", B/binary, "'">> + end, + Fields, Vals), + case ejabberd_odbc:sql_query(LServer, + [<<"update ">>, Table, <<" set ">>, + join(UPairs, <<", ">>), <<" where ">>, Where, + <<";">>]) + of + {updated, 1} -> {updated, 1}; + _ -> + ejabberd_odbc:sql_query(LServer, + [<<"insert into ">>, Table, <<"(">>, + join(Fields, <<", ">>), <<") values ('">>, + join(Vals, <<"', '">>), <<"');">>]) + end. + +%% Almost a copy of string:join/2. +join([], _Sep) -> []; +join([H | T], Sep) -> [H, [[Sep, X] || X <- T]]. + +get_commands_spec() -> + [#ejabberd_commands{name = delete_old_mam_messages, tags = [purge], + desc = "Delete MAM messages older than DAYS", + longdesc = "Valid message TYPEs: " + "\"chat\", \"groupchat\", \"all\".", + module = ?MODULE, function = delete_old_messages, + args = [{type, binary}, {days, integer}], + result = {res, rescode}}]. + +mod_opt_type(assume_mam_usage) -> + fun(if_enabled) -> if_enabled; + (on_request) -> on_request; + (never) -> never + end; +mod_opt_type(cache_life_time) -> + fun (I) when is_integer(I), I > 0 -> I end; +mod_opt_type(cache_size) -> + fun (I) when is_integer(I), I > 0 -> I end; +mod_opt_type(db_type) -> fun gen_mod:v_db/1; +mod_opt_type(default) -> + fun (always) -> always; + (never) -> never; + (roster) -> roster + end; +mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; +mod_opt_type(request_activates_archiving) -> + fun (B) when is_boolean(B) -> B end; +mod_opt_type(_) -> + [assume_mam_usage, cache_life_time, cache_size, db_type, default, iqdisc, + request_activates_archiving]. diff --git a/src/mod_metrics.erl b/src/mod_metrics.erl new file mode 100644 index 00000000..c175fcb8 --- /dev/null +++ b/src/mod_metrics.erl @@ -0,0 +1,128 @@ +%%%------------------------------------------------------------------- +%%% File : mod_metrics.erl +%%% Author : Christophe Romain <christophe.romain@process-one.net> +%%% Purpose : Simple metrics handler for runtime statistics +%%% Created : 22 Oct 2015 by Christophe Romain <christophe.romain@process-one.net> +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2016 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_metrics). + +-behaviour(ejabberd_config). +-author('christophe.romain@process-one.net'). +-behaviour(gen_mod). + +-include("ejabberd.hrl"). +-include("logger.hrl"). +-include("jlib.hrl"). + +-define(HOOKS, [offline_message_hook, + sm_register_connection_hook, sm_remove_connection_hook, + user_send_packet, user_receive_packet, + s2s_send_packet, s2s_receive_packet, + remove_user, register_user]). + +-export([start/2, stop/1, send_metrics/4, opt_type/1]). + +-export([offline_message_hook/3, + sm_register_connection_hook/3, sm_remove_connection_hook/3, + user_send_packet/4, user_receive_packet/5, + s2s_send_packet/3, s2s_receive_packet/3, + remove_user/2, register_user/2]). + +%%==================================================================== +%% API +%%==================================================================== + +start(Host, _Opts) -> + [ejabberd_hooks:add(Hook, Host, ?MODULE, Hook, 20) + || Hook <- ?HOOKS]. + +stop(Host) -> + [ejabberd_hooks:delete(Hook, Host, ?MODULE, Hook, 20) + || Hook <- ?HOOKS]. + +%%==================================================================== +%% Hooks handlers +%%==================================================================== + +offline_message_hook(_From, #jid{lserver=LServer}, _Packet) -> + push(LServer, offline_message). + +sm_register_connection_hook(_SID, #jid{lserver=LServer}, _Info) -> + push(LServer, sm_register_connection). +sm_remove_connection_hook(_SID, #jid{lserver=LServer}, _Info) -> + push(LServer, sm_remove_connection). + +user_send_packet(Packet, _C2SState, #jid{lserver=LServer}, _To) -> + push(LServer, user_send_packet), + Packet. +user_receive_packet(Packet, _C2SState, _JID, _From, #jid{lserver=LServer}) -> + push(LServer, user_receive_packet), + Packet. + +s2s_send_packet(#jid{lserver=LServer}, _To, _Packet) -> + push(LServer, s2s_send_packet). +s2s_receive_packet(_From, #jid{lserver=LServer}, _Packet) -> + push(LServer, s2s_receive_packet). + +remove_user(_User, Server) -> + push(jid:nameprep(Server), remove_user). +register_user(_User, Server) -> + push(jid:nameprep(Server), register_user). + +%%==================================================================== +%% metrics push handler +%%==================================================================== + +push(Host, Probe) -> + spawn(?MODULE, send_metrics, [Host, Probe, {127,0,0,1}, 11111]). + +send_metrics(Host, Probe, Peer, Port) -> + % our default metrics handler is https://github.com/processone/grapherl + % grapherl metrics are named first with service domain, then nodename + % and name of the data itself, followed by type timestamp and value + % example => process-one.net/xmpp-1.user_receive_packet:c/1441784958:1 + [_, NodeId] = str:tokens(jlib:atom_to_binary(node()), <<"@">>), + [Node | _] = str:tokens(NodeId, <<".">>), + BaseId = <<Host/binary, "/", Node/binary, ".">>, + DateTime = erlang:universaltime(), + UnixTime = calendar:datetime_to_gregorian_seconds(DateTime) - 62167219200, + TS = integer_to_binary(UnixTime), + case gen_udp:open(0) of + {ok, Socket} -> + case Probe of + {Key, Val} -> + BVal = integer_to_binary(Val), + Data = <<BaseId/binary, (jlib:atom_to_binary(Key))/binary, + ":g/", TS/binary, ":", BVal/binary>>, + gen_udp:send(Socket, Peer, Port, Data); + Key -> + Data = <<BaseId/binary, (jlib:atom_to_binary(Key))/binary, + ":c/", TS/binary, ":1">>, + gen_udp:send(Socket, Peer, Port, Data) + end, + gen_udp:close(Socket); + Error -> + ?WARNING_MSG("can not open udp socket to grapherl: ~p", [Error]) + end. + +opt_type(_) -> + []. diff --git a/src/mod_mix.erl b/src/mod_mix.erl new file mode 100644 index 00000000..d8cf94ac --- /dev/null +++ b/src/mod_mix.erl @@ -0,0 +1,347 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 2 Mar 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_mix). + +-behaviour(gen_server). +-behaviour(gen_mod). + +%% API +-export([start_link/2, start/2, stop/1, process_iq/3, + disco_items/5, disco_identity/5, disco_info/5, + disco_features/5, mod_opt_type/1]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-include("logger.hrl"). +-include("jlib.hrl"). +-include("pubsub.hrl"). + +-define(PROCNAME, ejabberd_mod_mix). +-define(NODES, [?NS_MIX_NODES_MESSAGES, + ?NS_MIX_NODES_PRESENCE, + ?NS_MIX_NODES_PARTICIPANTS, + ?NS_MIX_NODES_SUBJECT, + ?NS_MIX_NODES_CONFIG]). + +-record(state, {server_host :: binary(), + host :: binary()}). + +%%%=================================================================== +%%% API +%%%=================================================================== +start_link(Host, Opts) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []). + +start(Host, Opts) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]}, + temporary, 5000, worker, [?MODULE]}, + supervisor:start_child(ejabberd_sup, ChildSpec). + +stop(Host) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + supervisor:terminate_child(ejabberd_sup, Proc), + supervisor:delete_child(ejabberd_sup, Proc), + ok. + +disco_features(_Acc, _From, _To, _Node, _Lang) -> + {result, [?NS_MIX_0]}. + +disco_items(_Acc, _From, To, _Node, _Lang) when To#jid.luser /= <<"">> -> + To_s = jid:to_string(jid:remove_resource(To)), + {result, [#xmlel{name = <<"item">>, + attrs = [{<<"jid">>, To_s}, + {<<"node">>, Node}]} || Node <- ?NODES]}; +disco_items(_Acc, _From, _To, _Node, _Lang) -> + {result, []}. + +disco_identity(Acc, _From, To, _Node, _Lang) when To#jid.luser == <<"">> -> + Acc ++ [#xmlel{name = <<"identity">>, + attrs = + [{<<"category">>, <<"conference">>}, + {<<"name">>, <<"MIX service">>}, + {<<"type">>, <<"text">>}]}]; +disco_identity(Acc, _From, _To, _Node, _Lang) -> + Acc ++ [#xmlel{name = <<"identity">>, + attrs = + [{<<"category">>, <<"conference">>}, + {<<"type">>, <<"mix">>}]}]. + +disco_info(_Acc, _From, To, _Node, _Lang) when is_atom(To) -> + [#xmlel{name = <<"x">>, + attrs = [{<<"xmlns">>, ?NS_XDATA}, + {<<"type">>, <<"result">>}], + children = [#xmlel{name = <<"field">>, + attrs = [{<<"var">>, <<"FORM_TYPE">>}, + {<<"type">>, <<"hidden">>}], + children = [#xmlel{name = <<"value">>, + children = [{xmlcdata, + ?NS_MIX_SERVICEINFO_0}]}]}]}]; +disco_info(Acc, _From, _To, _Node, _Lang) -> + Acc. + +process_iq(From, To, + #iq{type = set, sub_el = #xmlel{name = <<"join">>} = SubEl} = IQ) -> + Nodes = lists:flatmap( + fun(#xmlel{name = <<"subscribe">>, attrs = Attrs}) -> + Node = fxml:get_attr_s(<<"node">>, Attrs), + case lists:member(Node, ?NODES) of + true -> [Node]; + false -> [] + end; + (_) -> + [] + end, SubEl#xmlel.children), + case subscribe_nodes(From, To, Nodes) of + {result, _} -> + case publish_participant(From, To) of + {result, _} -> + LFrom_s = jid:to_string(jid:tolower(jid:remove_resource(From))), + Subscribe = [#xmlel{name = <<"subscribe">>, + attrs = [{<<"node">>, Node}]} || Node <- Nodes], + IQ#iq{type = result, + sub_el = [#xmlel{name = <<"join">>, + attrs = [{<<"jid">>, LFrom_s}, + {<<"xmlns">>, ?NS_MIX_0}], + children = Subscribe}]}; + {error, Err} -> + IQ#iq{type = error, sub_el = [SubEl, Err]} + end; + {error, Err} -> + IQ#iq{type = error, sub_el = [SubEl, Err]} + end; +process_iq(From, To, + #iq{type = set, sub_el = #xmlel{name = <<"leave">>} = SubEl} = IQ) -> + case delete_participant(From, To) of + {result, _} -> + case unsubscribe_nodes(From, To, ?NODES) of + {result, _} -> + IQ#iq{type = result, sub_el = []}; + {error, Err} -> + IQ#iq{type = error, sub_el = [SubEl, Err]} + end; + {error, Err} -> + IQ#iq{type = error, sub_el = [SubEl, Err]} + end; +process_iq(_From, _To, #iq{sub_el = SubEl} = IQ) -> + IQ#iq{type = error, sub_el = [SubEl, ?ERR_BAD_REQUEST]}. + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== +init([ServerHost, Opts]) -> + Host = gen_mod:get_opt_host(ServerHost, Opts, <<"mix.@HOST@">>), + IQDisc = gen_mod:get_opt(iqdisc, Opts, fun gen_iq_handler:check_type/1, + one_queue), + ConfigTab = gen_mod:get_module_proc(Host, config), + ets:new(ConfigTab, [named_table]), + ets:insert(ConfigTab, {plugins, [<<"mix">>]}), + ejabberd_hooks:add(disco_local_items, Host, ?MODULE, disco_items, 100), + ejabberd_hooks:add(disco_local_features, Host, ?MODULE, disco_features, 100), + ejabberd_hooks:add(disco_local_identity, Host, ?MODULE, disco_identity, 100), + ejabberd_hooks:add(disco_sm_items, Host, ?MODULE, disco_items, 100), + ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, disco_features, 100), + ejabberd_hooks:add(disco_sm_identity, Host, ?MODULE, disco_identity, 100), + ejabberd_hooks:add(disco_info, Host, ?MODULE, disco_info, 100), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, + ?NS_DISCO_ITEMS, mod_disco, + process_local_iq_items, IQDisc), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, + ?NS_DISCO_INFO, mod_disco, + process_local_iq_info, IQDisc), + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, + ?NS_DISCO_ITEMS, mod_disco, + process_local_iq_items, IQDisc), + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, + ?NS_DISCO_INFO, mod_disco, + process_local_iq_info, IQDisc), + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, + ?NS_PUBSUB, mod_pubsub, iq_sm, IQDisc), + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, + ?NS_MIX_0, ?MODULE, process_iq, IQDisc), + ejabberd_router:register_route(Host, ServerHost), + {ok, #state{server_host = ServerHost, host = Host}}. + +handle_call(_Request, _From, State) -> + Reply = ok, + {reply, Reply, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info({route, From, To, Packet}, State) -> + case catch do_route(State, From, To, Packet) of + {'EXIT', _} = Err -> + try + ?ERROR_MSG("failed to route packet ~p from '~s' to '~s': ~p", + [Packet, jid:to_string(From), jid:to_string(To), Err]), + ErrPkt = jlib:make_error_reply(Packet, ?ERR_INTERNAL_SERVER_ERROR), + ejabberd_router:route_error(To, From, ErrPkt, Packet) + catch _:_ -> + ok + end; + _ -> + ok + end, + {noreply, State}; +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, #state{host = Host}) -> + ejabberd_hooks:delete(disco_local_items, Host, ?MODULE, disco_items, 100), + ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, disco_features, 100), + ejabberd_hooks:delete(disco_local_identity, Host, ?MODULE, disco_identity, 100), + ejabberd_hooks:delete(disco_sm_items, Host, ?MODULE, disco_items, 100), + ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, disco_features, 100), + ejabberd_hooks:delete(disco_sm_identity, Host, ?MODULE, disco_identity, 100), + ejabberd_hooks:delete(disco_info, Host, ?MODULE, disco_info, 100), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_DISCO_ITEMS), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_DISCO_INFO), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_PUBSUB), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MIX_0), + ejabberd_router:unregister_route(Host), + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +do_route(_State, From, To, #xmlel{name = <<"iq">>} = Packet) -> + if To#jid.luser == <<"">> -> + ejabberd_local:process_iq(From, To, Packet); + true -> + ejabberd_sm:process_iq(From, To, Packet) + end; +do_route(_State, From, To, #xmlel{name = <<"presence">>} = Packet) + when To#jid.luser /= <<"">> -> + case fxml:get_tag_attr_s(<<"type">>, Packet) of + <<"unavailable">> -> + delete_presence(From, To); + _ -> + ok + end; +do_route(_State, _From, _To, _Packet) -> + ok. + +subscribe_nodes(From, To, Nodes) -> + LTo = jid:tolower(jid:remove_resource(To)), + LFrom = jid:tolower(jid:remove_resource(From)), + From_s = jid:to_string(LFrom), + lists:foldl( + fun(_Node, {error, _} = Err) -> + Err; + (Node, {result, _}) -> + case mod_pubsub:subscribe_node(LTo, Node, From, From_s, []) of + {error, _} = Err -> + case is_item_not_found(Err) of + true -> + case mod_pubsub:create_node( + LTo, To#jid.lserver, Node, LFrom, <<"mix">>) of + {result, _} -> + mod_pubsub:subscribe_node(LTo, Node, From, From_s, []); + Error -> + Error + end; + false -> + Err + end; + {result, _} = Result -> + Result + end + end, {result, []}, Nodes). + +unsubscribe_nodes(From, To, Nodes) -> + LTo = jid:tolower(jid:remove_resource(To)), + LFrom = jid:tolower(jid:remove_resource(From)), + From_s = jid:to_string(LFrom), + lists:foldl( + fun(_Node, {error, _} = Err) -> + Err; + (Node, {result, _} = Result) -> + case mod_pubsub:unsubscribe_node(LTo, Node, From, From_s, <<"">>) of + {error, _} = Err -> + case is_not_subscribed(Err) of + true -> Result; + _ -> Err + end; + {result, _} = Res -> + Res + end + end, {result, []}, Nodes). + +publish_participant(From, To) -> + LFrom = jid:tolower(jid:remove_resource(From)), + LTo = jid:tolower(jid:remove_resource(To)), + Participant = #xmlel{name = <<"participant">>, + attrs = [{<<"xmlns">>, ?NS_MIX_0}, + {<<"jid">>, jid:to_string(LFrom)}]}, + ItemID = p1_sha:sha(jid:to_string(LFrom)), + mod_pubsub:publish_item( + LTo, To#jid.lserver, ?NS_MIX_NODES_PARTICIPANTS, + From, ItemID, [Participant]). + +delete_presence(From, To) -> + LFrom = jid:tolower(From), + LTo = jid:tolower(jid:remove_resource(To)), + case mod_pubsub:get_items(LTo, ?NS_MIX_NODES_PRESENCE) of + Items when is_list(Items) -> + lists:foreach( + fun(#pubsub_item{modification = {_, LJID}, + itemid = {ItemID, _}}) when LJID == LFrom -> + delete_item(From, To, ?NS_MIX_NODES_PRESENCE, ItemID); + (_) -> + ok + end, Items); + _ -> + ok + end. + +delete_participant(From, To) -> + LFrom = jid:tolower(jid:remove_resource(From)), + ItemID = p1_sha:sha(jid:to_string(LFrom)), + delete_presence(From, To), + delete_item(From, To, ?NS_MIX_NODES_PARTICIPANTS, ItemID). + +delete_item(From, To, Node, ItemID) -> + LTo = jid:tolower(jid:remove_resource(To)), + case mod_pubsub:delete_item( + LTo, Node, From, ItemID, true) of + {result, _} = Res -> + Res; + {error, _} = Err -> + case is_item_not_found(Err) of + true -> {result, []}; + false -> Err + end + end. + +is_item_not_found({error, ErrEl}) -> + case fxml:get_subtag_with_xmlns( + ErrEl, <<"item-not-found">>, ?NS_STANZAS) of + #xmlel{} -> true; + _ -> false + end. + +is_not_subscribed({error, ErrEl}) -> + case fxml:get_subtag_with_xmlns( + ErrEl, <<"not-subscribed">>, ?NS_PUBSUB_ERRORS) of + #xmlel{} -> true; + _ -> false + end. + +mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; +mod_opt_type(host) -> fun iolist_to_binary/1; +mod_opt_type(_) -> [host, iqdisc]. diff --git a/src/mod_muc.erl b/src/mod_muc.erl index a3a8a933..0d37a236 100644 --- a/src/mod_muc.erl +++ b/src/mod_muc.erl @@ -5,7 +5,7 @@ %%% Created : 19 Mar 2003 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -27,6 +27,8 @@ -author('alexey@process-one.net'). +-protocol({xep, 45, '1.25'}). + -behaviour(gen_server). -behaviour(gen_mod). @@ -40,35 +42,23 @@ restore_room/3, forget_room/3, create_room/5, - shutdown_rooms/1, + shutdown_rooms/1, process_iq_disco_items/4, broadcast_service_message/2, - export/1, - import/1, - import/3, + export/1, + import/1, + import/3, can_use_nick/4]). -%% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, - handle_info/2, terminate/2, code_change/3]). + handle_info/2, terminate/2, code_change/3, + mod_opt_type/1]). -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' | - {'_', binary()} | '_', - pid = self() :: pid() | '$2' | '_' | '$1'}). - --record(muc_registered, - {us_host = {{<<"">>, <<"">>}, <<"">>} :: {{binary(), binary()}, binary()} | '$1', - nick = <<"">> :: binary()}). +-include("mod_muc.hrl"). -record(state, {host = <<"">> :: binary(), @@ -80,20 +70,17 @@ -define(PROCNAME, ejabberd_mod_muc). +-define(MAX_ROOMS_DISCOITEMS, 100). + %%==================================================================== %% 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]}, @@ -101,7 +88,6 @@ start(Host, Opts) -> stop(Host) -> Rooms = shutdown_rooms(Host), - stop_supervisor(Host), Proc = gen_mod:get_module_proc(Host, ?PROCNAME), gen_server:call(Proc, stop), supervisor:delete_child(ejabberd_sup, Proc), @@ -113,7 +99,8 @@ shutdown_rooms(Host) -> Rooms = mnesia:dirty_select(muc_online_room, [{#muc_online_room{name_host = '$1', pid = '$2'}, - [{'==', {element, 2, '$1'}, MyHost}], + [{'==', {element, 2, '$1'}, MyHost}, + {'==', {node, '$2'}, node()}], ['$2']}]), [Pid ! shutdown || Pid <- Rooms], Rooms. @@ -137,7 +124,7 @@ create_room(Host, Name, From, Nick, Opts) -> gen_server:call(Proc, {create, Name, From, Nick, Opts}). store_room(ServerHost, Host, Name, Opts) -> - LServer = jlib:nameprep(ServerHost), + LServer = jid:nameprep(ServerHost), store_room(LServer, Host, Name, Opts, gen_mod:db_type(LServer, ?MODULE)). @@ -165,7 +152,7 @@ store_room(LServer, Host, Name, Opts, odbc) -> ejabberd_odbc:sql_transaction(LServer, F). restore_room(ServerHost, Host, Name) -> - LServer = jlib:nameprep(ServerHost), + LServer = jid:nameprep(ServerHost), restore_room(LServer, Host, Name, gen_mod:db_type(LServer, ?MODULE)). @@ -193,17 +180,20 @@ restore_room(LServer, Host, Name, odbc) -> end. forget_room(ServerHost, Host, Name) -> - LServer = jlib:nameprep(ServerHost), + LServer = jid:nameprep(ServerHost), forget_room(LServer, Host, Name, gen_mod:db_type(LServer, ?MODULE)). -forget_room(_LServer, Host, Name, mnesia) -> +forget_room(LServer, Host, Name, mnesia) -> + remove_room_mam(LServer, Host, Name), F = fun () -> mnesia:delete({muc_room, {Name, Host}}) end, mnesia:transaction(F); -forget_room(_LServer, Host, Name, riak) -> +forget_room(LServer, Host, Name, riak) -> + remove_room_mam(LServer, Host, Name), {atomic, ejabberd_riak:delete(muc_room, {Name, Host})}; forget_room(LServer, Host, Name, odbc) -> + remove_room_mam(LServer, Host, Name), SName = ejabberd_odbc:escape(Name), SHost = ejabberd_odbc:escape(Host), F = fun () -> @@ -213,24 +203,41 @@ forget_room(LServer, Host, Name, odbc) -> end, ejabberd_odbc:sql_transaction(LServer, F). +remove_room_mam(LServer, Host, Name) -> + case gen_mod:is_loaded(LServer, mod_mam) of + true -> + U = jid:nodeprep(Name), + S = jid:nameprep(Host), + DBType = gen_mod:db_type(LServer, mod_mam), + if DBType == odbc -> + mod_mam:remove_user(jid:to_string({U, S, <<>>}), + LServer, DBType); + true -> + mod_mam:remove_user(U, S, DBType) + end; + false -> + ok + end. + process_iq_disco_items(Host, From, To, #iq{lang = Lang} = IQ) -> Rsm = jlib:rsm_decode(IQ), + DiscoNode = fxml:get_tag_attr_s(<<"node">>, IQ#iq.sub_el), Res = IQ#iq{type = result, sub_el = [#xmlel{name = <<"query">>, attrs = [{<<"xmlns">>, ?NS_DISCO_ITEMS}], - children = iq_disco_items(Host, From, Lang, Rsm)}]}, + children = iq_disco_items(Host, From, Lang, DiscoNode, 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), + LServer = jid: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), + {LUser, LServer, _} = jid:tolower(JID), LUS = {LUser, LServer}, case catch mnesia:dirty_select(muc_registered, [{#muc_registered{us_host = '$1', @@ -243,7 +250,7 @@ can_use_nick(_LServer, Host, JID, Nick, mnesia) -> [#muc_registered{us_host = {U, _Host}}] -> U == LUS end; can_use_nick(LServer, Host, JID, Nick, riak) -> - {LUser, LServer, _} = jlib:jid_tolower(JID), + {LUser, LServer, _} = jid:tolower(JID), LUS = {LUser, LServer}, case ejabberd_riak:get_by_index(muc_registered, muc_registered_schema(), @@ -257,7 +264,7 @@ can_use_nick(LServer, Host, JID, Nick, riak) -> end; can_use_nick(LServer, Host, JID, Nick, odbc) -> SJID = - jlib:jid_to_string(jlib:jid_tolower(jlib:jid_remove_resource(JID))), + jid:to_string(jid:tolower(jid:remove_resource(JID))), SNick = ejabberd_odbc:escape(Nick), SHost = ejabberd_odbc:escape(Host), case catch ejabberd_odbc:sql_query(LServer, @@ -273,13 +280,6 @@ can_use_nick(LServer, Host, JID, Nick, odbc) -> %% 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@">>), @@ -305,18 +305,78 @@ init([Host, Opts]) -> 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), + Access = gen_mod:get_opt(access, Opts, + fun(A) when is_atom(A) -> A end, all), + AccessCreate = gen_mod:get_opt(access_create, Opts, + fun(A) when is_atom(A) -> A end, all), + AccessAdmin = gen_mod:get_opt(access_admin, Opts, + fun(A) when is_atom(A) -> A end, + none), + AccessPersistent = gen_mod:get_opt(access_persistent, Opts, + fun(A) when is_atom(A) -> A end, + all), + HistorySize = gen_mod:get_opt(history_size, Opts, + fun(I) when is_integer(I), I>=0 -> I end, + 20), + DefRoomOpts1 = gen_mod:get_opt(default_room_options, Opts, + fun(L) when is_list(L) -> L end, + []), + DefRoomOpts = + lists:flatmap( + fun({Opt, Val}) -> + Bool = fun(B) when is_boolean(B) -> B end, + VFun = case Opt of + allow_change_subj -> Bool; + allow_private_messages -> Bool; + allow_query_users -> Bool; + allow_user_invites -> Bool; + allow_visitor_nickchange -> Bool; + allow_visitor_status -> Bool; + anonymous -> Bool; + captcha_protected -> Bool; + logging -> Bool; + members_by_default -> Bool; + members_only -> Bool; + moderated -> Bool; + password_protected -> Bool; + persistent -> Bool; + public -> Bool; + public_list -> Bool; + mam -> Bool; + password -> fun iolist_to_binary/1; + title -> fun iolist_to_binary/1; + allow_private_messages_from_visitors -> + fun(anyone) -> anyone; + (moderators) -> moderators; + (nobody) -> nobody + end; + max_users -> + fun(I) when is_integer(I), I > 0 -> I end; + presence_broadcast -> + fun(L) -> + lists:map( + fun(moderator) -> moderator; + (participant) -> participant; + (visitor) -> visitor + end, L) + end; + _ -> + ?ERROR_MSG("unknown option ~p with value ~p", + [Opt, Val]), + fun(_) -> undefined end + end, + case gen_mod:get_opt(Opt, [{Opt, Val}], VFun) of + undefined -> []; + NewVal -> [{Opt, NewVal}] + end + end, DefRoomOpts1), + RoomShaper = gen_mod:get_opt(room_shaper, Opts, + fun(A) when is_atom(A) -> A end, + none), + ejabberd_router:register_route(MyHost, Host), load_permanent_rooms(MyHost, Host, {Access, AccessCreate, AccessAdmin, AccessPersistent}, - HistorySize, - RoomShaper), + HistorySize, RoomShaper), {ok, #state{host = MyHost, server_host = Host, access = {Access, AccessCreate, AccessAdmin, AccessPersistent}, @@ -324,15 +384,6 @@ init([Host, Opts]) -> 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, @@ -353,20 +404,8 @@ handle_call({create, Room, From, Nick, Opts}, _From, 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, @@ -393,39 +432,15 @@ handle_info({mnesia_system_event, {mnesia_down, Node}}, State) -> {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) -> @@ -433,13 +448,13 @@ do_route(Host, ServerHost, Access, HistorySize, RoomShaper, case acl:match_rule(ServerHost, AccessRoute, From) of allow -> do_route1(Host, ServerHost, Access, HistorySize, RoomShaper, - From, To, Packet, DefRoomOpts); + From, To, Packet, DefRoomOpts); _ -> #xmlel{attrs = Attrs} = Packet, - Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), + Lang = fxml:get_attr_s(<<"xml:lang">>, Attrs), ErrText = <<"Access denied by service policy">>, Err = jlib:make_error_reply(Packet, - ?ERRT_FORBIDDEN(Lang, ErrText)), + ?ERRT_FORBIDDEN(Lang, ErrText)), ejabberd_router:route_error(To, From, Err, Packet) end. @@ -447,7 +462,7 @@ do_route(Host, ServerHost, Access, HistorySize, RoomShaper, do_route1(Host, ServerHost, Access, HistorySize, RoomShaper, From, To, Packet, DefRoomOpts) -> {_AccessRoute, AccessCreate, AccessAdmin, _AccessPersistent} = Access, - {Room, _, Nick} = jlib:jid_tolower(To), + {Room, _, Nick} = jid:tolower(To), #xmlel{name = Name, attrs = Attrs} = Packet, case Room of <<"">> -> @@ -469,7 +484,8 @@ do_route1(Host, ServerHost, Access, HistorySize, RoomShaper, attrs = [{<<"xmlns">>, XMLNS}], children = - iq_disco_info(Lang) ++ + iq_disco_info( + ServerHost, Lang) ++ Info}]}, ejabberd_router:route(To, From, jlib:iq_to_xml(Res)); @@ -541,18 +557,18 @@ do_route1(Host, ServerHost, Access, HistorySize, RoomShaper, _ -> ok end; <<"message">> -> - case xml:get_attr_s(<<"type">>, Attrs) of + case fxml:get_attr_s(<<"type">>, Attrs) of <<"error">> -> ok; _ -> case acl:match_rule(ServerHost, AccessAdmin, From) of allow -> - Msg = xml:get_path_s(Packet, + Msg = fxml:get_path_s(Packet, [{elem, <<"body">>}, cdata]), broadcast_service_message(Host, Msg); _ -> - Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), + Lang = fxml:get_attr_s(<<"xml:lang">>, Attrs), ErrText = <<"Only service administrators are allowed " "to send service messages">>, @@ -565,70 +581,72 @@ do_route1(Host, ServerHost, Access, HistorySize, RoomShaper, <<"presence">> -> ok end; _ -> - case xml:get_attr_s(<<"type">>, Attrs) of + case fxml: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; - _ -> + 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), + Type = fxml:get_attr_s(<<"type">>, Attrs), case {Name, Type} of {<<"presence">>, <<"">>} -> case check_user_can_create_room(ServerHost, - AccessCreate, From, - Room) of + AccessCreate, From, Room) and + check_create_roomid(ServerHost, Room) of true -> - {ok, Pid} = start_new_room( - Host, ServerHost, Access, - Room, HistorySize, - RoomShaper, From, - Nick, DefRoomOpts), + {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), + Lang = fxml: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 + Lang = fxml: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) -> + 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); + allow -> true; _ -> false end. +check_create_roomid(ServerHost, RoomID) -> + Max = gen_mod:get_module_opt(ServerHost, ?MODULE, max_room_id, + fun(infinity) -> infinity; + (I) when is_integer(I), I>0 -> I + end, infinity), + Regexp = gen_mod:get_module_opt(ServerHost, ?MODULE, regexp_room_id, + fun iolist_to_binary/1, ""), + (byte_size(RoomID) =< Max) and + (re:run(RoomID, Regexp, [unicode, {capture, none}]) == match). + get_rooms(ServerHost, Host) -> - LServer = jlib:nameprep(ServerHost), + LServer = jid:nameprep(ServerHost), get_rooms(LServer, Host, gen_mod:db_type(LServer, ?MODULE)). @@ -667,52 +685,47 @@ get_rooms(LServer, Host, odbc) -> Err -> ?ERROR_MSG("failed to get rooms: ~p", [Err]), [] end. -load_permanent_rooms(Host, ServerHost, Access, HistorySize, RoomShaper) -> +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)). + {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) -> + HistorySize, RoomShaper, From, + Nick, DefRoomOpts) -> case restore_room(ServerHost, Host, Room) of - error -> + error -> ?DEBUG("MUC: open new room '~s'~n", [Room]), - mod_muc_room:start(Host, ServerHost, Access, - Room, HistorySize, - RoomShaper, From, - Nick, DefRoomOpts); - Opts -> + 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) + 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:write(#muc_online_room{name_host = {Room, Host}, + pid = Pid}) + end, mnesia:transaction(F). -iq_disco_info(Lang) -> +iq_disco_info(ServerHost, Lang) -> [#xmlel{name = <<"identity">>, attrs = [{<<"category">>, <<"conference">>}, @@ -733,40 +746,49 @@ iq_disco_info(Lang) -> #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 = [{<<"var">>, ?NS_VCARD}], children = []}] ++ + case gen_mod:is_loaded(ServerHost, mod_mam) of + true -> + [#xmlel{name = <<"feature">>, + attrs = [{<<"var">>, ?NS_MAM_TMP}]}, + #xmlel{name = <<"feature">>, + attrs = [{<<"var">>, ?NS_MAM_0}]}, + #xmlel{name = <<"feature">>, + attrs = [{<<"var">>, ?NS_MAM_1}]}]; + false -> + [] + end. + +iq_disco_items(Host, From, Lang, <<>>, none) -> + Rooms = get_vh_rooms(Host), + case erlang:length(Rooms) < ?MAX_ROOMS_DISCOITEMS of + true -> + iq_disco_items_list(Host, Rooms, {get_disco_item, all, From, Lang}); + false -> + iq_disco_items(Host, From, Lang, <<"nonemptyrooms">>, none) + end; +iq_disco_items(Host, From, Lang, <<"nonemptyrooms">>, none) -> + XmlEmpty = #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) -> + [{<<"jid">>, <<"conference.localhost">>}, + {<<"node">>, <<"emptyrooms">>}, + {<<"name">>, translate:translate(Lang, <<"Empty Rooms">>)}], + children = []}, + Query = {get_disco_item, only_non_empty, From, Lang}, + [XmlEmpty | iq_disco_items_list(Host, get_vh_rooms(Host), Query)]; +iq_disco_items(Host, From, Lang, <<"emptyrooms">>, none) -> + iq_disco_items_list(Host, get_vh_rooms(Host), {get_disco_item, 0, From, Lang}); +iq_disco_items(Host, From, Lang, _DiscoNode, Rsm) -> {Rooms, RsmO} = get_vh_rooms(Host, Rsm), RsmOut = jlib:rsm_encode(RsmO), + iq_disco_items_list(Host, Rooms, {get_disco_item, all, From, Lang}) ++ RsmOut. + +iq_disco_items_list(Host, Rooms, Query) -> 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}, + Query, 100) of {item, Desc} -> @@ -775,15 +797,13 @@ iq_disco_items(Host, From, Lang, Rsm) -> #xmlel{name = <<"item">>, attrs = [{<<"jid">>, - jlib:jid_to_string({Name, Host, + jid:to_string({Name, Host, <<"">>})}, {<<"name">>, Desc}], children = []}}; _ -> false end - end, - Rooms) - ++ RsmOut. + end, Rooms). get_vh_rooms(Host, #rsm_in{max=M, direction=Direction, id=I, index=Index})-> AllRooms = lists:sort(get_vh_rooms(Host)), @@ -837,13 +857,6 @@ get_room_pos(Desired, [_ | Rooms], HeadPosition) -> 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}, @@ -855,16 +868,16 @@ flush() -> receive _ -> flush() after 0 -> ok end. iq_get_unique(From) -> {xmlcdata, - p1_sha:sha(term_to_binary([From, now(), + p1_sha:sha(term_to_binary([From, p1_time_compat:timestamp(), randoms:get_string()]))}. get_nick(ServerHost, Host, From) -> - LServer = jlib:nameprep(ServerHost), + LServer = jid: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), + {LUser, LServer, _} = jid:tolower(From), LUS = {LUser, LServer}, case catch mnesia:dirty_read(muc_registered, {LUS, Host}) @@ -874,7 +887,7 @@ get_nick(_LServer, Host, From, mnesia) -> [#muc_registered{nick = Nick}] -> Nick end; get_nick(LServer, Host, From, riak) -> - {LUser, LServer, _} = jlib:jid_tolower(From), + {LUser, LServer, _} = jid:tolower(From), US = {LUser, LServer}, case ejabberd_riak:get(muc_registered, muc_registered_schema(), @@ -884,7 +897,7 @@ get_nick(LServer, Host, From, riak) -> end; get_nick(LServer, Host, From, odbc) -> SJID = - ejabberd_odbc:escape(jlib:jid_to_string(jlib:jid_tolower(jlib:jid_remove_resource(From)))), + ejabberd_odbc:escape(jid:to_string(jid:tolower(jid:remove_resource(From)))), SHost = ejabberd_odbc:escape(Host), case catch ejabberd_odbc:sql_query(LServer, [<<"select nick from muc_registered where " @@ -932,12 +945,12 @@ iq_get_register_info(ServerHost, Host, From, Lang) -> Nick)]}]. set_nick(ServerHost, Host, From, Nick) -> - LServer = jlib:nameprep(ServerHost), + LServer = jid: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), + {LUser, LServer, _} = jid:tolower(From), LUS = {LUser, LServer}, F = fun () -> case Nick of @@ -967,7 +980,7 @@ set_nick(_LServer, Host, From, Nick, mnesia) -> end, mnesia:transaction(F); set_nick(LServer, Host, From, Nick, riak) -> - {LUser, LServer, _} = jlib:jid_tolower(From), + {LUser, LServer, _} = jid:tolower(From), LUS = {LUser, LServer}, {atomic, case Nick of @@ -997,7 +1010,7 @@ set_nick(LServer, Host, From, Nick, riak) -> end}; set_nick(LServer, Host, From, Nick, odbc) -> JID = - jlib:jid_to_string(jlib:jid_tolower(jlib:jid_remove_resource(From))), + jid:to_string(jid:tolower(jid:remove_resource(From))), SJID = ejabberd_odbc:escape(JID), SNick = ejabberd_odbc:escape(Nick), SHost = ejabberd_odbc:escape(Host), @@ -1049,12 +1062,12 @@ iq_set_register_info(ServerHost, Host, From, Nick, process_iq_register_set(ServerHost, Host, From, SubEl, Lang) -> #xmlel{children = Els} = SubEl, - case xml:get_subtag(SubEl, <<"remove">>) of + case fxml:get_subtag(SubEl, <<"remove">>) of false -> - case xml:remove_cdata(Els) of + case fxml:remove_cdata(Els) of [#xmlel{name = <<"x">>} = XEl] -> - case {xml:get_tag_attr_s(<<"xmlns">>, XEl), - xml:get_tag_attr_s(<<"type">>, XEl)} + case {fxml:get_tag_attr_s(<<"xmlns">>, XEl), + fxml:get_tag_attr_s(<<"type">>, XEl)} of {?NS_XDATA, <<"cancel">>} -> {result, []}; {?NS_XDATA, <<"submit">>} -> @@ -1092,15 +1105,14 @@ iq_get_vcard(Lang) -> [{xmlcdata, <<(translate:translate(Lang, <<"ejabberd MUC module">>))/binary, - "\nCopyright (c) 2003-2015 ProcessOne">>}]}]. - + "\nCopyright (c) 2003-2016 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)). + 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) -> @@ -1247,8 +1259,8 @@ export(_Server) -> case str:suffix(Host, RoomHost) of true -> SJID = ejabberd_odbc:escape( - jlib:jid_to_string( - jlib:make_jid(U, S, <<"">>))), + jid:to_string( + jid:make(U, S, <<"">>))), SNick = ejabberd_odbc:escape(Nick), SRoomHost = ejabberd_odbc:escape(RoomHost), [[<<"delete from muc_registered where jid='">>, @@ -1266,13 +1278,12 @@ import(_LServer) -> [{<<"select name, host, opts from muc_room;">>, fun([Name, RoomHost, SOpts]) -> Opts = opts_to_binary(ejabberd_odbc:decode_term(SOpts)), - #muc_room{name_host = {Name, RoomHost}, - opts = Opts} + #muc_room{name_host = {Name, RoomHost}, opts = Opts} end}, {<<"select jid, host, nick from muc_registered;">>, fun([J, RoomHost, Nick]) -> #jid{user = U, server = S} = - jlib:string_to_jid(J), + jid:from_string(J), #muc_registered{us_host = {{U, S}, RoomHost}, nick = Nick} end}]. @@ -1289,3 +1300,58 @@ import(_LServer, riak, [{'2i', [{<<"nick_host">>, {Nick, Host}}]}]); import(_, _, _) -> pass. + +mod_opt_type(access) -> + fun (A) when is_atom(A) -> A end; +mod_opt_type(access_admin) -> + fun (A) when is_atom(A) -> A end; +mod_opt_type(access_create) -> + fun (A) when is_atom(A) -> A end; +mod_opt_type(access_persistent) -> + fun (A) when is_atom(A) -> A end; +mod_opt_type(db_type) -> fun gen_mod:v_db/1; +mod_opt_type(default_room_options) -> + fun (L) when is_list(L) -> L end; +mod_opt_type(history_size) -> + fun (I) when is_integer(I), I >= 0 -> I end; +mod_opt_type(host) -> fun iolist_to_binary/1; +mod_opt_type(max_room_desc) -> + fun (infinity) -> infinity; + (I) when is_integer(I), I > 0 -> I + end; +mod_opt_type(max_room_id) -> + fun (infinity) -> infinity; + (I) when is_integer(I), I > 0 -> I + end; +mod_opt_type(regexp_room_id) -> + fun iolist_to_binary/1; +mod_opt_type(max_room_name) -> + fun (infinity) -> infinity; + (I) when is_integer(I), I > 0 -> I + end; +mod_opt_type(max_user_conferences) -> + fun (I) when is_integer(I), I > 0 -> I end; +mod_opt_type(max_users) -> + fun (I) when is_integer(I), I > 0 -> I end; +mod_opt_type(max_users_admin_threshold) -> + fun (I) when is_integer(I), I > 0 -> I end; +mod_opt_type(max_users_presence) -> + fun (MUP) when is_integer(MUP) -> MUP end; +mod_opt_type(min_message_interval) -> + fun (MMI) when is_number(MMI) -> MMI end; +mod_opt_type(min_presence_interval) -> + fun (I) when is_number(I), I >= 0 -> I end; +mod_opt_type(room_shaper) -> + fun (A) when is_atom(A) -> A end; +mod_opt_type(user_message_shaper) -> + fun (A) when is_atom(A) -> A end; +mod_opt_type(user_presence_shaper) -> + fun (A) when is_atom(A) -> A end; +mod_opt_type(_) -> + [access, access_admin, access_create, access_persistent, + db_type, default_room_options, history_size, host, + max_room_desc, max_room_id, max_room_name, regexp_room_id, + max_user_conferences, max_users, + max_users_admin_threshold, max_users_presence, + min_message_interval, min_presence_interval, + room_shaper, user_message_shaper, user_presence_shaper]. diff --git a/src/mod_muc_admin.erl b/src/mod_muc_admin.erl index 11f96b9b..7c6e84c4 100644 --- a/src/mod_muc_admin.erl +++ b/src/mod_muc_admin.erl @@ -11,48 +11,39 @@ -behaviour(gen_mod). --export([ - start/2, stop/1, % gen_mod API - muc_online_rooms/1, - muc_unregister_nick/1, - create_room/3, destroy_room/3, +-export([start/2, stop/1, muc_online_rooms/1, + muc_unregister_nick/1, create_room/3, destroy_room/2, create_rooms_file/1, destroy_rooms_file/1, rooms_unused_list/2, rooms_unused_destroy/2, - get_user_rooms/2, - get_room_occupants/2, - get_room_occupants_number/2, - send_direct_invitation/4, - change_room_option/4, - set_room_affiliation/4, - get_room_affiliations/2, - web_menu_main/2, web_page_main/2, % Web Admin API - web_menu_host/3, web_page_host/3 - ]). + get_user_rooms/2, get_room_occupants/2, + get_room_occupants_number/2, send_direct_invitation/5, + change_room_option/4, get_room_options/2, + set_room_affiliation/4, get_room_affiliations/2, + web_menu_main/2, web_page_main/2, web_menu_host/3, + web_page_host/3, mod_opt_type/1, get_commands_spec/0]). -include("ejabberd.hrl"). -include("logger.hrl"). -include("jlib.hrl"). -include("mod_muc_room.hrl"). +-include("mod_muc.hrl"). -include("ejabberd_http.hrl"). -include("ejabberd_web_admin.hrl"). -include("ejabberd_commands.hrl"). -%% Copied from mod_muc/mod_muc.erl --record(muc_online_room, {name_host, pid}). - %%---------------------------- %% gen_mod %%---------------------------- start(Host, _Opts) -> - ejabberd_commands:register_commands(commands()), + ejabberd_commands:register_commands(get_commands_spec()), ejabberd_hooks:add(webadmin_menu_main, ?MODULE, web_menu_main, 50), ejabberd_hooks:add(webadmin_menu_host, Host, ?MODULE, web_menu_host, 50), ejabberd_hooks:add(webadmin_page_main, ?MODULE, web_page_main, 50), ejabberd_hooks:add(webadmin_page_host, Host, ?MODULE, web_page_host, 50). stop(Host) -> - ejabberd_commands:unregister_commands(commands()), + ejabberd_commands:unregister_commands(get_commands_spec()), ejabberd_hooks:delete(webadmin_menu_main, ?MODULE, web_menu_main, 50), ejabberd_hooks:delete(webadmin_menu_host, Host, ?MODULE, web_menu_host, 50), ejabberd_hooks:delete(webadmin_page_main, ?MODULE, web_page_main, 50), @@ -62,10 +53,11 @@ stop(Host) -> %%% Register commands %%% -commands() -> +get_commands_spec() -> [ #ejabberd_commands{name = muc_online_rooms, tags = [muc], desc = "List existing rooms ('global' to get all vhosts)", + policy = admin, module = ?MODULE, function = muc_online_rooms, args = [{host, binary}], result = {rooms, {list, {room, string}}}}, @@ -84,16 +76,17 @@ commands() -> #ejabberd_commands{name = destroy_room, tags = [muc_room], desc = "Destroy a MUC room", module = ?MODULE, function = destroy_room, - args = [{name, binary}, {service, binary}, - {host, binary}], + args = [{name, binary}, {service, binary}], result = {res, rescode}}, #ejabberd_commands{name = create_rooms_file, tags = [muc], desc = "Create the rooms indicated in file", + longdesc = "Provide one room JID per line. Rooms will be created after restart.", module = ?MODULE, function = create_rooms_file, args = [{file, string}], result = {res, rescode}}, #ejabberd_commands{name = destroy_rooms_file, tags = [muc], desc = "Destroy the rooms indicated in file", + longdesc = "Provide one room JID per line.", module = ?MODULE, function = destroy_rooms_file, args = [{file, string}], result = {res, rescode}}, @@ -136,7 +129,7 @@ commands() -> desc = "Send a direct invitation to several destinations", longdesc = "Password and Message can also be: none. Users JIDs are separated with : ", module = ?MODULE, function = send_direct_invitation, - args = [{room, binary}, {password, binary}, {reason, binary}, {users, binary}], + args = [{name, binary}, {service, binary}, {password, binary}, {reason, binary}, {users, binary}], result = {res, rescode}}, #ejabberd_commands{name = change_room_option, tags = [muc_room], @@ -145,6 +138,16 @@ commands() -> args = [{name, binary}, {service, binary}, {option, binary}, {value, binary}], result = {res, rescode}}, + #ejabberd_commands{name = get_room_options, tags = [muc_room], + desc = "Get options from a MUC room", + module = ?MODULE, function = get_room_options, + args = [{name, binary}, {service, binary}], + result = {options, {list, + {option, {tuple, + [{name, string}, + {value, string} + ]}} + }}}, #ejabberd_commands{name = set_room_affiliation, tags = [muc_room], desc = "Change an affiliation in a MUC room", @@ -175,7 +178,8 @@ muc_online_rooms(ServerHost) -> MUCHost = find_host(ServerHost), Rooms = ets:tab2list(muc_online_room), lists:foldl( - fun({_, {Roomname, Host}, _}, Results) -> + fun(Room, Results) -> + {Roomname, Host} = Room#muc_online_room.name_host, case MUCHost of global -> [<<Roomname/binary, "@", Host/binary>> | Results]; @@ -238,8 +242,8 @@ web_menu_host(Acc, _Host, Lang) -> ])). web_page_main(_, #request{path=[<<"muc">>], lang = Lang} = _Request) -> - Res = [?XC(<<"h1">>, <<"Multi-User Chat">>), - ?XC(<<"h3">>, <<"Statistics">>), + Res = [?XCT(<<"h1">>, <<"Multi-User Chat">>), + ?XCT(<<"h3">>, <<"Statistics">>), ?XAE(<<"table">>, [], [?XE(<<"tbody">>, [?TDTD(<<"Total rooms">>, ets:info(muc_online_room, size)), ?TDTD(<<"Permanent rooms">>, mnesia:table_info(muc_room, size)), @@ -276,7 +280,7 @@ get_sort_query(Q) -> get_sort_query2(Q) -> {value, {_, String}} = lists:keysearch(<<"sort">>, 1, Q), - Integer = list_to_integer(binary_to_list(String)), + Integer = jlib:binary_to_integer(String), case Integer >= 0 of true -> {ok, {normal, Integer}}; false -> {ok, {reverse, abs(Integer)}} @@ -298,22 +302,22 @@ make_rooms_page(Host, Lang, {Sort_direction, Sort_column}) -> <<"Persistent">>, <<"Logging">>, <<"Just created">>, - <<"Title">>], + <<"Room title">>], {Titles_TR, _} = lists:mapfoldl( fun(Title, Num_column) -> NCS = jlib:integer_to_binary(Num_column), TD = ?XE(<<"td">>, [?CT(Title), ?C(<<" ">>), - ?ACT(<<"?sort=", NCS/binary>>, <<"<">>), + ?AC(<<"?sort=", NCS/binary>>, <<"<">>), ?C(<<" ">>), - ?ACT(<<"?sort=-", NCS/binary>>, <<">">>)]), + ?AC(<<"?sort=-", NCS/binary>>, <<">">>)]), {TD, Num_column+1} end, 1, Titles), - [?XC(<<"h1">>, <<"Multi-User Chat">>), - ?XC(<<"h2">>, <<"Rooms">>), + [?XCT(<<"h1">>, <<"Multi-User Chat">>), + ?XCT(<<"h2">>, <<"Chatrooms">>), ?XE(<<"table">>, [?XE(<<"thead">>, [?XE(<<"tr">>, Titles_TR)] @@ -352,7 +356,7 @@ build_info_room({Name, Host, Pid}) -> false -> Last_message1 = queue:last(History), {_, _, _, Ts_last, _} = Last_message1, - jlib:timestamp_to_iso(Ts_last) + jlib:timestamp_to_legacy(Ts_last) end, {<<Name/binary, "@", Host/binary>>, @@ -392,7 +396,9 @@ prepare_room_info(Room_info) -> %% @spec (Name::binary(), Host::binary(), ServerHost::binary()) -> %% ok | error %% @doc Create a room immediately with the default options. -create_room(Name, Host, ServerHost) -> +create_room(Name1, Host1, ServerHost) -> + Name = jid:nodeprep(Name1), + Host = jid:nodeprep(Host1), %% Get the default room options from the muc configuration DefRoomOpts = gen_mod:get_module_opt(ServerHost, mod_muc, @@ -406,7 +412,6 @@ create_room(Name, Host, ServerHost) -> AcCreate = gen_mod:get_module_opt(ServerHost, mod_muc, access_create, fun(X) -> X end, all), AcAdmin = gen_mod:get_module_opt(ServerHost, mod_muc, access_admin, fun(X) -> X end, none), AcPer = gen_mod:get_module_opt(ServerHost, mod_muc, access_persistent, fun(X) -> X end, all), - _PersistHistory = gen_mod:get_module_opt(ServerHost, mod_muc, persist_history, fun(X) -> X end, false), HistorySize = gen_mod:get_module_opt(ServerHost, mod_muc, history_size, fun(X) -> X end, 20), RoomShaper = gen_mod:get_module_opt(ServerHost, mod_muc, room_shaper, fun(X) -> X end, none), @@ -441,12 +446,12 @@ muc_create_room(ServerHost, {Name, Host, _}, DefRoomOpts) -> io:format("Creating room ~s@~s~n", [Name, Host]), mod_muc:store_room(ServerHost, Host, Name, DefRoomOpts). -%% @spec (Name::binary(), Host::binary(), ServerHost::binary()) -> +%% @spec (Name::binary(), Host::binary()) -> %% ok | {error, room_not_exists} %% @doc Destroy the room immediately. %% If the room has participants, they are not notified that the room was destroyed; %% they will notice when they try to chat and receive an error that the room doesn't exist. -destroy_room(Name, Service, _Server) -> +destroy_room(Name, Service) -> case mnesia:dirty_read(muc_online_room, {Name, Service}) of [R] -> Pid = R#muc_online_room.pid, @@ -458,7 +463,7 @@ destroy_room(Name, Service, _Server) -> destroy_room({N, H, SH}) -> io:format("Destroying room: ~s@~s - vhost: ~s~n", [N, H, SH]), - destroy_room(N, H, SH). + destroy_room(N, H). %%---------------------------- @@ -469,7 +474,7 @@ destroy_room({N, H, SH}) -> %% The file encoding must be UTF-8 destroy_rooms_file(Filename) -> - {ok, F} = file:open(Filename, [read]), + {ok, F} = file:open(Filename, [read, binary]), RJID = read_room(F), Rooms = read_rooms(F, RJID, []), file:close(F), @@ -497,23 +502,16 @@ read_room(F) -> %% This function is quite rudimentary %% and may not be accurate split_roomjid(RoomJID) -> - [Name, Host] = string:tokens(RoomJID, "@"), - [_MUC_service_name | ServerHostList] = string:tokens(Host, "."), - ServerHost = join(ServerHostList, "."), + [Name, Host] = binary:split(RoomJID, <<"@">>), + [_MUC_service_name, ServerHost] = binary:split(Host, <<".">>), {Name, Host, ServerHost}. -%% This function is copied from string:join/2 in Erlang/OTP R12B-1 -%% Note that string:join/2 is not implemented in Erlang/OTP R11B -join([H|T], Sep) -> - H ++ lists:concat([Sep ++ X || X <- T]). - - %%---------------------------- %% Create Rooms in File %%---------------------------- create_rooms_file(Filename) -> - {ok, F} = file:open(Filename, [read]), + {ok, F} = file:open(Filename, [read, binary]), RJID = read_room(F), Rooms = read_rooms(F, RJID, []), file:close(F), @@ -604,7 +602,7 @@ decide_room({_Room_name, _Host, Room_pid}, Last_allowed) -> Num_users = length(?DICT:to_list(Room_users)), History = (S#state.history)#lqueue.queue, - Ts_now = calendar:now_to_universal_time(now()), + Ts_now = calendar:universal_time(), Ts_uptime = uptime_seconds(), {Has_hist, Last} = case queue:is_empty(History) of true -> @@ -672,7 +670,7 @@ get_room_occupants(Pid) -> S = get_room_state(Pid), lists:map( fun({_LJID, Info}) -> - {jlib:jid_to_string(Info#user.jid), + {jid:to_string(Info#user.jid), Info#user.nick, atom_to_list(Info#user.role)} end, @@ -686,32 +684,36 @@ get_room_occupants_number(Room, Host) -> %%---------------------------- %% http://xmpp.org/extensions/xep-0249.html -send_direct_invitation(RoomString, Password, Reason, UsersString) -> - RoomJid = jlib:string_to_jid(RoomString), +send_direct_invitation(RoomName, RoomService, Password, Reason, UsersString) -> + RoomJid = jid:make(RoomName, RoomService, <<"">>), + RoomString = jid:to_string(RoomJid), XmlEl = build_invitation(Password, Reason, RoomString), - UsersStrings = get_users_to_invite(RoomJid, binary_to_list(UsersString)), - [send_direct_invitation(RoomJid, jlib:string_to_jid(list_to_binary(UserStrings)), XmlEl) + UsersStrings = get_users_to_invite(RoomJid, UsersString), + [send_direct_invitation(RoomJid, UserStrings, XmlEl) || UserStrings <- UsersStrings], timer:sleep(1000), ok. get_users_to_invite(RoomJid, UsersString) -> - UsersStrings = string:tokens(UsersString, ":"), + UsersStrings = binary:split(UsersString, <<":">>, [global]), OccupantsTuples = get_room_occupants(RoomJid#jid.luser, RoomJid#jid.lserver), - OccupantsJids = [jlib:string_to_jid(JidString) + OccupantsJids = [jid:from_string(JidString) || {JidString, _Nick, _} <- OccupantsTuples], - lists:filter( - fun(UserString) -> - UserJid = jlib:string_to_jid(list_to_binary(UserString)), - %% [{"badlop@localhost/work","badlop","moderator"}] - lists:all(fun(OccupantJid) -> - UserJid#jid.luser /= OccupantJid#jid.luser - orelse UserJid#jid.lserver /= OccupantJid#jid.lserver - end, - OccupantsJids) - end, - UsersStrings). + lists:filtermap( + fun(UserString) -> + UserJid = jid:from_string(UserString), + Val = lists:all(fun(OccupantJid) -> + UserJid#jid.luser /= OccupantJid#jid.luser + orelse UserJid#jid.lserver /= OccupantJid#jid.lserver + end, + OccupantsJids), + case Val of + true -> {true, UserJid}; + _ -> false + end + end, + UsersStrings). build_invitation(Password, Reason, RoomString) -> PasswordAttrList = case Password of @@ -782,10 +784,17 @@ change_option(Option, Value, Config) -> case Option of allow_change_subj -> Config#config{allow_change_subj = Value}; allow_private_messages -> Config#config{allow_private_messages = Value}; + allow_private_messages_from_visitors -> Config#config{allow_private_messages_from_visitors = Value}; allow_query_users -> Config#config{allow_query_users = Value}; allow_user_invites -> Config#config{allow_user_invites = Value}; + allow_visitor_nickchange -> Config#config{allow_visitor_nickchange = Value}; + allow_visitor_status -> Config#config{allow_visitor_status = Value}; + allow_voice_requests -> Config#config{allow_voice_requests = Value}; anonymous -> Config#config{anonymous = Value}; + captcha_protected -> Config#config{captcha_protected = Value}; + description -> Config#config{description = Value}; logging -> Config#config{logging = Value}; + mam -> Config#config{mam = Value}; max_users -> Config#config{max_users = Value}; members_by_default -> Config#config{members_by_default = Value}; members_only -> Config#config{members_only = Value}; @@ -795,9 +804,29 @@ change_option(Option, Value, Config) -> persistent -> Config#config{persistent = Value}; public -> Config#config{public = Value}; public_list -> Config#config{public_list = Value}; - title -> Config#config{title = Value} + title -> Config#config{title = Value}; + vcard -> Config#config{vcard = Value}; + voice_request_min_interval -> Config#config{voice_request_min_interval = Value} + end. + +%%---------------------------- +%% Get Room Options +%%---------------------------- + +get_room_options(Name, Service) -> + case get_room_pid(Name, Service) of + room_not_found -> []; + Pid -> get_room_options(Pid) end. +get_room_options(Pid) -> + Config = get_room_config(Pid), + get_options(Config). + +get_options(Config) -> + Fields = record_info(fields, config), + [config | Values] = tuple_to_list(Config), + lists:zip(Fields, Values). %%---------------------------- %% Get Room Affiliations @@ -841,34 +870,32 @@ set_room_affiliation(Name, Service, JID, AffiliationString) -> [R] -> %% Get the PID for the online room so we can get the state of the room Pid = R#muc_online_room.pid, - {ok, StateData} = gen_fsm:sync_send_all_state_event(Pid, {process_item_change, {jlib:string_to_jid(JID), affiliation, Affiliation, <<"">>}, <<"">>}), + {ok, StateData} = gen_fsm:sync_send_all_state_event(Pid, {process_item_change, {jid:from_string(JID), affiliation, Affiliation, <<"">>}, <<"">>}), mod_muc:store_room(StateData#state.server_host, StateData#state.host, StateData#state.room, make_opts(StateData)), ok; [] -> error end. --define(MAKE_CONFIG_OPT(Opt), {Opt, Config#config.Opt}). - make_opts(StateData) -> Config = StateData#state.config, [ - ?MAKE_CONFIG_OPT(title), - ?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(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), + {title, Config#config.title}, + {allow_change_subj, Config#config.allow_change_subj}, + {allow_query_users, Config#config.allow_query_users}, + {allow_private_messages, Config#config.allow_private_messages}, + {public, Config#config.public}, + {public_list, Config#config.public_list}, + {persistent, Config#config.persistent}, + {moderated, Config#config.moderated}, + {members_by_default, Config#config.members_by_default}, + {members_only, Config#config.members_only}, + {allow_user_invites, Config#config.allow_user_invites}, + {password_protected, Config#config.password_protected}, + {password, Config#config.password}, + {anonymous, Config#config.anonymous}, + {logging, Config#config.logging}, + {max_users, Config#config.max_users}, {affiliations, ?DICT:to_list(StateData#state.affiliations)}, {subject, StateData#state.subject}, {subject_author, StateData#state.subject_author} @@ -892,3 +919,5 @@ find_host(ServerHost) when is_list(ServerHost) -> find_host(list_to_binary(ServerHost)); find_host(ServerHost) -> gen_mod:get_module_opt_host(ServerHost, mod_muc, <<"conference.@HOST@">>). + +mod_opt_type(_) -> []. diff --git a/src/mod_muc_log.erl b/src/mod_muc_log.erl index d9415141..1f32f6f9 100644 --- a/src/mod_muc_log.erl +++ b/src/mod_muc_log.erl @@ -5,7 +5,7 @@ %%% Created : 12 Mar 2006 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,6 +25,10 @@ -module(mod_muc_log). +-protocol({xep, 334, '0.2'}). + +-behaviour(ejabberd_config). + -author('badlop@process-one.net'). -behaviour(gen_server). @@ -35,21 +39,17 @@ -export([start_link/2, start/2, stop/1, transform_module_options/1, check_access_log/2, add_to_log/5]). -%% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, - handle_info/2, terminate/2, code_change/3]). + handle_info/2, terminate/2, code_change/3, + mod_opt_type/1, opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). -include("jlib.hrl"). - +-include("mod_muc.hrl"). -include("mod_muc_room.hrl"). -%% Copied from mod_muc/mod_muc.erl --record(muc_online_room, {name_host = {<<>>, <<>>} :: {binary(), binary()}, - pid = self() :: pid()}). - -define(T(Text), translate:translate(Lang, Text)). -define(PROCNAME, ejabberd_mod_muc_log). -record(room, {jid, title, subject, subject_author, config}). @@ -74,23 +74,14 @@ %%==================================================================== %% 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) -> Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - ChildSpec = - {Proc, - {?MODULE, start_link, [Host, Opts]}, - temporary, - 1000, - worker, - [?MODULE]}, + ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]}, + temporary, 1000, worker, [?MODULE]}, supervisor:start_child(ejabberd_sup, ChildSpec). stop(Host) -> @@ -121,19 +112,11 @@ transform_module_options(Opts) -> %%==================================================================== %% gen_server callbacks %%==================================================================== - -%%-------------------------------------------------------------------- -%% Function: init(Args) -> {ok, State} | -%% {ok, State, Timeout} | -%% ignore | -%% {stop, Reason} -%% Description: Initiates the server -%%-------------------------------------------------------------------- init([Host, Opts]) -> OutDir = gen_mod:get_opt(outdir, Opts, fun iolist_to_binary/1, <<"www/muc">>), - DirType = gen_mod:get_opt(dirtype, Opts, + DirType = gen_mod:get_opt(dirtype, Opts, fun(subdirs) -> subdirs; (plain) -> plain end, subdirs), @@ -179,31 +162,17 @@ init([Host, Opts]) -> {ok, #logstate{host = Host, out_dir = OutDir, dir_type = DirType, dir_name = DirName, - file_format = FileFormat, file_permissions = FilePermissions, css_file = CSSFile, + file_format = FileFormat, css_file = CSSFile, + file_permissions = FilePermissions, access = AccessLog, lang = Lang, timezone = Timezone, spam_prevention = NoFollow, top_link = Top_link}}. -%%-------------------------------------------------------------------- -%% 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({check_access_log, ServerHost, FromJID}, _From, State) -> Reply = acl:match_rule(ServerHost, State#logstate.access, FromJID), {reply, Reply, State}; handle_call(stop, _From, State) -> {stop, normal, ok, State}. -%%-------------------------------------------------------------------- -%% Function: handle_cast(Msg, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling cast messages -%%-------------------------------------------------------------------- handle_cast({add_to_log, Type, Data, Room, Opts}, State) -> case catch add_to_log2(Type, Data, Room, Opts, State) of {'EXIT', Reason} -> ?ERROR_MSG("~p", [Reason]); @@ -212,49 +181,30 @@ handle_cast({add_to_log, Type, Data, Room, Opts}, State) -> {noreply, State}; 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(_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) -> 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 %%-------------------------------------------------------------------- add_to_log2(text, {Nick, Packet}, Room, Opts, State) -> - case {xml:get_subtag(Packet, <<"no-store">>), - xml:get_subtag(Packet, <<"no-permanent-store">>)} - of - {false, false} -> - case {xml:get_subtag(Packet, <<"subject">>), - xml:get_subtag(Packet, <<"body">>)} - of - {false, false} -> ok; - {false, SubEl} -> - Message = {body, xml:get_tag_cdata(SubEl)}, - add_message_to_log(Nick, Message, Room, Opts, State); - {SubEl, _} -> - Message = {subject, xml:get_tag_cdata(SubEl)}, - add_message_to_log(Nick, Message, Room, Opts, State) - end; - {_, _} -> ok + case has_no_permanent_store_hint(Packet) of + false -> + case {fxml:get_subtag(Packet, <<"subject">>), + fxml:get_subtag(Packet, <<"body">>)} + of + {false, false} -> ok; + {false, SubEl} -> + Message = {body, fxml:get_tag_cdata(SubEl)}, + add_message_to_log(Nick, Message, Room, Opts, State); + {SubEl, _} -> + Message = {subject, fxml:get_tag_cdata(SubEl)}, + add_message_to_log(Nick, Message, Room, Opts, State) + end; + true -> ok end; add_to_log2(roomconfig_change, _Occupants, Room, Opts, State) -> @@ -327,7 +277,7 @@ build_filename_string(TimeStamp, OutDir, RoomJID, {Fd, Fn, Fnrel}. get_room_name(RoomJID) -> - JID = jlib:string_to_jid(RoomJID), JID#jid.user. + JID = jid:from_string(RoomJID), JID#jid.user. %% calculate day before get_timestamp_daydiff(TimeStamp, Daydiff) -> @@ -349,12 +299,11 @@ close_previous_log(Fn, Images_dir, FileFormat) -> write_last_lines(_, _, plaintext) -> ok; write_last_lines(F, Images_dir, _FileFormat) -> -%% list_to_integer/2 was introduced in OTP R14 fw(F, <<"<div class=\"legend\">">>), fw(F, <<" <a href=\"http://www.ejabberd.im\"><img " "style=\"border:0\" src=\"~s/powered-by-ejabbe" - "rd.png\" alt=\"Powered by ejabberd\"/></a>">>, + "rd.png\" alt=\"Powered by ejabberd - robust, scalable and extensible XMPP server\"/></a>">>, [Images_dir]), fw(F, <<" <a href=\"http://www.erlang.org/\"><img " @@ -378,7 +327,7 @@ write_last_lines(F, Images_dir, _FileFormat) -> fw(F, <<"</span></div></body></html>">>). set_filemode(Fn, {FileMode, FileGroup}) -> - ok = file:change_mode(Fn, list_to_integer(integer_to_list(FileMode), 8)), + ok = file:change_mode(Fn, list_to_integer(integer_to_list(FileMode), 8)), ok = file:change_group(Fn, FileGroup). htmlize_nick(Nick1, html) -> @@ -397,7 +346,7 @@ add_message_to_log(Nick1, Message, RoomJID, Opts, Room = get_room_info(RoomJID, Opts), Nick = htmlize(Nick1, FileFormat), Nick2 = htmlize_nick(Nick1, FileFormat), - Now = now(), + Now = p1_time_compat:timestamp(), TimeStamp = case Timezone of local -> calendar:now_to_local_time(Now); universal -> calendar:now_to_universal_time(Now) @@ -951,7 +900,7 @@ put_header_script(F) -> put_room_config(_F, _RoomConfig, _Lang, plaintext) -> ok; put_room_config(F, RoomConfig, Lang, _FileFormat) -> - {_, Now2, _} = now(), + {_, Now2, _} = p1_time_compat:timestamp(), fw(F, <<"<div class=\"rc\">">>), fw(F, <<"<div class=\"rct\" onclick=\"sh('a~p');return " @@ -968,7 +917,7 @@ put_room_occupants(_F, _RoomOccupants, _Lang, ok; put_room_occupants(F, RoomOccupants, Lang, _FileFormat) -> - {_, Now2, _} = now(), + {_, Now2, _} = p1_time_compat:timestamp(), %% htmlize %% The default behaviour is to ignore the nofollow spam prevention on links %% (NoFollow=false) @@ -1048,7 +997,7 @@ get_room_info(RoomJID, Opts) -> {value, {_, SA}} -> SA; false -> <<"">> end, - #room{jid = jlib:jid_to_string(RoomJID), title = Title, + #room{jid = jid:to_string(RoomJID), title = Title, subject = Subject, subject_author = SubjectAuthor, config = Opts}. @@ -1163,10 +1112,7 @@ roomoccupants_to_string(Users, _FileFormat) -> Users1 /= []], iolist_to_binary([<<"<div class=\"rcot\">">>, Res, <<"</div>">>]). -%% Users = [{JID, Nick, Role}] group_by_role(Users) -> -%% Role = atom() -%% Users = [{JID, Nick}] {Ms, Ps, Vs, Ns} = lists:foldl(fun ({JID, Nick, moderator}, {Mod, Par, Vis, Non}) -> @@ -1212,7 +1158,7 @@ role_users_to_string(RoleS, Users) -> <<RoleS/binary, ": ", UsersString/binary>>. get_room_occupants(RoomJIDString) -> - RoomJID = jlib:string_to_jid(RoomJIDString), + RoomJID = jid:from_string(RoomJIDString), RoomName = RoomJID#jid.luser, MucService = RoomJID#jid.lserver, StateData = get_room_state(RoomName, MucService), @@ -1238,10 +1184,11 @@ get_room_state(RoomPid) -> get_state), R. -get_proc_name(Host) -> gen_mod:get_module_proc(Host, ?PROCNAME). +get_proc_name(Host) -> + gen_mod:get_module_proc(Host, ?PROCNAME). calc_hour_offset(TimeHere) -> - TimeZero = calendar:now_to_universal_time(now()), + TimeZero = calendar:universal_time(), TimeHereHour = calendar:datetime_to_gregorian_seconds(TimeHere) div 3600, @@ -1252,3 +1199,54 @@ calc_hour_offset(TimeHere) -> fjoin(FileList) -> list_to_binary(filename:join([binary_to_list(File) || File <- FileList])). + +has_no_permanent_store_hint(Packet) -> + fxml:get_subtag_with_xmlns(Packet, <<"no-store">>, ?NS_HINTS) + =/= false orelse + fxml:get_subtag_with_xmlns(Packet, <<"no-storage">>, ?NS_HINTS) + =/= false orelse + fxml:get_subtag_with_xmlns(Packet, <<"no-permanent-store">>, ?NS_HINTS) + =/= false orelse + fxml:get_subtag_with_xmlns(Packet, <<"no-permanent-storage">>, ?NS_HINTS) + =/= false. + +mod_opt_type(access_log) -> + fun (A) when is_atom(A) -> A end; +mod_opt_type(cssfile) -> fun iolist_to_binary/1; +mod_opt_type(dirname) -> + fun (room_jid) -> room_jid; + (room_name) -> room_name + end; +mod_opt_type(dirtype) -> + fun (subdirs) -> subdirs; + (plain) -> plain + end; +mod_opt_type(file_format) -> + fun (html) -> html; + (plaintext) -> plaintext + end; +mod_opt_type(file_permissions) -> + fun (SubOpts) -> + F = fun ({mode, Mode}, {_M, G}) -> {Mode, G}; + ({group, Group}, {M, _G}) -> {M, Group} + end, + lists:foldl(F, {644, 33}, SubOpts) + end; +mod_opt_type(outdir) -> fun iolist_to_binary/1; +mod_opt_type(spam_prevention) -> + fun (B) when is_boolean(B) -> B end; +mod_opt_type(timezone) -> + fun (local) -> local; + (universal) -> universal + end; +mod_opt_type(top_link) -> + fun ([{S1, S2}]) -> + {iolist_to_binary(S1), iolist_to_binary(S2)} + end; +mod_opt_type(_) -> + [access_log, cssfile, dirname, dirtype, file_format, + file_permissions, outdir, spam_prevention, timezone, + top_link]. + +opt_type(language) -> fun iolist_to_binary/1; +opt_type(_) -> [language]. diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl index 8d0b36b6..06fdf325 100644 --- a/src/mod_muc_room.erl +++ b/src/mod_muc_room.erl @@ -5,7 +5,7 @@ %%% Created : 19 Mar 2003 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -34,6 +34,9 @@ start_link/7, start/9, start/7, + get_role/2, + get_affiliation/2, + is_occupant_or_admin/2, route/4]). %% gen_fsm callbacks @@ -55,6 +58,8 @@ -define(MAX_USERS_DEFAULT_LIST, [5, 10, 20, 30, 50, 100, 200, 500, 1000, 2000, 5000]). +-define(DEFAULT_MAX_USERS_PRESENCE,1000). + %-define(DBGFSM, true). -ifdef(DBGFSM). @@ -67,32 +72,19 @@ -endif. -%% Module start with or without supervisor: --ifdef(NO_TRANSIENT_SUPERVISORS). --define(SUPERVISOR_START, - gen_fsm:start(?MODULE, [Host, ServerHost, Access, Room, HistorySize, - RoomShaper, Creator, Nick, DefRoomOpts], - ?FSMOPTS)). --else. --define(SUPERVISOR_START, - Supervisor = gen_mod:get_module_proc(ServerHost, ejabberd_mod_muc_sup), - supervisor:start_child( - Supervisor, [Host, ServerHost, Access, Room, HistorySize, RoomShaper, - Creator, Nick, DefRoomOpts])). --endif. - %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- start(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Creator, Nick, DefRoomOpts) -> - ?SUPERVISOR_START. + gen_fsm:start(?MODULE, [Host, ServerHost, Access, Room, HistorySize, + RoomShaper, Creator, Nick, DefRoomOpts], + ?FSMOPTS). start(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts) -> - Supervisor = gen_mod:get_module_proc(ServerHost, ejabberd_mod_muc_sup), - supervisor:start_child( - Supervisor, [Host, ServerHost, Access, Room, HistorySize, RoomShaper, - Opts]). + gen_fsm:start(?MODULE, [Host, ServerHost, Access, Room, HistorySize, + RoomShaper, Opts], + ?FSMOPTS). start_link(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Creator, Nick, DefRoomOpts) -> @@ -109,23 +101,17 @@ start_link(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts) -> %%% Callback functions from gen_fsm %%%---------------------------------------------------------------------- -%%---------------------------------------------------------------------- -%% Func: init/1 -%% Returns: {ok, StateName, StateData} | -%% {ok, StateName, StateData, Timeout} | -%% ignore | -%% {stop, StopReason} -%%---------------------------------------------------------------------- -init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Creator, _Nick, DefRoomOpts]) -> +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, - history = lqueue_new(HistorySize), - jid = jlib:make_jid(Room, Host, <<"">>), - just_created = true, - room_shaper = Shaper}), + #state{host = Host, server_host = ServerHost, + access = Access, room = Room, + history = lqueue_new(HistorySize), + jid = jid:make(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, @@ -134,8 +120,8 @@ init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Creator, _Nick, D make_opts(State1)); true -> ok end, - ?INFO_MSG("Created MUC room ~s@~s by ~s", - [Room, Host, jlib:jid_to_string(Creator)]), + ?INFO_MSG("Created MUC room ~s@~s by ~s", + [Room, Host, jid:to_string(Creator)]), add_to_log(room_existence, created, State1), add_to_log(room_existence, started, State1), {ok, normal_state, State1}; @@ -147,31 +133,25 @@ init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts]) -> access = Access, room = Room, history = lqueue_new(HistorySize), - jid = jlib:make_jid(Room, Host, <<"">>), + jid = jid:make(Room, Host, <<"">>), room_shaper = Shaper}), add_to_log(room_existence, started, State), {ok, normal_state, State}. -%%---------------------------------------------------------------------- -%% Func: StateName/2 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} -%%---------------------------------------------------------------------- normal_state({route, From, <<"">>, #xmlel{name = <<"message">>, attrs = Attrs, children = Els} = Packet}, StateData) -> - Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), + Lang = fxml: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 + case fxml:get_attr_s(<<"type">>, Attrs) of <<"groupchat">> -> Activity = get_user_activity(From, StateData), - Now = now_to_usec(now()), + Now = p1_time_compat:system_time(micro_seconds), MinMessageInterval = trunc(gen_mod:get_module_opt(StateData#state.server_host, mod_muc, min_message_interval, fun(MMI) when is_number(MMI) -> MMI end, 0) @@ -247,8 +227,9 @@ normal_state({route, From, <<"">>, <<"error">> -> case is_user_online(From, StateData) of true -> - ErrorText = <<"This participant is kicked from the " - "room because he sent an error message">>, + ErrorText = <<"It is not allowed to send error messages to the" + " room. The participant (~s) has sent an error " + "message (~s) and got kicked from the room">>, NewState = expulse_participant(Packet, From, StateData, translate:translate(Lang, ErrorText)), @@ -309,8 +290,8 @@ normal_state({route, From, <<"">>, MinInterval = (StateData#state.config)#config.voice_request_min_interval, BareFrom = - jlib:jid_remove_resource(jlib:jid_tolower(From)), - NowPriority = -now_to_usec(now()), + jid:remove_resource(jid:tolower(From)), + NowPriority = -p1_time_compat:system_time(micro_seconds), CleanPriority = NowPriority + MinInterval * 1000000, @@ -385,7 +366,8 @@ normal_state({route, From, <<"">>, catch send_new_presence(TargetJid, Reason, - NSD), + NSD, + StateData), NSD; _ -> StateData end @@ -412,7 +394,7 @@ normal_state({route, From, <<"">>, {next_state, normal_state, StateData} end; _ -> - case xml:get_attr_s(<<"type">>, Attrs) of + case fxml:get_attr_s(<<"type">>, Attrs) of <<"error">> -> ok; _ -> handle_roommessage_from_nonparticipant(Packet, Lang, @@ -424,62 +406,76 @@ 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 = #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} + reply -> + {next_state, normal_state, StateData}; + IQ0 -> + case ejabberd_hooks:run_fold( + muc_process_iq, + StateData#state.server_host, + IQ0, [StateData, From, StateData#state.jid]) of + ignore -> + {next_state, normal_state, StateData}; + #iq{type = T} = IQRes when T == error; T == result -> + ejabberd_router:route(StateData#state.jid, From, jlib:iq_to_xml(IQRes)), + {next_state, normal_state, StateData}; + #iq{type = Type, xmlns = XMLNS, lang = Lang, + sub_el = #xmlel{name = SubElName, attrs = Attrs} = 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 -> + case fxml:get_attr(<<"node">>, Attrs) of + false -> process_iq_disco_info(From, Type, Lang, StateData); + {value, _} -> {error, ?ERR_SERVICE_UNAVAILABLE} + end; + ?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; + _ -> + Err = jlib:make_error_reply(Packet, + ?ERR_FEATURE_NOT_IMPLEMENTED), + ejabberd_router:route(StateData#state.jid, From, Err), + {next_state, normal_state, StateData} + end end; normal_state({route, From, Nick, #xmlel{name = <<"presence">>} = Packet}, StateData) -> Activity = get_user_activity(From, StateData), - Now = now_to_usec(now()), + Now = p1_time_compat:system_time(micro_seconds), MinPresenceInterval = trunc(gen_mod:get_module_opt(StateData#state.server_host, mod_muc, min_presence_interval, @@ -513,15 +509,15 @@ normal_state({route, From, Nick, normal_state({route, From, ToNick, #xmlel{name = <<"message">>, attrs = Attrs} = Packet}, StateData) -> - Type = xml:get_attr_s(<<"type">>, Attrs), - Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), + Type = fxml:get_attr_s(<<"type">>, Attrs), + Lang = fxml: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">>, + ErrorText = <<"It is not allowed to send error messages to the" + " room. The participant (~s) has sent an error " + "message (~s) and got kicked from the room">>, NewState = expulse_participant(Packet, From, StateData, translate:translate(Lang, ErrorText)), {next_state, normal_state, NewState}; @@ -540,7 +536,7 @@ normal_state({route, From, ToNick, Err = jlib:make_error_reply(Packet, ?ERRT_BAD_REQUEST(Lang, ErrText)), - ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, + ejabberd_router:route(jid:replace_resource(StateData#state.jid, ToNick), From, Err); _ -> @@ -551,7 +547,7 @@ normal_state({route, From, ToNick, Err = jlib:make_error_reply(Packet, ?ERRT_ITEM_NOT_FOUND(Lang, ErrText)), - ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, + ejabberd_router:route(jid:replace_resource(StateData#state.jid, ToNick), From, Err); ToJIDs -> @@ -565,14 +561,14 @@ normal_state({route, From, ToNick, (PmFromVisitors == moderators) and DstIsModerator -> {ok, #user{nick = FromNick}} = - (?DICT):find(jlib:jid_tolower(From), + (?DICT):find(jid:tolower(From), StateData#state.users), FromNickJID = - jlib:jid_replace_resource(StateData#state.jid, + jid:replace_resource(StateData#state.jid, FromNick), X = #xmlel{name = <<"x">>, attrs = [{<<"xmlns">>, ?NS_MUC_USER}]}, - PrivMsg = xml:append_subtags(Packet, [X]), + PrivMsg = fxml:append_subtags(Packet, [X]), [ejabberd_router:route(FromNickJID, ToJID, PrivMsg) || ToJID <- ToJIDs]; true -> @@ -581,7 +577,7 @@ normal_state({route, From, ToNick, Err = jlib:make_error_reply(Packet, ?ERRT_FORBIDDEN(Lang, ErrText)), - ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, + ejabberd_router:route(jid:replace_resource(StateData#state.jid, ToNick), From, Err) end @@ -594,7 +590,7 @@ normal_state({route, From, ToNick, Err = jlib:make_error_reply(Packet, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)), - ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, + ejabberd_router:route(jid:replace_resource(StateData#state.jid, ToNick), From, Err); {false, _} -> @@ -602,7 +598,7 @@ normal_state({route, From, ToNick, <<"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, + ejabberd_router:route(jid:replace_resource(StateData#state.jid, ToNick), From, Err) end, @@ -611,8 +607,8 @@ normal_state({route, From, ToNick, normal_state({route, From, ToNick, #xmlel{name = <<"iq">>, attrs = Attrs} = Packet}, StateData) -> - Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), - StanzaId = xml:get_attr_s(<<"id">>, Attrs), + Lang = fxml:get_attr_s(<<"xml:lang">>, Attrs), + StanzaId = fxml:get_attr_s(<<"id">>, Attrs), case {(StateData#state.config)#config.allow_query_users, is_user_online_iq(StanzaId, From, StateData)} of @@ -626,17 +622,17 @@ normal_state({route, From, ToNick, Err = jlib:make_error_reply(Packet, ?ERRT_ITEM_NOT_FOUND(Lang, ErrText)), - ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, + ejabberd_router:route(jid:replace_resource(StateData#state.jid, ToNick), From, Err) end; ToJID -> {ok, #user{nick = FromNick}} = - (?DICT):find(jlib:jid_tolower(FromFull), + (?DICT):find(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, + ejabberd_router:route(jid:replace_resource(StateData#state.jid, FromNick), ToJID2, Packet2) end; @@ -650,7 +646,7 @@ normal_state({route, From, ToNick, Err = jlib:make_error_reply(Packet, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)), - ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, + ejabberd_router:route(jid:replace_resource(StateData#state.jid, ToNick), From, Err) end; @@ -662,7 +658,7 @@ normal_state({route, From, ToNick, "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, + ejabberd_router:route(jid:replace_resource(StateData#state.jid, ToNick), From, Err) end @@ -671,12 +667,6 @@ normal_state({route, From, ToNick, 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 = #xmlel{name = <<"message">>, @@ -711,12 +701,12 @@ handle_event({destroy, Reason}, _StateName, end}, StateData), ?INFO_MSG("Destroyed MUC room ~s with reason: ~p", - [jlib:jid_to_string(StateData#state.jid), Reason]), + [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)]), + [jid:to_string(StateData#state.jid)]), handle_event({destroy, none}, StateName, StateData); handle_event({set_affiliations, Affiliations}, StateName, StateData) -> @@ -725,19 +715,20 @@ handle_event({set_affiliations, Affiliations}, handle_event(_Event, StateName, StateData) -> {next_state, StateName, StateData}. -%%---------------------------------------------------------------------- -%% Func: handle_sync_event/4 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {reply, Reply, NextStateName, NextStateData} | -%% {reply, Reply, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} | -%% {stop, Reason, Reply, NewStateData} -%%---------------------------------------------------------------------- -handle_sync_event({get_disco_item, JID, Lang}, _From, StateName, StateData) -> - Reply = get_roomdesc_reply(JID, StateData, - get_roomdesc_tail(StateData, Lang)), +handle_sync_event({get_disco_item, Filter, JID, Lang}, _From, StateName, StateData) -> + Len = ?DICT:fold(fun(_, _, Acc) -> Acc + 1 end, 0, + StateData#state.users), + Reply = case (Filter == all) or (Filter == Len) or ((Filter /= 0) and (Len /= 0)) of + true -> + get_roomdesc_reply(JID, StateData, + get_roomdesc_tail(StateData, Lang)); + false -> + false + end, {reply, Reply, StateName, StateData}; +%% This clause is only for backwards compatibility +handle_sync_event({get_disco_item, JID, Lang}, From, StateName, StateData) -> + handle_sync_event({get_disco_item, any, JID, Lang}, From, StateName, StateData); handle_sync_event(get_config, _From, StateName, StateData) -> {reply, {ok, StateData#state.config}, StateName, @@ -762,12 +753,6 @@ handle_sync_event(_Event, _From, StateName, code_change(_OldVsn, StateName, StateData, _Extra) -> {ok, StateName, StateData}. -%%---------------------------------------------------------------------- -%% Func: handle_info/3 -%% Returns: {next_state, NextStateName, NextStateData} | -%% {next_state, NextStateName, NextStateData, Timeout} | -%% {stop, Reason, NewStateData} -%%---------------------------------------------------------------------- handle_info({process_user_presence, From}, normal_state = _StateName, StateData) -> RoomQueueEmpty = queue:is_empty(StateData#state.room_queue), RoomQueue = queue:in({presence, From}, StateData#state.room_queue), @@ -835,7 +820,7 @@ handle_info({captcha_failed, From}, normal_state, Err = jlib:make_error_reply(Packet, ?ERR_NOT_AUTHORIZED), ejabberd_router:route % TODO: s/Nick/""/ - (jlib:jid_replace_resource(StateData#state.jid, + (jid:replace_resource(StateData#state.jid, Nick), From, Err), StateData#state{robots = Robots}; @@ -847,11 +832,6 @@ handle_info(shutdown, _StateName, StateData) -> handle_info(_Info, StateName, StateData) -> {next_state, StateName, StateData}. -%%---------------------------------------------------------------------- -%% Func: terminate/3 -%% Purpose: Shutdown the fsm -%% Returns: any -%%---------------------------------------------------------------------- terminate(Reason, _StateName, StateData) -> ?INFO_MSG("Stopping MUC room ~s@~s", [StateData#state.room, StateData#state.host]), @@ -881,7 +861,7 @@ terminate(Reason, _StateName, StateData) -> Nick = Info#user.nick, case Reason of shutdown -> - ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, + ejabberd_router:route(jid:replace_resource(StateData#state.jid, Nick), Info#user.jid, Packet); _ -> ok @@ -904,7 +884,7 @@ route(Pid, From, ToNick, Packet) -> process_groupchat_message(From, #xmlel{name = <<"message">>, attrs = Attrs} = Packet, StateData) -> - Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), + Lang = fxml:get_attr_s(<<"xml:lang">>, Attrs), case is_user_online(From, StateData) orelse is_user_allowed_message_nonparticipant(From, StateData) of @@ -945,20 +925,33 @@ process_groupchat_message(From, end, case IsAllowed of true -> - send_multiple( - jlib:jid_replace_resource(StateData#state.jid, FromNick), - StateData#state.server_host, - StateData#state.users, - Packet), - 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}; + case + ejabberd_hooks:run_fold(muc_filter_message, + StateData#state.server_host, + Packet, + [StateData, + StateData#state.jid, + From, FromNick]) + of + drop -> + {next_state, normal_state, StateData}; + NewPacket1 -> + NewPacket = fxml:remove_subtags(NewPacket1, <<"nick">>, {<<"xmlns">>, ?NS_NICK}), + send_multiple(jid:replace_resource(StateData#state.jid, + FromNick), + StateData#state.server_host, + StateData#state.users, + NewPacket), + NewStateData2 = case has_body_or_subject(NewPacket) of + true -> + add_message_to_history(FromNick, From, + NewPacket, + NewStateData1); + false -> + NewStateData1 + end, + {next_state, normal_state, NewStateData2} + end; _ -> Err = case (StateData#state.config)#config.allow_change_subj @@ -1012,7 +1005,7 @@ is_user_allowed_message_nonparticipant(JID, %% @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), + case (?DICT):find(jid:tolower(From), StateData#state.users) of {ok, #user{nick = FromNick, role = Role}} -> @@ -1021,117 +1014,126 @@ get_participant_data(From, StateData) -> end. process_presence(From, Nick, - #xmlel{name = <<"presence">>, attrs = Attrs} = Packet, + #xmlel{name = <<"presence">>, attrs = Attrs0} = Packet0, 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 -> - 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). + Type0 = fxml:get_attr_s(<<"type">>, Attrs0), + IsOnline = is_user_online(From, StateData), + if Type0 == <<"">>; + IsOnline and ((Type0 == <<"unavailable">>) or (Type0 == <<"error">>)) -> + case ejabberd_hooks:run_fold(muc_filter_presence, + StateData#state.server_host, + Packet0, + [StateData, + StateData#state.jid, + From, Nick]) of + drop -> + {next_state, normal_state, StateData}; + #xmlel{attrs = Attrs} = Packet -> + Type = fxml:get_attr_s(<<"type">>, Attrs), + Lang = fxml:get_attr_s(<<"xml:lang">>, Attrs), + StateData1 = case Type of + <<"unavailable">> -> + 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, StateData) + end, + Reason = case fxml:get_subtag(NewPacket, + <<"status">>) + of + false -> <<"">>; + Status_el -> + fxml:get_tag_cdata(Status_el) + end, + remove_online_user(From, NewState, Reason); + <<"error">> -> + ErrorText = <<"It is not allowed to send error messages to the" + " room. The participant (~s) has sent an error " + "message (~s) and got kicked from the room">>, + expulse_participant(Packet, From, StateData, + translate:translate(Lang, + ErrorText)); + <<"">> -> + if not IsOnline -> + add_new_user(From, Nick, Packet, StateData); + 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(jid:replace_resource(StateData#state.jid, + Nick), + From, Err), + StateData; + {true, _, _} -> + Lang = fxml: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(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(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, StateData), + NewState + end + end + end, + close_room_if_temporary_and_empty(StateData1) + end; + true -> + {next_state, normal_state, StateData} + end. close_room_if_temporary_and_empty(StateData1) -> case not (StateData1#state.config)#config.persistent @@ -1140,14 +1142,14 @@ close_room_if_temporary_and_empty(StateData1) -> true -> ?INFO_MSG("Destroyed MUC room ~s because it's temporary " "and empty", - [jlib:jid_to_string(StateData1#state.jid)]), + [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), + LJID = jid:tolower(JID), (?DICT):is_key(LJID, StateData#state.users). %% Check if the user is occupant of the room, or at least is an admin or owner. @@ -1172,7 +1174,7 @@ is_user_online_iq(StanzaId, JID, StateData) when JID#jid.lresource == <<"">> -> try stanzaid_unpack(StanzaId) of {OriginalId, Resource} -> - JIDWithResource = jlib:jid_replace_resource(JID, + JIDWithResource = jid:replace_resource(JID, Resource), {is_user_online(JIDWithResource, StateData), OriginalId, JIDWithResource} @@ -1182,7 +1184,7 @@ is_user_online_iq(StanzaId, JID, StateData) handle_iq_vcard(FromFull, ToJID, StanzaId, NewId, Packet) -> - ToBareJID = jlib:jid_remove_resource(ToJID), + ToBareJID = jid:remove_resource(ToJID), IQ = jlib:iq_query_info(Packet), handle_iq_vcard2(FromFull, ToJID, ToBareJID, StanzaId, NewId, IQ, Packet). @@ -1264,7 +1266,7 @@ decide_fate_message(<<"error">>, Packet, From, Reason = io_lib:format("This participant is considered a ghost " "and is expulsed: ~s", - [jlib:jid_to_string(From)]), + [jid:to_string(From)]), {expulse_sender, Reason}; false -> continue_delivery end, @@ -1302,7 +1304,7 @@ get_error_condition(Packet) -> end. get_error_condition2(Packet) -> - #xmlel{children = EEls} = xml:get_subtag(Packet, + #xmlel{children = EEls} = fxml:get_subtag(Packet, <<"error">>), [Condition] = [Name || #xmlel{name = Name, @@ -1311,11 +1313,13 @@ get_error_condition2(Packet) -> <- EEls], {condition, Condition}. +make_reason(Packet, From, StateData, Reason1) -> + {ok, #user{nick = FromNick}} = (?DICT):find(jid:tolower(From), StateData#state.users), + Condition = get_error_condition(Packet), + iolist_to_binary(io_lib:format(Reason1, [FromNick, 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])), + Reason2 = make_reason(Packet, From, StateData, Reason1), NewState = add_user_presence_un(From, #xmlel{name = <<"presence">>, attrs = @@ -1328,14 +1332,14 @@ expulse_participant(Packet, From, StateData, Reason1) -> [{xmlcdata, Reason2}]}]}, StateData), - send_new_presence(From, NewState), + send_new_presence(From, NewState, StateData), remove_online_user(From, NewState). set_affiliation(JID, Affiliation, StateData) -> set_affiliation(JID, Affiliation, StateData, <<"">>). set_affiliation(JID, Affiliation, StateData, Reason) -> - LJID = jlib:jid_remove_resource(jlib:jid_tolower(JID)), + LJID = jid:remove_resource(jid:tolower(JID)), Affiliations = case Affiliation of none -> (?DICT):erase(LJID, StateData#state.affiliations); @@ -1354,11 +1358,11 @@ get_affiliation(JID, StateData) -> of allow -> owner; _ -> - LJID = jlib:jid_tolower(JID), + LJID = jid:tolower(JID), case (?DICT):find(LJID, StateData#state.affiliations) of {ok, Affiliation} -> Affiliation; _ -> - LJID1 = jlib:jid_remove_resource(LJID), + LJID1 = jid:remove_resource(LJID), case (?DICT):find(LJID1, StateData#state.affiliations) of {ok, Affiliation} -> Affiliation; @@ -1369,7 +1373,7 @@ get_affiliation(JID, StateData) -> of {ok, Affiliation} -> Affiliation; _ -> - LJID3 = jlib:jid_remove_resource(LJID2), + LJID3 = jid:remove_resource(LJID2), case (?DICT):find(LJID3, StateData#state.affiliations) of @@ -1397,7 +1401,7 @@ get_service_affiliation(JID, StateData) -> end. set_role(JID, Role, StateData) -> - LJID = jlib:jid_tolower(JID), + LJID = jid:tolower(JID), LJIDs = case LJID of {U, S, <<"">>} -> (?DICT):fold(fun (J, _, Js) -> @@ -1444,7 +1448,7 @@ set_role(JID, Role, StateData) -> StateData#state{users = Users, nicks = Nicks}. get_role(JID, StateData) -> - LJID = jlib:jid_tolower(JID), + LJID = jid:tolower(JID), case (?DICT):find(LJID, StateData#state.users) of {ok, #user{role = Role}} -> Role; _ -> none @@ -1494,7 +1498,7 @@ get_max_users_admin_threshold(StateData) -> 5). get_user_activity(JID, StateData) -> - case treap:lookup(jlib:jid_tolower(JID), + case treap:lookup(jid:tolower(JID), StateData#state.activity) of {ok, _P, A} -> A; @@ -1526,8 +1530,8 @@ store_user_activity(JID, UserActivity, StateData) -> fun(I) when is_number(I), I>=0 -> I end, 0) * 1000), - Key = jlib:jid_tolower(JID), - Now = now_to_usec(now()), + Key = jid:tolower(JID), + Now = p1_time_compat:system_time(micro_seconds), Activity1 = clean_treap(StateData#state.activity, {1, -Now}), Activity = case treap:lookup(Key, Activity1) of @@ -1609,7 +1613,7 @@ prepare_room_queue(StateData) -> end. add_online_user(JID, Nick, Role, StateData) -> - LJID = jlib:jid_tolower(JID), + LJID = jid:tolower(JID), Users = (?DICT):store(LJID, #user{jid = JID, nick = Nick, role = Role}, StateData#state.users), @@ -1629,7 +1633,7 @@ remove_online_user(JID, StateData) -> remove_online_user(JID, StateData, <<"">>). remove_online_user(JID, StateData, Reason) -> - LJID = jlib:jid_tolower(JID), + LJID = jid:tolower(JID), {ok, #user{nick = Nick}} = (?DICT):find(LJID, StateData#state.users), add_to_log(leave, {Nick, Reason}, StateData), @@ -1651,7 +1655,7 @@ filter_presence(#xmlel{name = <<"presence">>, case El of {xmlcdata, _} -> false; #xmlel{attrs = Attrs1} -> - XMLNS = xml:get_attr_s(<<"xmlns">>, + XMLNS = fxml:get_attr_s(<<"xmlns">>, Attrs1), NS_MUC = ?NS_MUC, Size = byte_size(NS_MUC), @@ -1678,7 +1682,7 @@ strip_status(#xmlel{name = <<"presence">>, children = FEls}. add_user_presence(JID, Presence, StateData) -> - LJID = jlib:jid_tolower(JID), + LJID = jid:tolower(JID), FPresence = filter_presence(Presence), Users = (?DICT):update(LJID, fun (#user{} = User) -> @@ -1688,7 +1692,7 @@ add_user_presence(JID, Presence, StateData) -> StateData#state{users = Users}. add_user_presence_un(JID, Presence, StateData) -> - LJID = jlib:jid_tolower(JID), + LJID = jid:tolower(JID), FPresence = filter_presence(Presence), Users = (?DICT):update(LJID, fun (#user{} = User) -> @@ -1702,8 +1706,8 @@ add_user_presence_un(JID, Presence, StateData) -> %% 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]; + {ok, [User]} -> [jid:make(User)]; + {ok, Users} -> [jid:make(LJID) || LJID <- Users]; error -> false end. @@ -1711,7 +1715,7 @@ find_jids_by_nick(Nick, StateData) -> %% highest-priority presence. Return jid record. find_jid_by_nick(Nick, StateData) -> case (?DICT):find(Nick, StateData#state.nicks) of - {ok, [User]} -> jlib:make_jid(User); + {ok, [User]} -> jid:make(User); {ok, [FirstUser | Users]} -> #user{last_presence = FirstPresence} = (?DICT):fetch(FirstUser, StateData#state.users), @@ -1729,7 +1733,7 @@ find_jid_by_nick(Nick, StateData) -> end end, {FirstUser, FirstPresence}, Users), - jlib:make_jid(LJID); + jid:make(LJID); error -> false end. @@ -1739,11 +1743,11 @@ higher_presence(Pres1, Pres2) -> Pri1 > Pri2. get_priority_from_presence(PresencePacket) -> - case xml:get_subtag(PresencePacket, <<"priority">>) of + case fxml:get_subtag(PresencePacket, <<"priority">>) of false -> 0; SubEl -> case catch - jlib:binary_to_integer(xml:get_tag_cdata(SubEl)) + jlib:binary_to_integer(fxml:get_tag_cdata(SubEl)) of P when is_integer(P) -> P; _ -> 0 @@ -1759,7 +1763,7 @@ find_nick_by_jid(Jid, StateData) -> Nick. is_nick_change(JID, Nick, StateData) -> - LJID = jlib:jid_tolower(JID), + LJID = jid:tolower(JID), case Nick of <<"">> -> false; _ -> @@ -1770,14 +1774,14 @@ is_nick_change(JID, Nick, StateData) -> 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)). + (UserOfNick /= false andalso + jid:remove_resource(jid:tolower(UserOfNick)) + /= jid:remove_resource(jid:tolower(User))). add_new_user(From, Nick, #xmlel{attrs = Attrs, children = Els} = Packet, StateData) -> - Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), + Lang = fxml:get_attr_s(<<"xml:lang">>, Attrs), MaxUsers = get_max_users(StateData), MaxAdminUsers = MaxUsers + get_max_users_admin_threshold(StateData), @@ -1807,7 +1811,7 @@ add_new_user(From, Nick, Err = jlib:make_error_reply(Packet, ?ERR_SERVICE_UNAVAILABLE), ejabberd_router:route % TODO: s/Nick/""/ - (jlib:jid_replace_resource(StateData#state.jid, Nick), + (jid:replace_resource(StateData#state.jid, Nick), From, Err), StateData; {_, _, _, none} -> @@ -1824,14 +1828,14 @@ add_new_user(From, Nick, ErrText) end), ejabberd_router:route % TODO: s/Nick/""/ - (jlib:jid_replace_resource(StateData#state.jid, Nick), + (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, + ejabberd_router:route(jid:replace_resource(StateData#state.jid, Nick), From, Err), StateData; @@ -1839,7 +1843,7 @@ add_new_user(From, Nick, 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, + ejabberd_router:route(jid:replace_resource(StateData#state.jid, Nick), From, Err), StateData; @@ -1851,37 +1855,12 @@ add_new_user(From, Nick, 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), + send_initial_presence(From, NewState, StateData), Shift = count_stanza_shift(Nick, Els, NewState), case send_history(From, Shift, NewState) of true -> ok; - _ -> send_subject(From, Lang, StateData) + _ -> send_subject(From, StateData) end, case NewState#state.just_created of true -> NewState#state{just_created = false}; @@ -1895,14 +1874,14 @@ add_new_user(From, Nick, ?ERRT_NOT_AUTHORIZED(Lang, ErrText)), ejabberd_router:route % TODO: s/Nick/""/ - (jlib:jid_replace_resource(StateData#state.jid, + (jid:replace_resource(StateData#state.jid, Nick), From, Err), StateData; captcha_required -> - SID = xml:get_attr_s(<<"id">>, Attrs), + SID = fxml:get_attr_s(<<"id">>, Attrs), RoomJID = StateData#state.jid, - To = jlib:jid_replace_resource(RoomJID, Nick), + To = jid:replace_resource(RoomJID, Nick), Limiter = {From#jid.luser, From#jid.lserver}, case ejabberd_captcha:create_captcha(SID, RoomJID, To, Lang, Limiter, From) @@ -1921,7 +1900,7 @@ add_new_user(From, Nick, ?ERRT_RESOURCE_CONSTRAINT(Lang, ErrText)), ejabberd_router:route % TODO: s/Nick/""/ - (jlib:jid_replace_resource(StateData#state.jid, + (jid:replace_resource(StateData#state.jid, Nick), From, Err), StateData; @@ -1931,7 +1910,7 @@ add_new_user(From, Nick, ?ERRT_INTERNAL_SERVER_ERROR(Lang, ErrText)), ejabberd_router:route % TODO: s/Nick/""/ - (jlib:jid_replace_resource(StateData#state.jid, + (jid:replace_resource(StateData#state.jid, Nick), From, Err), StateData @@ -1942,7 +1921,7 @@ add_new_user(From, Nick, ?ERRT_NOT_AUTHORIZED(Lang, ErrText)), ejabberd_router:route % TODO: s/Nick/""/ - (jlib:jid_replace_resource(StateData#state.jid, + (jid:replace_resource(StateData#state.jid, Nick), From, Err), StateData @@ -2000,11 +1979,11 @@ check_captcha(Affiliation, From, StateData) -> extract_password([]) -> false; extract_password([#xmlel{attrs = Attrs} = El | Els]) -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of + case fxml:get_attr_s(<<"xmlns">>, Attrs) of ?NS_MUC -> - case xml:get_subtag(El, <<"password">>) of + case fxml:get_subtag(El, <<"password">>) of false -> false; - SubEl -> xml:get_tag_cdata(SubEl) + SubEl -> fxml:get_tag_cdata(SubEl) end; _ -> extract_password(Els) end; @@ -2023,9 +2002,8 @@ count_stanza_shift(Nick, Els, StateData) -> Shift1 = case Seconds of false -> 0; _ -> - Sec = - calendar:datetime_to_gregorian_seconds(calendar:now_to_universal_time(now())) - - Seconds, + Sec = calendar:datetime_to_gregorian_seconds(calendar:universal_time()) + - Seconds, count_seconds_shift(Sec, HL) end, MaxStanzas = extract_history(Els, <<"maxstanzas">>), @@ -2079,9 +2057,9 @@ calc_shift(MaxSize, Size, Shift, [S | TSizes]) -> extract_history([], _Type) -> false; extract_history([#xmlel{attrs = Attrs} = El | Els], Type) -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of + case fxml:get_attr_s(<<"xmlns">>, Attrs) of ?NS_MUC -> - AttrVal = xml:get_path_s(El, + AttrVal = fxml:get_path_s(El, [{elem, <<"history">>}, {attr, Type}]), case Type of <<"since">> -> @@ -2101,11 +2079,31 @@ extract_history([#xmlel{attrs = Attrs} = El | Els], extract_history([_ | Els], Type) -> extract_history(Els, Type). -send_update_presence(JID, StateData) -> - send_update_presence(JID, <<"">>, StateData). +is_room_overcrowded(StateData) -> + MaxUsersPresence = gen_mod:get_module_opt(StateData#state.server_host, + mod_muc, max_users_presence, + fun(MUP) when is_integer(MUP) -> MUP end, + ?DEFAULT_MAX_USERS_PRESENCE), + (?DICT):size(StateData#state.users) > MaxUsersPresence. + +presence_broadcast_allowed(JID, StateData) -> + Role = get_role(JID, StateData), + lists:member(Role, (StateData#state.config)#config.presence_broadcast). + +send_initial_presence(NJID, StateData, OldStateData) -> + send_new_presence1(NJID, <<"">>, true, StateData, OldStateData). -send_update_presence(JID, Reason, StateData) -> - LJID = jlib:jid_tolower(JID), +send_update_presence(JID, StateData, OldStateData) -> + send_update_presence(JID, <<"">>, StateData, OldStateData). + +send_update_presence(JID, Reason, StateData, OldStateData) -> + case is_room_overcrowded(StateData) of + true -> ok; + false -> send_update_presence1(JID, Reason, StateData, OldStateData) + end. + +send_update_presence1(JID, Reason, StateData, OldStateData) -> + LJID = jid:tolower(JID), LJIDs = case LJID of {U, S, <<"">>} -> (?DICT):fold(fun (J, _, Js) -> @@ -2122,34 +2120,67 @@ send_update_presence(JID, Reason, StateData) -> end end, lists:foreach(fun (J) -> - send_new_presence(J, Reason, StateData) + send_new_presence1(J, Reason, false, StateData, + OldStateData) end, LJIDs). -send_new_presence(NJID, StateData) -> - send_new_presence(NJID, <<"">>, StateData). +send_new_presence(NJID, StateData, OldStateData) -> + send_new_presence(NJID, <<"">>, false, StateData, OldStateData). -send_new_presence(NJID, Reason, StateData) -> - #user{nick = Nick} = - (?DICT):fetch(jlib:jid_tolower(NJID), - StateData#state.users), +send_new_presence(NJID, Reason, StateData, OldStateData) -> + send_new_presence(NJID, Reason, false, StateData, OldStateData). + +send_new_presence(NJID, Reason, IsInitialPresence, StateData, OldStateData) -> + case is_room_overcrowded(StateData) of + true -> ok; + false -> send_new_presence1(NJID, Reason, IsInitialPresence, StateData, + OldStateData) + end. + +send_new_presence1(NJID, Reason, IsInitialPresence, StateData, OldStateData) -> + LNJID = jid:tolower(NJID), + #user{nick = Nick} = (?DICT):fetch(LNJID, 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), + #user{jid = RealJID, role = Role0, + last_presence = Presence0} = UserInfo} = + (?DICT):find(jid:tolower(LJID), StateData#state.users), + {Role1, Presence1} = + case presence_broadcast_allowed(NJID, StateData) of + true -> {Role0, Presence0}; + false -> + {none, + #xmlel{name = <<"presence">>, + attrs = [{<<"type">>, <<"unavailable">>}], + children = []} + } + end, Affiliation = get_affiliation(LJID, StateData), SAffiliation = affiliation_to_list(Affiliation), - SRole = role_to_list(Role), - lists:foreach(fun ({_LJID, Info}) -> + UserList = + case not (presence_broadcast_allowed(NJID, StateData) orelse + presence_broadcast_allowed(NJID, OldStateData)) of + true -> + [{LNJID, UserInfo}]; + false -> + (?DICT):to_list(StateData#state.users) + end, + lists:foreach(fun ({LUJID, Info}) -> + {Role, Presence} = + if + LNJID == LUJID -> {Role0, Presence0}; + true -> {Role1, Presence1} + end, + SRole = role_to_list(Role), ItemAttrs = case Info#user.role == moderator orelse (StateData#state.config)#config.anonymous == false of true -> [{<<"jid">>, - jlib:jid_to_string(RealJID)}, + jid:to_string(RealJID)}, {<<"affiliation">>, SAffiliation}, {<<"role">>, SRole}]; _ -> @@ -2164,37 +2195,9 @@ send_new_presence(NJID, Reason, StateData) -> 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, + StatusEls = status_els(IsInitialPresence, NJID, Info, + StateData), + Packet = fxml:append_subtags(Presence, [#xmlel{name = <<"x">>, attrs = [{<<"xmlns">>, @@ -2208,25 +2211,35 @@ send_new_presence(NJID, Reason, StateData) -> children = ItemEls} - | Status3]}]), - ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, + | StatusEls]}]), + ejabberd_router:route(jid:replace_resource(StateData#state.jid, Nick), Info#user.jid, Packet) end, - (?DICT):to_list(StateData#state.users)). + UserList). send_existing_presences(ToJID, StateData) -> - LToJID = jlib:jid_tolower(ToJID), + case is_room_overcrowded(StateData) of + true -> ok; + false -> send_existing_presences1(ToJID, StateData) + end. + +send_existing_presences1(ToJID, StateData) -> + LToJID = jid:tolower(ToJID), {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), + (?DICT):fetch(jid:tolower(LJID), StateData#state.users), - case RealToJID of - FromJID -> ok; + PresenceBroadcast = + lists:member( + FromRole, (StateData#state.config)#config.presence_broadcast), + case {RealToJID, PresenceBroadcast} of + {FromJID, _} -> ok; + {_, false} -> ok; _ -> FromAffiliation = get_affiliation(LJID, StateData), @@ -2236,7 +2249,7 @@ send_existing_presences(ToJID, StateData) -> of true -> [{<<"jid">>, - jlib:jid_to_string(FromJID)}, + jid:to_string(FromJID)}, {<<"affiliation">>, affiliation_to_list(FromAffiliation)}, {<<"role">>, @@ -2247,7 +2260,7 @@ send_existing_presences(ToJID, StateData) -> {<<"role">>, role_to_list(FromRole)}] end, - Packet = xml:append_subtags(Presence, + Packet = fxml:append_subtags(Presence, [#xmlel{name = <<"x">>, attrs = @@ -2263,18 +2276,15 @@ send_existing_presences(ToJID, StateData) -> children = []}]}]), - ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, + ejabberd_router:route(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. - change_nick(JID, Nick, StateData) -> - LJID = jlib:jid_tolower(JID), + LJID = jid:tolower(JID), {ok, #user{nick = OldNick}} = (?DICT):find(LJID, StateData#state.users), Users = (?DICT):update(LJID, @@ -2302,8 +2312,12 @@ change_nick(JID, Nick, StateData) -> end, NewStateData = StateData#state{users = Users, nicks = Nicks}, - send_nick_changing(JID, OldNick, NewStateData, - SendOldUnavailable, SendNewAvailable), + case presence_broadcast_allowed(JID, NewStateData) of + true -> + send_nick_changing(JID, OldNick, NewStateData, + SendOldUnavailable, SendNewAvailable); + false -> ok + end, add_to_log(nickchange, {OldNick, Nick}, StateData), NewStateData. @@ -2312,7 +2326,7 @@ send_nick_changing(JID, OldNick, StateData, {ok, #user{jid = RealJID, nick = Nick, role = Role, last_presence = Presence}} = - (?DICT):find(jlib:jid_tolower(JID), + (?DICT):find(jid:tolower(JID), StateData#state.users), Affiliation = get_affiliation(JID, StateData), SAffiliation = affiliation_to_list(Affiliation), @@ -2324,7 +2338,7 @@ send_nick_changing(JID, OldNick, StateData, of true -> [{<<"jid">>, - jlib:jid_to_string(RealJID)}, + jid:to_string(RealJID)}, {<<"affiliation">>, SAffiliation}, {<<"role">>, SRole}, {<<"nick">>, Nick}]; @@ -2339,7 +2353,7 @@ send_nick_changing(JID, OldNick, StateData, of true -> [{<<"jid">>, - jlib:jid_to_string(RealJID)}, + jid:to_string(RealJID)}, {<<"affiliation">>, SAffiliation}, {<<"role">>, SRole}]; _ -> @@ -2377,7 +2391,7 @@ send_nick_changing(JID, OldNick, StateData, <<"303">>}], children = []}|Status110]}]}, - Packet2 = xml:append_subtags(Presence, + Packet2 = fxml:append_subtags(Presence, [#xmlel{name = <<"x">>, attrs = [{<<"xmlns">>, @@ -2393,13 +2407,13 @@ send_nick_changing(JID, OldNick, StateData, = []}|Status110]}]), if SendOldUnavailable -> - ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, + ejabberd_router:route(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, + ejabberd_router:route(jid:replace_resource(StateData#state.jid, Nick), Info#user.jid, Packet2); true -> ok @@ -2407,6 +2421,40 @@ send_nick_changing(JID, OldNick, StateData, end, (?DICT):to_list(StateData#state.users)). +status_els(IsInitialPresence, JID, #user{jid = JID}, StateData) -> + Status = case IsInitialPresence of + true -> + S1 = case StateData#state.just_created of + true -> + [#xmlel{name = <<"status">>, + attrs = [{<<"code">>, <<"201">>}], + children = []}]; + false -> [] + end, + S2 = case (StateData#state.config)#config.anonymous of + true -> S1; + false -> + [#xmlel{name = <<"status">>, + attrs = [{<<"code">>, <<"100">>}], + children = []} | S1] + end, + S3 = case (StateData#state.config)#config.logging of + true -> + [#xmlel{name = <<"status">>, + attrs = [{<<"code">>, <<"170">>}], + children = []} | S2]; + false -> S2 + end, + S3; + false -> [] + end, + [#xmlel{name = <<"status">>, + attrs = + [{<<"code">>, + <<"110">>}], + children = []} | Status]; +status_els(_IsInitialPresence, _JID, _Info, _StateData) -> []. + lqueue_new(Max) -> #lqueue{queue = queue:new(), len = 0, max = Max}. @@ -2431,28 +2479,28 @@ lqueue_to_list(#lqueue{queue = Q1}) -> add_message_to_history(FromNick, FromJID, Packet, StateData) -> - HaveSubject = case xml:get_subtag(Packet, <<"subject">>) + HaveSubject = case fxml:get_subtag(Packet, <<"subject">>) of false -> false; _ -> true end, - TimeStamp = now(), + TimeStamp = p1_time_compat:timestamp(), AddrPacket = case (StateData#state.config)#config.anonymous of true -> Packet; false -> Address = #xmlel{name = <<"address">>, attrs = [{<<"type">>, <<"ofrom">>}, {<<"jid">>, - jlib:jid_to_string(FromJID)}], + jid:to_string(FromJID)}], children = []}, Addresses = #xmlel{name = <<"addresses">>, attrs = [{<<"xmlns">>, ?NS_ADDRESS}], children = [Address]}, - xml:append_subtags(Packet, [Addresses]) + fxml:append_subtags(Packet, [Addresses]) end, TSPacket = jlib:add_delay_info(AddrPacket, StateData#state.jid, TimeStamp), SPacket = - jlib:replace_from_to(jlib:jid_replace_resource(StateData#state.jid, + jlib:replace_from_to(jid:replace_resource(StateData#state.jid, FromNick), StateData#state.jid, TSPacket), Size = element_size(SPacket), @@ -2466,7 +2514,7 @@ send_history(JID, Shift, StateData) -> lists:foldl(fun ({Nick, Packet, HaveSubject, _TimeStamp, _Size}, B) -> - ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, + ejabberd_router:route(jid:replace_resource(StateData#state.jid, Nick), JID, Packet), B or HaveSubject @@ -2475,30 +2523,21 @@ send_history(JID, Shift, StateData) -> 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 = #xmlel{name = <<"message">>, - attrs = [{<<"type">>, <<"groupchat">>}], - children = - [#xmlel{name = <<"subject">>, attrs = [], - children = [{xmlcdata, Subject}]}, - #xmlel{name = <<"body">>, attrs = [], - children = - [{xmlcdata, - <<Nick/binary, - (translate:translate(Lang, - <<" has set the subject to: ">>))/binary, - Subject/binary>>}]}]}, - ejabberd_router:route(StateData#state.jid, JID, Packet) - end. +send_subject(_JID, #state{subject_author = <<"">>}) -> ok; +send_subject(JID, #state{subject_author = Nick} = StateData) -> + Subject = StateData#state.subject, + Packet = #xmlel{name = <<"message">>, + attrs = [{<<"type">>, <<"groupchat">>}], + children = + [#xmlel{name = <<"subject">>, attrs = [], + children = [{xmlcdata, Subject}]}]}, + ejabberd_router:route(jid:replace_resource(StateData#state.jid, Nick), JID, + Packet). check_subject(Packet) -> - case xml:get_subtag(Packet, <<"subject">>) of + case fxml:get_subtag(Packet, <<"subject">>) of false -> false; - SubjEl -> xml:get_tag_cdata(SubjEl) + SubjEl -> fxml:get_tag_cdata(SubjEl) end. can_change_subject(Role, StateData) -> @@ -2515,14 +2554,14 @@ process_iq_admin(From, set, Lang, SubEl, StateData) -> #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 + case fxml: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 + case fxml:get_tag_attr(<<"role">>, Item) of false -> - case xml:get_tag_attr(<<"affiliation">>, Item) of + case fxml:get_tag_attr(<<"affiliation">>, Item) of false -> {error, ?ERR_BAD_REQUEST}; {value, StrAffiliation} -> case catch list_to_affiliation(StrAffiliation) of @@ -2530,7 +2569,8 @@ process_iq_admin(From, get, Lang, SubEl, StateData) -> SAffiliation -> if (FAffiliation == owner) or (FAffiliation == admin) or - ((FAffiliation == member) and (SAffiliation == member)) -> + ((FAffiliation == member) and not + (StateData#state.config)#config.anonymous) -> Items = items_with_affiliation(SAffiliation, StateData), {result, Items, StateData}; @@ -2567,7 +2607,7 @@ items_with_affiliation(SAffiliation, StateData) -> attrs = [{<<"affiliation">>, affiliation_to_list(Affiliation)}, - {<<"jid">>, jlib:jid_to_string(JID)}], + {<<"jid">>, jid:to_string(JID)}], children = [#xmlel{name = <<"reason">>, attrs = [], children = [{xmlcdata, Reason}]}]}; @@ -2576,7 +2616,7 @@ items_with_affiliation(SAffiliation, StateData) -> attrs = [{<<"affiliation">>, affiliation_to_list(Affiliation)}, - {<<"jid">>, jlib:jid_to_string(JID)}], + {<<"jid">>, jid:to_string(JID)}], children = []} end, search_affiliation(SAffiliation, StateData)). @@ -2589,7 +2629,7 @@ user_to_item(#user{role = Role, nick = Nick, jid = JID}, [{<<"role">>, role_to_list(Role)}, {<<"affiliation">>, affiliation_to_list(Affiliation)}, {<<"nick">>, Nick}, - {<<"jid">>, jlib:jid_to_string(JID)}], + {<<"jid">>, jid:to_string(JID)}], children = []}. search_role(Role, StateData) -> @@ -2615,8 +2655,8 @@ process_admin_items_set(UJID, Items, Lang, StateData) -> {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]), + [jid:to_string(UJID), + jid:to_string(StateData#state.jid), Res]), NSD = lists:foldl(process_item_change(UJID), StateData, lists:flatten(Res)), case (NSD#state.config)#config.persistent of @@ -2661,7 +2701,7 @@ process_item_change(E, SD, UJID) -> set_role(JID, none, SD1); _ -> SD1 = set_affiliation(JID, none, SD), - send_update_presence(JID, SD1), + send_update_presence(JID, SD1, SD), SD1 end; {JID, affiliation, outcast, Reason} -> @@ -2679,21 +2719,21 @@ process_item_change(E, SD, UJID) -> 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), + send_update_presence(JID, Reason, SD2, SD), SD2; {JID, affiliation, member, Reason} -> SD1 = set_affiliation(JID, member, SD, Reason), SD2 = set_role(JID, participant, SD1), - send_update_presence(JID, Reason, SD2), + send_update_presence(JID, Reason, SD2, SD), SD2; {JID, role, Role, Reason} -> SD1 = set_role(JID, Role, SD), catch - send_new_presence(JID, Reason, SD1), + send_new_presence(JID, Reason, SD1, SD), SD1; {JID, affiliation, A, _Reason} -> SD1 = set_affiliation(JID, A, SD), - send_update_presence(JID, SD1), + send_update_presence(JID, SD1, SD), SD1 end of @@ -2714,9 +2754,9 @@ find_changed_items(UJID, UAffiliation, URole, [#xmlel{name = <<"item">>, attrs = Attrs} = Item | Items], Lang, StateData, Res) -> - TJID = case xml:get_attr(<<"jid">>, Attrs) of + TJID = case fxml:get_attr(<<"jid">>, Attrs) of {value, S} -> - case jlib:string_to_jid(S) of + case jid:from_string(S) of error -> ErrText = iolist_to_binary( io_lib:format(translate:translate( @@ -2727,7 +2767,7 @@ find_changed_items(UJID, UAffiliation, URole, J -> {value, [J]} end; _ -> - case xml:get_attr(<<"nick">>, Attrs) of + case fxml:get_attr(<<"nick">>, Attrs) of {value, N} -> case find_jids_by_nick(N, StateData) of false -> @@ -2747,9 +2787,9 @@ find_changed_items(UJID, UAffiliation, URole, {value, [JID | _] = JIDs} -> TAffiliation = get_affiliation(JID, StateData), TRole = get_role(JID, StateData), - case xml:get_attr(<<"role">>, Attrs) of + case fxml:get_attr(<<"role">>, Attrs) of false -> - case xml:get_attr(<<"affiliation">>, Attrs) of + case fxml:get_attr(<<"affiliation">>, Attrs) of false -> {error, ?ERR_BAD_REQUEST}; {value, StrAffiliation} -> case catch list_to_affiliation(StrAffiliation) of @@ -2777,9 +2817,9 @@ find_changed_items(UJID, UAffiliation, URole, StateData) of [{OJID, _}] -> - jlib:jid_remove_resource(OJID) + jid:remove_resource(OJID) /= - jlib:jid_tolower(jlib:jid_remove_resource(UJID)); + jid:tolower(jid:remove_resource(UJID)); _ -> true end; _ -> false @@ -2790,10 +2830,10 @@ find_changed_items(UJID, UAffiliation, URole, Items, Lang, StateData, Res); true -> - Reason = xml:get_path_s(Item, + Reason = fxml:get_path_s(Item, [{elem, <<"reason">>}, cdata]), - MoreRes = [{jlib:jid_remove_resource(Jidx), + MoreRes = [{jid:remove_resource(Jidx), affiliation, SAffiliation, Reason} || Jidx <- JIDs], find_changed_items(UJID, UAffiliation, URole, @@ -2825,9 +2865,9 @@ find_changed_items(UJID, UAffiliation, URole, StateData) of [{OJID, _}] -> - jlib:jid_remove_resource(OJID) + jid:remove_resource(OJID) /= - jlib:jid_tolower(jlib:jid_remove_resource(UJID)); + jid:tolower(jid:remove_resource(UJID)); _ -> true end; _ -> false @@ -2837,7 +2877,7 @@ find_changed_items(UJID, UAffiliation, URole, find_changed_items(UJID, UAffiliation, URole, Items, Lang, StateData, Res); true -> - Reason = xml:get_path_s(Item, + Reason = fxml:get_path_s(Item, [{elem, <<"reason">>}, cdata]), MoreRes = [{Jidx, role, SRole, Reason} @@ -2986,7 +3026,7 @@ send_kickban_presence(UJID, JID, Reason, Code, StateData) -> send_kickban_presence(UJID, JID, Reason, Code, NewAffiliation, StateData) -> - LJID = jlib:jid_tolower(JID), + LJID = jid:tolower(JID), LJIDs = case LJID of {U, S, <<"">>} -> (?DICT):fold(fun (J, _, Js) -> @@ -3015,14 +3055,14 @@ send_kickban_presence(UJID, JID, Reason, Code, NewAffiliation, send_kickban_presence1(MJID, UJID, Reason, Code, Affiliation, StateData) -> {ok, #user{jid = RealJID, nick = Nick}} = - (?DICT):find(jlib:jid_tolower(UJID), + (?DICT):find(jid:tolower(UJID), StateData#state.users), SAffiliation = affiliation_to_list(Affiliation), - BannedJIDString = jlib:jid_to_string(RealJID), + BannedJIDString = jid:to_string(RealJID), case MJID /= <<"">> of true -> {ok, #user{nick = ActorNick}} = - (?DICT):find(jlib:jid_tolower(MJID), + (?DICT):find(jid:tolower(MJID), StateData#state.users); false -> ActorNick = <<"">> @@ -3075,7 +3115,7 @@ send_kickban_presence1(MJID, UJID, Reason, Code, Affiliation, Code}], children = []}]}]}, - ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, + ejabberd_router:route(jid:replace_resource(StateData#state.jid, Nick), Info#user.jid, Packet) end, @@ -3089,10 +3129,10 @@ process_iq_owner(From, set, Lang, SubEl, StateData) -> case FAffiliation of owner -> #xmlel{children = Els} = SubEl, - case xml:remove_cdata(Els) of + case fxml:remove_cdata(Els) of [#xmlel{name = <<"x">>} = XEl] -> - case {xml:get_tag_attr_s(<<"xmlns">>, XEl), - xml:get_tag_attr_s(<<"type">>, XEl)} + case {fxml:get_tag_attr_s(<<"xmlns">>, XEl), + fxml:get_tag_attr_s(<<"type">>, XEl)} of {?NS_XDATA, <<"cancel">>} -> {result, [], StateData}; {?NS_XDATA, <<"submit">>} -> @@ -3110,8 +3150,8 @@ process_iq_owner(From, set, Lang, SubEl, StateData) -> 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)]), + [jid:to_string(StateData#state.jid), + jid:to_string(From)]), add_to_log(room_existence, destroyed, StateData), destroy_room(SubEl1, StateData); Items -> @@ -3126,10 +3166,10 @@ process_iq_owner(From, get, Lang, SubEl, StateData) -> case FAffiliation of owner -> #xmlel{children = Els} = SubEl, - case xml:remove_cdata(Els) of + case fxml:remove_cdata(Els) of [] -> get_config(Lang, StateData, From); [Item] -> - case xml:get_tag_attr(<<"affiliation">>, Item) of + case fxml:get_tag_attr(<<"affiliation">>, Item) of false -> {error, ?ERR_BAD_REQUEST}; {value, StrAffiliation} -> case catch list_to_affiliation(StrAffiliation) of @@ -3276,7 +3316,7 @@ is_password_settings_correct(XEl, StateData) -> {<<"var">>, Var}], children = [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, jlib:jid_to_string(JID)}]} + children = [{xmlcdata, jid:to_string(JID)}]} || JID <- JIDList]}). get_default_room_maxusers(RoomState) -> @@ -3312,7 +3352,7 @@ get_config(Lang, StateData, From) -> translate:translate( Lang, <<"Configuration of room ~s">>), - [jlib:jid_to_string(StateData#state.jid)]))}]}, + [jid:to_string(StateData#state.jid)]))}]}, #xmlel{name = <<"field">>, attrs = [{<<"type">>, <<"hidden">>}, @@ -3429,6 +3469,53 @@ get_config(Lang, StateData, From) -> children = [{xmlcdata, <<"anyone">>}]}]}]}, + #xmlel{name = <<"field">>, + attrs = + [{<<"type">>, <<"list-multi">>}, + {<<"label">>, + translate:translate(Lang, + <<"Roles for which Presence is Broadcasted">>)}, + {<<"var">>, <<"muc#roomconfig_presencebroadcast">>}], + children = + lists:map( + fun(Role) -> + #xmlel{name = <<"value">>, attrs = [], + children = + [{xmlcdata, + atom_to_binary(Role, utf8)}]} + end, Config#config.presence_broadcast + ) ++ + [#xmlel{name = <<"option">>, + attrs = + [{<<"label">>, + translate:translate(Lang, + <<"Moderator">>)}], + children = + [#xmlel{name = <<"value">>, attrs = [], + children = + [{xmlcdata, + <<"moderator">>}]}]}, + #xmlel{name = <<"option">>, + attrs = + [{<<"label">>, + translate:translate(Lang, + <<"Participant">>)}], + children = + [#xmlel{name = <<"value">>, attrs = [], + children = + [{xmlcdata, + <<"participant">>}]}]}, + #xmlel{name = <<"option">>, + attrs = + [{<<"label">>, + translate:translate(Lang, + <<"Visitor">>)}], + children = + [#xmlel{name = <<"value">>, attrs = [], + children = + [{xmlcdata, + <<"visitor">>}]}]} + ]}, ?BOOLXFIELD(<<"Make room members-only">>, <<"muc#roomconfig_membersonly">>, (Config#config.members_only)), @@ -3519,6 +3606,13 @@ get_config(Lang, StateData, From) -> <<"captcha_protected">>, (Config#config.captcha_protected))]; false -> [] + end ++ + case gen_mod:is_loaded(StateData#state.server_host, mod_mam) of + true -> + [?BOOLXFIELD(<<"Enable message archiving">>, + <<"muc#roomconfig_mam">>, + (Config#config.mam))]; + false -> [] end ++ [?JIDMULTIXFIELD(<<"Exclude Jabber IDs from CAPTCHA challenge">>, @@ -3695,6 +3789,28 @@ set_xoption([{<<"muc#roomconfig_roomsecret">>, [Val]} set_xoption([{<<"anonymous">>, [Val]} | Opts], Config) -> ?SET_BOOL_XOPT(anonymous, Val); +set_xoption([{<<"muc#roomconfig_presencebroadcast">>, Vals} | Opts], + Config) -> + Roles = + lists:foldl( + fun(_S, error) -> error; + (S, {M, P, V}) -> + case S of + <<"moderator">> -> {true, P, V}; + <<"participant">> -> {M, true, V}; + <<"visitor">> -> {M, P, true}; + _ -> error + end + end, {false, false, false}, Vals), + case Roles of + error -> {error, ?ERR_BAD_REQUEST}; + {M, P, V} -> + Res = + if M -> [moderator]; true -> [] end ++ + if P -> [participant]; true -> [] end ++ + if V -> [visitor]; true -> [] end, + set_xoption(Opts, Config#config{presence_broadcast = Res}) + end; set_xoption([{<<"muc#roomconfig_allowvoicerequests">>, [Val]} | Opts], @@ -3728,11 +3844,13 @@ set_xoption([{<<"muc#roomconfig_enablelogging">>, [Val]} | Opts], Config) -> ?SET_BOOL_XOPT(logging, Val); +set_xoption([{<<"muc#roomconfig_mam">>, [Val]}|Opts], Config) -> + ?SET_BOOL_XOPT(mam, Val); set_xoption([{<<"muc#roomconfig_captcha_whitelist">>, Vals} | Opts], Config) -> - JIDs = [jlib:string_to_jid(Val) || Val <- Vals], + JIDs = [jid:from_string(Val) || Val <- Vals], ?SET_JIDMULTI_XOPT(captcha_whitelist, JIDs); set_xoption([{<<"FORM_TYPE">>, _} | Opts], Config) -> set_xoption(Opts, Config); @@ -3740,6 +3858,7 @@ set_xoption([_ | _Opts], _Config) -> {error, ?ERR_BAD_REQUEST}. change_config(Config, StateData) -> + send_config_change_info(Config, StateData), NSD = StateData#state{config = Config}, case {(StateData#state.config)#config.persistent, Config#config.persistent} @@ -3760,6 +3879,39 @@ change_config(Config, StateData) -> _ -> {result, [], NSD} end. +send_config_change_info(Config, #state{config = Config}) -> ok; +send_config_change_info(New, #state{config = Old} = StateData) -> + Codes = case {Old#config.logging, New#config.logging} of + {false, true} -> [<<"170">>]; + {true, false} -> [<<"171">>]; + _ -> [] + end + ++ + case {Old#config.anonymous, New#config.anonymous} of + {true, false} -> [<<"172">>]; + {false, true} -> [<<"173">>]; + _ -> [] + end + ++ + case Old#config{anonymous = New#config.anonymous, + logging = New#config.logging} of + New -> []; + _ -> [<<"104">>] + end, + StatusEls = [#xmlel{name = <<"status">>, + attrs = [{<<"code">>, Code}], + children = []} || Code <- Codes], + Message = #xmlel{name = <<"message">>, + attrs = [{<<"type">>, <<"groupchat">>}, + {<<"id">>, randoms:get_string()}], + children = [#xmlel{name = <<"x">>, + attrs = [{<<"xmlns">>, ?NS_MUC_USER}], + children = StatusEls}]}, + send_multiple(StateData#state.jid, + StateData#state.server_host, + StateData#state.users, + Message). + remove_nonmembers(StateData) -> lists:foldl(fun ({_LJID, #user{jid = JID}}, SD) -> Affiliation = get_affiliation(JID, SD), @@ -3852,10 +4004,17 @@ set_opts([{Opt, Val} | Opts], StateData) -> StateData#state{config = (StateData#state.config)#config{anonymous = Val}}; + presence_broadcast -> + StateData#state{config = + (StateData#state.config)#config{presence_broadcast = + Val}}; logging -> StateData#state{config = (StateData#state.config)#config{logging = Val}}; + mam -> + StateData#state{config = + (StateData#state.config)#config{mam = Val}}; captcha_whitelist -> StateData#state{config = (StateData#state.config)#config{captcha_whitelist @@ -3912,6 +4071,7 @@ make_opts(StateData) -> ?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(mam), ?MAKE_CONFIG_OPT(voice_request_min_interval), ?MAKE_CONFIG_OPT(vcard), {captcha_whitelist, @@ -3942,7 +4102,7 @@ destroy_room(DEl, StateData) -> children = []}, DEl]}]}, - ejabberd_router:route(jlib:jid_replace_resource(StateData#state.jid, + ejabberd_router:route(jid:replace_resource(StateData#state.jid, Nick), Info#user.jid, Packet) end, @@ -3995,6 +4155,15 @@ process_iq_disco_info(_From, get, Lang, StateData) -> <<"muc_moderated">>, <<"muc_unmoderated">>), ?CONFIG_OPT_TO_FEATURE((Config#config.password_protected), <<"muc_passwordprotected">>, <<"muc_unsecured">>)] + ++ case {gen_mod:is_loaded(StateData#state.server_host, mod_mam), + Config#config.mam} of + {true, true} -> + [?FEATURE(?NS_MAM_TMP), + ?FEATURE(?NS_MAM_0), + ?FEATURE(?NS_MAM_1)]; + _ -> + [] + end ++ iq_disco_info_extras(Lang, StateData), StateData}. @@ -4056,7 +4225,7 @@ process_iq_captcha(_From, set, _Lang, SubEl, process_iq_vcard(_From, get, _Lang, _SubEl, StateData) -> #state{config = #config{vcard = VCardRaw}} = StateData, - case xml_stream:parse_element(VCardRaw) of + case fxml_stream:parse_element(VCardRaw) of #xmlel{children = VCardEls} -> {result, VCardEls, StateData}; {error, _} -> @@ -4065,7 +4234,7 @@ process_iq_vcard(_From, get, _Lang, _SubEl, StateData) -> process_iq_vcard(From, set, Lang, SubEl, StateData) -> case get_affiliation(From, StateData) of owner -> - VCardRaw = xml:element_to_binary(SubEl), + VCardRaw = fxml:element_to_binary(SubEl), Config = StateData#state.config, NewConfig = Config#config{vcard = VCardRaw}, change_config(NewConfig, StateData); @@ -4109,7 +4278,7 @@ get_mucroom_disco_items(StateData) -> #xmlel{name = <<"item">>, attrs = [{<<"jid">>, - jlib:jid_to_string({StateData#state.room, + jid:to_string({StateData#state.room, StateData#state.host, Nick})}, {<<"name">>, Nick}], @@ -4124,7 +4293,7 @@ is_voice_request(Els) -> lists:foldl(fun (#xmlel{name = <<"x">>, attrs = Attrs} = El, false) -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of + case fxml:get_attr_s(<<"xmlns">>, Attrs) of ?NS_XDATA -> case jlib:parse_xdata_submit(El) of [_ | _] = Fields -> @@ -4186,7 +4355,7 @@ prepare_request_form(Requester, Nick, Lang) -> [{xmlcdata, <<"participant">>}]}]}, ?STRINGXFIELD(<<"User JID">>, <<"muc#jid">>, - (jlib:jid_to_string(Requester))), + (jid:to_string(Requester))), ?STRINGXFIELD(<<"Nickname">>, <<"muc#roomnick">>, Nick), ?BOOLXFIELD(<<"Grant voice to this person?">>, @@ -4207,7 +4376,7 @@ is_voice_approvement(Els) -> lists:foldl(fun (#xmlel{name = <<"x">>, attrs = Attrs} = El, false) -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of + case fxml:get_attr_s(<<"xmlns">>, Attrs) of ?NS_XDATA -> case jlib:parse_xdata_submit(El) of [_ | _] = Fs -> @@ -4243,7 +4412,7 @@ extract_jid_from_voice_approvement(Els) -> Res -> Res end, lists:foldl(fun ({<<"muc#jid">>, [JIDStr]}, error) -> - case jlib:string_to_jid(JIDStr) of + case jid:from_string(JIDStr) of error -> error; J -> {ok, J} end; @@ -4261,9 +4430,9 @@ is_invitation(Els) -> lists:foldl(fun (#xmlel{name = <<"x">>, attrs = Attrs} = El, false) -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of + case fxml:get_attr_s(<<"xmlns">>, Attrs) of ?NS_MUC_USER -> - case xml:get_subtag(El, <<"invite">>) of + case fxml:get_subtag(El, <<"invite">>) of false -> false; _ -> true end; @@ -4279,20 +4448,20 @@ check_invitation(From, Els, Lang, StateData) -> (StateData#state.config)#config.allow_user_invites orelse FAffiliation == admin orelse FAffiliation == owner, - InviteEl = case xml:remove_cdata(Els) of + InviteEl = case fxml:remove_cdata(Els) of [#xmlel{name = <<"x">>, children = Els1} = XEl] -> - case xml:get_tag_attr_s(<<"xmlns">>, XEl) of + case fxml:get_tag_attr_s(<<"xmlns">>, XEl) of ?NS_MUC_USER -> ok; _ -> throw({error, ?ERR_BAD_REQUEST}) end, - case xml:remove_cdata(Els1) of + case fxml: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">>, + jid:from_string(fxml:get_tag_attr_s(<<"to">>, InviteEl)) of error -> throw({error, ?ERR_JID_MALFORMED}); @@ -4301,16 +4470,16 @@ check_invitation(From, Els, Lang, StateData) -> case CanInvite of false -> throw({error, ?ERR_NOT_ALLOWED}); true -> - Reason = xml:get_path_s(InviteEl, + Reason = fxml:get_path_s(InviteEl, [{elem, <<"reason">>}, cdata]), - ContinueEl = case xml:get_path_s(InviteEl, + ContinueEl = case fxml:get_path_s(InviteEl, [{elem, <<"continue">>}]) of <<>> -> []; Continue1 -> [Continue1] end, IEl = [#xmlel{name = <<"invite">>, - attrs = [{<<"from">>, jlib:jid_to_string(From)}], + attrs = [{<<"from">>, jid:to_string(From)}], children = [#xmlel{name = <<"reason">>, attrs = [], children = [{xmlcdata, Reason}]}] @@ -4333,11 +4502,10 @@ check_invitation(From, Els, Lang, StateData) -> translate:translate( Lang, <<"~s invites you to the room ~s">>), - [jlib:jid_to_string(From), - jlib:jid_to_string({StateData#state.room, + [jid:to_string(From), + jid:to_string({StateData#state.room, StateData#state.host, <<"">>})]), - case (StateData#state.config)#config.password_protected of @@ -4365,7 +4533,7 @@ check_invitation(From, Els, Lang, StateData) -> attrs = [{<<"xmlns">>, ?NS_XCONFERENCE}, {<<"jid">>, - jlib:jid_to_string({StateData#state.room, + jid:to_string({StateData#state.room, StateData#state.host, <<"">>})}], children = [{xmlcdata, Reason}]}, @@ -4390,15 +4558,15 @@ handle_roommessage_from_nonparticipant(Packet, Lang, %% Check in the packet is a decline. %% If so, also returns the splitted packet. -%% This function must be catched, +%% 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), + XEl = fxml:get_subtag(Packet, <<"x">>), + (?NS_MUC_USER) = fxml:get_tag_attr_s(<<"xmlns">>, XEl), + DEl = fxml:get_subtag(XEl, <<"decline">>), + ToString = fxml:get_tag_attr_s(<<"to">>, DEl), + ToJID = jid:from_string(ToString), {true, {Packet, XEl, DEl, ToJID}}. %% Send the decline to the inviter user. @@ -4406,7 +4574,7 @@ check_decline_invitation(Packet) -> send_decline_invitation({Packet, XEl, DEl, ToJID}, RoomJID, FromJID) -> FromString = - jlib:jid_to_string(jlib:jid_remove_resource(FromJID)), + jid:to_string(jid:remove_resource(FromJID)), #xmlel{name = <<"decline">>, attrs = DAttrs, children = DEls} = DEl, @@ -4418,7 +4586,7 @@ send_decline_invitation({Packet, XEl, DEl, ToJID}, Packet2 = replace_subelement(Packet, XEl2), ejabberd_router:route(RoomJID, ToJID, Packet2). -%% Given an element and a new subelement, +%% 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}, @@ -4456,7 +4624,7 @@ add_to_log(Type, Data, StateData) -> %% Users number checking tab_add_online_user(JID, StateData) -> - {LUser, LServer, LResource} = jlib:jid_tolower(JID), + {LUser, LServer, LResource} = jid:tolower(JID), US = {LUser, LServer}, Room = StateData#state.room, Host = StateData#state.host, @@ -4465,7 +4633,7 @@ tab_add_online_user(JID, StateData) -> room = Room, host = Host}). tab_remove_online_user(JID, StateData) -> - {LUser, LServer, LResource} = jlib:jid_tolower(JID), + {LUser, LServer, LResource} = jid:tolower(JID), US = {LUser, LServer}, Room = StateData#state.room, Host = StateData#state.host, @@ -4474,7 +4642,7 @@ tab_remove_online_user(JID, StateData) -> room = Room, host = Host}). tab_count_user(JID) -> - {LUser, LServer, _} = jlib:jid_tolower(JID), + {LUser, LServer, _} = jid:tolower(JID), US = {LUser, LServer}, case catch ets:select(muc_online_users, [{#muc_online_users{us = US, _ = '_'}, [], [[]]}]) @@ -4484,7 +4652,14 @@ tab_count_user(JID) -> end. element_size(El) -> - byte_size(xml:element_to_binary(El)). + byte_size(fxml:element_to_binary(El)). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Multicast + +send_multiple(From, Server, Users, Packet) -> + JIDs = [ User#user.jid || {_, User} <- ?DICT:to_list(Users)], + ejabberd_router_multicast:route_multicast(From, Server, JIDs, Packet). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Detect messange stanzas that don't have meaninful content @@ -4495,10 +4670,3 @@ has_body_or_subject(Packet) -> (#xmlel{name = <<"subject">>}) -> false; (_) -> true end, Packet#xmlel.children). - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% Multicast - -send_multiple(From, Server, Users, Packet) -> - JIDs = [ User#user.jid || {_, User} <- ?DICT:to_list(Users)], - ejabberd_router_multicast:route_multicast(From, Server, JIDs, Packet). diff --git a/src/mod_multicast.erl b/src/mod_multicast.erl index 8a196008..83520c0b 100644 --- a/src/mod_multicast.erl +++ b/src/mod_multicast.erl @@ -3,12 +3,32 @@ %%% Author : Badlop <badlop@process-one.net> %%% Purpose : Extended Stanza Addressing (XEP-0033) support %%% Created : 29 May 2007 by Badlop <badlop@process-one.net> +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2016 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_multicast). -author('badlop@process-one.net'). +-protocol({xep, 33, '1.1'}). + -behaviour(gen_server). -behaviour(gen_mod). @@ -20,7 +40,7 @@ -export([init/1, handle_info/2, handle_call/3, handle_cast/2, terminate/2, code_change/3]). --export([purge_loop/1]). +-export([purge_loop/1, mod_opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -131,7 +151,7 @@ init([LServerS, Opts]) -> try_start_loop(), create_pool(), ejabberd_router_multicast:register_route(LServerS), - ejabberd_router:register_route(LServiceS), + ejabberd_router:register_route(LServiceS, LServerS), {ok, #state{lservice = LServiceS, lserver = LServerS, access = Access, service_limits = SLimits}}. @@ -151,24 +171,11 @@ handle_cast(_Msg, State) -> {noreply, State}. handle_info({route, From, To, #xmlel{name = <<"iq">>, attrs = Attrs} = Packet}, State) -> - IQ = jlib:iq_query_info(Packet), - case catch process_iq(From, IQ, State) of - Result when is_record(Result, iq) -> - ejabberd_router:route(To, From, jlib:iq_to_xml(Result)); - {'EXIT', Reason} -> - ?ERROR_MSG("Error when processing IQ stanza: ~p", - [Reason]), - Err = jlib:make_error_reply(Packet, - ?ERR_INTERNAL_SERVER_ERROR), - ejabberd_router:route(To, From, Err); - reply -> - LServiceS = jts(To), - case xml:get_attr_s(<<"type">>, Attrs) of - <<"result">> -> - process_iqreply_result(From, LServiceS, Packet, State); - <<"error">> -> - process_iqreply_error(From, LServiceS, Packet) - end + case catch handle_iq(From, To, #xmlel{attrs = Attrs} = Packet, State) of + {'EXIT', Reason} -> + ?ERROR_MSG("Error when processing IQ stanza: ~p", + [Reason]); + _ -> ok end, {noreply, State}; %% XEP33 allows only 'message' and 'presence' stanza type @@ -186,11 +193,16 @@ handle_info({route, From, To, handle_info({route_trusted, From, Destinations, Packet}, #state{lservice = LServiceS, lserver = LServerS} = State) -> - route_trusted(LServiceS, LServerS, From, Destinations, - Packet), + case catch route_trusted(LServiceS, LServerS, From, Destinations, + Packet) of + {'EXIT', Reason} -> + ?ERROR_MSG("Error in route_trusted: ~p", [Reason]); + _ -> ok + end, {noreply, State}; handle_info({get_host, Pid}, State) -> - Pid ! {my_host, State#state.lservice}, {noreply, State}; + Pid ! {my_host, State#state.lservice}, + {noreply, State}; handle_info(_Info, State) -> {noreply, State}. terminate(_Reason, State) -> @@ -208,6 +220,28 @@ code_change(_OldVsn, State, _Extra) -> {ok, State}. %%% IQ Request Processing %%%------------------------ +handle_iq(From, To, #xmlel{attrs = Attrs} = Packet, State) -> + IQ = jlib:iq_query_info(Packet), + case catch process_iq(From, IQ, State) of + Result when is_record(Result, iq) -> + ejabberd_router:route(To, From, jlib:iq_to_xml(Result)); + {'EXIT', Reason} -> + ?ERROR_MSG("Error when processing IQ stanza: ~p", + [Reason]), + Err = jlib:make_error_reply(Packet, + ?ERR_INTERNAL_SERVER_ERROR), + ejabberd_router:route(To, From, Err); + reply -> + LServiceS = jts(To), + case fxml:get_attr_s(<<"type">>, Attrs) of + <<"result">> -> + process_iqreply_result(From, LServiceS, Packet, State); + <<"error">> -> + process_iqreply_error(From, LServiceS, Packet) + end; + ok -> ok + end. + process_iq(From, #iq{type = get, xmlns = ?NS_DISCO_INFO, lang = Lang} = IQ, @@ -270,7 +304,7 @@ iq_vcard(Lang) -> [{xmlcdata, <<(translate:translate(Lang, <<"ejabberd Multicast service">>))/binary, - "\nCopyright (c) 2002-2015 ProcessOne">>}]}]. + "\nCopyright (c) 2002-2016 ProcessOne">>}]}]. %%%------------------------- %%% Route @@ -361,7 +395,7 @@ act_groups(FromJID, Packet_stripped, AAttrs, LServiceS, perform(From, Packet, AAttrs, _, {route_single, Group}) -> [route_packet(From, ToUser, Packet, AAttrs, - Group#group.addresses) + Group#group.others, Group#group.addresses) || ToUser <- Group#group.dests]; perform(From, Packet, AAttrs, _, {{route_multicast, JID, RLimits}, Group}) -> @@ -375,6 +409,9 @@ perform(From, Packet, AAttrs, LServiceS, group = Group, renewal = true, sender = From, packet = Packet, aattrs = AAttrs, addresses = Group#group.addresses}); +perform(_From, _Packet, _AAttrs, LServiceS, + {{ask, LServiceS, _}, _Group}) -> + ok; perform(From, Packet, AAttrs, LServiceS, {{ask, Server, not_renewal}, Group}) -> send_query_info(Server, LServiceS), @@ -399,17 +436,17 @@ check_access(LServerS, Access, From) -> %%%------------------------- strip_addresses_element(Packet) -> - case xml:get_subtag(Packet, <<"addresses">>) of + case fxml:get_subtag(Packet, <<"addresses">>) of #xmlel{name = <<"addresses">>, attrs = AAttrs, children = Addresses} -> - case xml:get_attr_s(<<"xmlns">>, AAttrs) of + case fxml:get_attr_s(<<"xmlns">>, AAttrs) of ?NS_ADDRESS -> #xmlel{name = Name, attrs = Attrs, children = Els} = Packet, Els_stripped = lists:keydelete(<<"addresses">>, 2, Els), Packet_stripped = #xmlel{name = Name, attrs = Attrs, children = Els_stripped}, - {ok, Packet_stripped, AAttrs, Addresses}; + {ok, Packet_stripped, AAttrs, fxml:remove_cdata(Addresses)}; _ -> throw(ewxmlns) end; _ -> throw(eadsele) @@ -423,10 +460,10 @@ split_addresses_todeliver(Addresses) -> lists:partition(fun (XML) -> case XML of #xmlel{name = <<"address">>, attrs = Attrs} -> - case xml:get_attr_s(<<"delivered">>, Attrs) of + case fxml:get_attr_s(<<"delivered">>, Attrs) of <<"true">> -> false; _ -> - Type = xml:get_attr_s(<<"type">>, + Type = fxml:get_attr_s(<<"type">>, Attrs), case Type of <<"to">> -> true; @@ -462,10 +499,10 @@ check_limit_dests(SLimits, FromJID, Packet, convert_dest_record(XMLs) -> lists:map(fun (XML) -> - case xml:get_tag_attr_s(<<"jid">>, XML) of + case fxml:get_tag_attr_s(<<"jid">>, XML) of <<"">> -> #dest{jid_string = none, full_xml = XML}; JIDS -> - Type = xml:get_tag_attr_s(<<"type">>, XML), + Type = fxml:get_tag_attr_s(<<"type">>, XML), JIDJ = stj(JIDS), #dest{jid_string = JIDS, jid_jid = JIDJ, type = Type, full_xml = XML} @@ -488,7 +525,7 @@ split_dests_jid(Dests) -> Dests). report_not_jid(From, Packet, Dests) -> - Dests2 = [xml:element_to_binary(Dest#dest.full_xml) + Dests2 = [fxml:element_to_binary(Dest#dest.full_xml) || Dest <- Dests], [route_error(From, From, Packet, jid_malformed, <<"This service can not process the address: ", @@ -597,13 +634,13 @@ decide_action_group(Group) -> %%% Route packet %%%------------------------- -route_packet(From, ToDest, Packet, AAttrs, Addresses) -> +route_packet(From, ToDest, Packet, AAttrs, Others, Addresses) -> Dests = case ToDest#dest.type of <<"bcc">> -> []; _ -> [ToDest] end, route_packet2(From, ToDest#dest.jid_string, Dests, - Packet, AAttrs, Addresses). + Packet, AAttrs, {Others, Addresses}). route_packet_multicast(From, ToS, Packet, AAttrs, Dests, Addresses, Limits) -> @@ -629,6 +666,8 @@ route_packet2(From, ToS, Dests, Packet, AAttrs, ToJID = stj(ToS), ejabberd_router:route(From, ToJID, Packet2). +append_dests(_Dests, {Others, Addresses}) -> + Addresses++Others; append_dests([], Addresses) -> Addresses; append_dests([Dest | Dests], Addresses) -> append_dests(Dests, [Dest#dest.full_xml | Addresses]). @@ -644,7 +683,8 @@ check_relay(RS, LS, Gs) -> end. check_relay_required(RServer, LServerS, Groups) -> - case str:str(RServer, LServerS) > 0 of + case lists:suffix(str:tokens(LServerS, <<".">>), + str:tokens(RServer, <<".">>)) of true -> false; false -> check_relay_required(LServerS, Groups) end. @@ -693,12 +733,11 @@ process_iqreply_error(From, LServiceS, _Packet) -> %%% Check protocol support: Receive response: Disco %%%------------------------- -process_iqreply_result(From, LServiceS, Packet, - State) -> +process_iqreply_result(From, LServiceS, Packet, State) -> #xmlel{name = <<"query">>, attrs = Attrs2, children = Els2} = - xml:get_subtag(Packet, <<"query">>), - case xml:get_attr_s(<<"xmlns">>, Attrs2) of + fxml:get_subtag(Packet, <<"query">>), + case fxml:get_attr_s(<<"xmlns">>, Attrs2) of ?NS_DISCO_INFO -> process_discoinfo_result(From, LServiceS, Els2, State); ?NS_DISCO_ITEMS -> @@ -721,37 +760,35 @@ process_discoinfo_result(From, LServiceS, Els, process_discoinfo_result2(From, FromS, LServiceS, Els, Waiter) -> - Multicast_support = lists:any(fun (XML) -> - case XML of - #xmlel{name = <<"feature">>, - attrs = Attrs} -> - (?NS_ADDRESS) == - xml:get_attr_s(<<"var">>, - Attrs); - _ -> false - end - end, - Els), + Multicast_support = + lists:any( + fun(XML) -> + case XML of + #xmlel{name = <<"feature">>, attrs = Attrs} -> + (?NS_ADDRESS) == fxml:get_attr_s(<<"var">>, Attrs); + _ -> false + end + end, + Els), Group = Waiter#waiter.group, RServer = Group#group.server, case Multicast_support of - true -> - SenderT = sender_type(From), - RLimits = get_limits_xml(Els, SenderT), - add_response(RServer, - {multicast_supported, FromS, RLimits}), - FromM = Waiter#waiter.sender, - DestsM = Group#group.dests, - PacketM = Waiter#waiter.packet, - AAttrsM = Waiter#waiter.aattrs, - AddressesM = Waiter#waiter.addresses, - RServiceM = FromS, - route_packet_multicast(FromM, RServiceM, PacketM, - AAttrsM, DestsM, AddressesM, RLimits), - delo_waiter(Waiter); - false -> - case FromS of - RServer -> + true -> + SenderT = sender_type(From), + RLimits = get_limits_xml(Els, SenderT), + add_response(RServer, {multicast_supported, FromS, RLimits}), + FromM = Waiter#waiter.sender, + DestsM = Group#group.dests, + PacketM = Waiter#waiter.packet, + AAttrsM = Waiter#waiter.aattrs, + AddressesM = Waiter#waiter.addresses, + RServiceM = FromS, + route_packet_multicast(FromM, RServiceM, PacketM, + AAttrsM, DestsM, AddressesM, RLimits), + delo_waiter(Waiter); + false -> + case FromS of + RServer -> send_query_items(FromS, LServiceS), delo_waiter(Waiter), add_waiter(Waiter#waiter{awaiting = @@ -772,10 +809,10 @@ get_limits_els(Els) -> #xmlel{name = <<"x">>, attrs = Attrs, children = SubEls} -> case ((?NS_XDATA) == - xml:get_attr_s(<<"xmlns">>, Attrs)) + fxml:get_attr_s(<<"xmlns">>, Attrs)) and (<<"result">> == - xml:get_attr_s(<<"type">>, Attrs)) + fxml:get_attr_s(<<"type">>, Attrs)) of true -> get_limits_fields(SubEls) ++ R; false -> R @@ -791,11 +828,11 @@ get_limits_fields(Fields) -> #xmlel{name = <<"field">>, attrs = Attrs} -> (<<"FORM_TYPE">> == - xml:get_attr_s(<<"var">>, + fxml:get_attr_s(<<"var">>, Attrs)) and (<<"hidden">> == - xml:get_attr_s(<<"type">>, + fxml:get_attr_s(<<"type">>, Attrs)); _ -> false end @@ -813,8 +850,8 @@ get_limits_values(Values) -> children = SubEls} -> [#xmlel{name = <<"value">>, children = SubElsV}] = SubEls, - Number = xml:get_cdata(SubElsV), - Name = xml:get_attr_s(<<"var">>, Attrs), + Number = fxml:get_cdata(SubElsV), + Name = fxml:get_attr_s(<<"var">>, Attrs), [{jlib:binary_to_atom(Name), jlib:binary_to_integer(Number)} | R]; @@ -828,29 +865,44 @@ get_limits_values(Values) -> %%%------------------------- process_discoitems_result(From, LServiceS, Els) -> - List = lists:foldl(fun (XML, Res) -> - case XML of - #xmlel{name = <<"item">>, attrs = Attrs} -> - Res ++ [xml:get_attr_s(<<"jid">>, Attrs)]; - _ -> Res - end - end, - [], Els), - [send_query_info(Item, LServiceS) || Item <- List], FromS = jts(From), - {found_waiter, Waiter} = search_waiter(FromS, LServiceS, - items), - delo_waiter(Waiter), - add_waiter(Waiter#waiter{awaiting = - {List, LServiceS, info}, - renewal = false}). + case search_waiter(FromS, LServiceS, items) of + {found_waiter, Waiter} -> + List = lists:foldl( + fun(XML, Res) -> + case XML of + #xmlel{name = <<"item">>, attrs = Attrs} -> + SJID = fxml:get_attr_s(<<"jid">>, Attrs), + case jid:from_string(SJID) of + #jid{luser = <<"">>, + lresource = <<"">>} -> + [SJID | Res]; + _ -> Res + end; + _ -> Res + end + end, + [], Els), + case List of + [] -> + received_awaiter(FromS, Waiter, LServiceS); + _ -> + [send_query_info(Item, LServiceS) || Item <- List], + delo_waiter(Waiter), + add_waiter(Waiter#waiter{awaiting = + {List, LServiceS, info}, + renewal = false}) + end; + _ -> + ok + end. %%%------------------------- %%% Check protocol support: Receive response: Received awaiter %%%------------------------- received_awaiter(JID, Waiter, LServiceS) -> - {JIDs, LServiceS, info} = Waiter#waiter.awaiting, + {JIDs, LServiceS, _} = Waiter#waiter.awaiting, delo_waiter(Waiter), Group = Waiter#waiter.group, RServer = Group#group.server, @@ -862,8 +914,9 @@ received_awaiter(JID, Waiter, LServiceS) -> From = Waiter#waiter.sender, Packet = Waiter#waiter.packet, AAttrs = Waiter#waiter.aattrs, + Others = Group#group.others, Addresses = Waiter#waiter.addresses, - [route_packet(From, ToUser, Packet, AAttrs, Addresses) + [route_packet(From, ToUser, Packet, AAttrs, Others, Addresses) || ToUser <- Group#group.dests]; true -> send_query_info(RServer, LServiceS), @@ -887,8 +940,7 @@ create_cache() -> {attributes, record_info(fields, multicastc)}]). add_response(RServer, Response) -> - Secs = - calendar:datetime_to_gregorian_seconds(calendar:now_to_datetime(now())), + Secs = calendar:datetime_to_gregorian_seconds(calendar:local_time()), mnesia:dirty_write(#multicastc{rserver = RServer, response = Response, ts = Secs}). @@ -899,8 +951,7 @@ search_server_on_cache(RServer, _LServerS, Maxmins) -> case look_server(RServer) of not_cached -> not_cached; {cached, Response, Ts} -> - Now = - calendar:datetime_to_gregorian_seconds(calendar:now_to_datetime(now())), + Now = calendar:datetime_to_gregorian_seconds(calendar:local_time()), case is_obsolete(Response, Ts, Now, Maxmins) of false -> {cached, Response}; true -> {obsolete, Response} @@ -928,7 +979,7 @@ purge() -> Maxmins_positive = (?MAXTIME_CACHE_POSITIVE), Maxmins_negative = (?MAXTIME_CACHE_NEGATIVE), Now = - calendar:datetime_to_gregorian_seconds(calendar:now_to_datetime(now())), + calendar:datetime_to_gregorian_seconds(calendar:local_time()), purge(Now, {Maxmins_positive, Maxmins_negative}). purge(Now, Maxmins) -> @@ -982,8 +1033,12 @@ purge_loop(NM) -> %%%------------------------- create_pool() -> - catch ets:new(multicastp, - [duplicate_bag, public, named_table, {keypos, 2}]). + catch + begin + ets:new(multicastp, + [duplicate_bag, public, named_table, {keypos, 2}]), + ets:give_away(multicastp, whereis(ejabberd), ok) + end. add_waiter(Waiter) -> true = ets:insert(multicastp, Waiter). @@ -991,6 +1046,9 @@ add_waiter(Waiter) -> delo_waiter(Waiter) -> true = ets:delete_object(multicastp, Waiter). +-spec search_waiter(binary(), binary(), info | items) -> + {found_waiter, #waiter{}} | waiter_not_found. + search_waiter(JID, LServiceS, Type) -> Rs = ets:foldl(fun (W, Res) -> {JIDs, LServiceS1, Type1} = W#waiter.awaiting, @@ -1141,7 +1199,7 @@ to_binary(A) -> list_to_binary(hd(io_lib:format("~p", [A]))). route_error(From, To, Packet, ErrType, ErrText) -> #xmlel{attrs = Attrs} = Packet, - Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), + Lang = fxml:get_attr_s(<<"xml:lang">>, Attrs), Reply = make_reply(ErrType, Lang, ErrText), Err = jlib:make_error_reply(Packet, Reply), ejabberd_router:route(From, To, Err). @@ -1157,6 +1215,13 @@ make_reply(internal_server_error, Lang, ErrText) -> make_reply(forbidden, Lang, ErrText) -> ?ERRT_FORBIDDEN(Lang, ErrText). -stj(String) -> jlib:string_to_jid(String). +stj(String) -> jid:from_string(String). + +jts(String) -> jid:to_string(String). -jts(String) -> jlib:jid_to_string(String). +mod_opt_type(access) -> + fun (A) when is_atom(A) -> A end; +mod_opt_type(host) -> fun iolist_to_binary/1; +mod_opt_type(limits) -> + fun (A) when is_list(A) -> A end; +mod_opt_type(_) -> [access, host, limits]. diff --git a/src/mod_offline.erl b/src/mod_offline.erl index 7f9a81a0..fa6a961f 100644 --- a/src/mod_offline.erl +++ b/src/mod_offline.erl @@ -5,7 +5,7 @@ %%% Created : 5 Jan 2003 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,36 +25,51 @@ -module(mod_offline). +-compile([{parse_transform, ejabberd_sql_pt}]). + -author('alexey@process-one.net'). + +-protocol({xep, 13, '1.2'}). +-protocol({xep, 22, '1.4'}). +-protocol({xep, 23, '1.3'}). +-protocol({xep, 160, '1.0'}). +-protocol({xep, 334, '0.2'}). + -define(GEN_SERVER, p1_server). -behaviour(?GEN_SERVER). -behaviour(gen_mod). --export([count_offline_messages/2]). - -export([start/2, - start_link/2, + start_link/2, stop/1, store_packet/3, + store_offline_msg/5, resend_offline_messages/2, pop_offline_messages/3, get_sm_features/5, + get_sm_identity/5, + get_sm_items/5, + get_info/5, + handle_offline_query/3, remove_expired_messages/1, remove_old_messages/2, remove_user/2, - import/1, - import/3, - export/1, + import/1, + import/3, + export/1, get_queue_length/2, - get_offline_els/2, + count_offline_messages/2, + get_offline_els/2, webadmin_page/3, webadmin_user/4, webadmin_user_parse_query/5]). -%% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, - handle_info/2, terminate/2, code_change/3]). + handle_info/2, terminate/2, code_change/3, + mod_opt_type/1]). + +-deprecated({get_queue_length,2}). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -65,17 +80,9 @@ -include("ejabberd_web_admin.hrl"). --record(offline_msg, - {us = {<<"">>, <<"">>} :: {binary(), binary()}, - timestamp = now() :: erlang:timestamp() | '_', - expire = now() :: erlang:timestamp() | never | '_', - from = #jid{} :: jid() | '_', - to = #jid{} :: jid() | '_', - packet = #xmlel{} :: xmlel() | '_'}). +-include("mod_offline.hrl"). --record(state, - {host = <<"">> :: binary(), - access_max_offline_messages}). +-include("ejabberd_sql_pt.hrl"). -define(PROCNAME, ejabberd_offline). @@ -116,6 +123,8 @@ init([Host, Opts]) -> update_table(); _ -> ok end, + IQDisc = gen_mod:get_opt(iqdisc, Opts, fun gen_iq_handler:check_type/1, + no_queue), ejabberd_hooks:add(offline_message_hook, Host, ?MODULE, store_packet, 50), ejabberd_hooks:add(resend_offline_messages_hook, Host, @@ -128,13 +137,23 @@ init([Host, Opts]) -> ?MODULE, get_sm_features, 50), ejabberd_hooks:add(disco_local_features, Host, ?MODULE, get_sm_features, 50), + ejabberd_hooks:add(disco_sm_identity, Host, + ?MODULE, get_sm_identity, 50), + ejabberd_hooks:add(disco_sm_items, Host, + ?MODULE, get_sm_items, 50), + ejabberd_hooks:add(disco_info, Host, ?MODULE, get_info, 50), ejabberd_hooks:add(webadmin_page_host, Host, ?MODULE, webadmin_page, 50), ejabberd_hooks:add(webadmin_user, Host, ?MODULE, webadmin_user, 50), ejabberd_hooks:add(webadmin_user_parse_query, Host, - ?MODULE, webadmin_user_parse_query, 50), - AccessMaxOfflineMsgs = gen_mod:get_opt(access_max_user_messages, Opts, fun(A) -> A end, max_user_offline_messages), + ?MODULE, webadmin_user_parse_query, 50), + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_FLEX_OFFLINE, + ?MODULE, handle_offline_query, IQDisc), + AccessMaxOfflineMsgs = + gen_mod:get_opt(access_max_user_messages, Opts, + fun(A) when is_atom(A) -> A end, + max_user_offline_messages), {ok, #state{host = Host, access_max_offline_messages = AccessMaxOfflineMsgs}}. @@ -175,17 +194,24 @@ terminate(_Reason, State) -> ?MODULE, remove_user, 50), ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, get_sm_features, 50), ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, get_sm_features, 50), + ejabberd_hooks:delete(disco_sm_identity, Host, ?MODULE, get_sm_identity, 50), + ejabberd_hooks:delete(disco_sm_items, Host, ?MODULE, get_sm_items, 50), + ejabberd_hooks:delete(disco_info, Host, ?MODULE, get_info, 50), ejabberd_hooks:delete(webadmin_page_host, Host, ?MODULE, webadmin_page, 50), ejabberd_hooks:delete(webadmin_user, Host, ?MODULE, webadmin_user, 50), ejabberd_hooks:delete(webadmin_user_parse_query, Host, ?MODULE, webadmin_user_parse_query, 50), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_FLEX_OFFLINE), ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. +store_offline_msg(Host, US, Msgs, Len, MaxOfflineMsgs) -> + DBType = gen_mod:db_type(Host, ?MODULE), + store_offline_msg(Host, US, Msgs, Len, MaxOfflineMsgs, DBType). store_offline_msg(_Host, US, Msgs, Len, MaxOfflineMsgs, mnesia) -> @@ -224,7 +250,7 @@ store_offline_msg(Host, {User, _Server}, Msgs, Len, MaxOfflineMsgs, odbc) -> M#offline_msg.timestamp, <<"Offline Storage">>), XML = - ejabberd_odbc:escape(xml:element_to_binary(NewPacket)), + ejabberd_odbc:escape(fxml:element_to_binary(NewPacket)), odbc_queries:add_spool_sql(Username, XML) end, Msgs), @@ -248,10 +274,9 @@ store_offline_msg(Host, {User, _}, Msgs, Len, MaxOfflineMsgs, end, Msgs) end. -%% Function copied from ejabberd_sm.erl: get_max_user_messages(AccessRule, {User, Server}, Host) -> case acl:match_rule( - Host, AccessRule, jlib:make_jid(User, Server, <<"">>)) of + Host, AccessRule, jid:make(User, Server, <<"">>)) of Max when is_integer(Max) -> Max; infinity -> infinity; _ -> ?MAX_USER_MESSAGES @@ -274,27 +299,229 @@ get_sm_features(Acc, _From, _To, <<"">>, _Lang) -> {result, I} -> I; _ -> [] end, - {result, Feats ++ [?NS_FEATURE_MSGOFFLINE]}; + {result, Feats ++ [?NS_FEATURE_MSGOFFLINE, ?NS_FLEX_OFFLINE]}; get_sm_features(_Acc, _From, _To, ?NS_FEATURE_MSGOFFLINE, _Lang) -> %% override all lesser features... {result, []}; +get_sm_features(_Acc, #jid{luser = U, lserver = S}, #jid{luser = U, lserver = S}, + ?NS_FLEX_OFFLINE, _Lang) -> + {result, [?NS_FLEX_OFFLINE]}; + get_sm_features(Acc, _From, _To, _Node, _Lang) -> Acc. +get_sm_identity(_Acc, #jid{luser = U, lserver = S}, #jid{luser = U, lserver = S}, + ?NS_FLEX_OFFLINE, _Lang) -> + Identity = #xmlel{name = <<"identity">>, + attrs = [{<<"category">>, <<"automation">>}, + {<<"type">>, <<"message-list">>}]}, + [Identity]; +get_sm_identity(Acc, _From, _To, _Node, _Lang) -> + Acc. + +get_sm_items(_Acc, #jid{luser = U, lserver = S, lresource = R} = JID, + #jid{luser = U, lserver = S}, + ?NS_FLEX_OFFLINE, _Lang) -> + case ejabberd_sm:get_session_pid(U, S, R) of + Pid when is_pid(Pid) -> + Hdrs = read_message_headers(U, S), + BareJID = jid:to_string(jid:remove_resource(JID)), + Pid ! dont_ask_offline, + {result, lists:map( + fun({Node, From, _OfflineMsg}) -> + #xmlel{name = <<"item">>, + attrs = [{<<"jid">>, BareJID}, + {<<"node">>, Node}, + {<<"name">>, From}]} + end, Hdrs)}; + none -> + {result, []} + end; +get_sm_items(Acc, _From, _To, _Node, _Lang) -> + Acc. + +get_info(_Acc, #jid{luser = U, lserver = S, lresource = R}, + #jid{luser = U, lserver = S}, ?NS_FLEX_OFFLINE, _Lang) -> + N = jlib:integer_to_binary(count_offline_messages(U, S)), + case ejabberd_sm:get_session_pid(U, S, R) of + Pid when is_pid(Pid) -> + Pid ! dont_ask_offline; + none -> + ok + end, + [#xmlel{name = <<"x">>, + attrs = [{<<"xmlns">>, ?NS_XDATA}, + {<<"type">>, <<"result">>}], + children = [#xmlel{name = <<"field">>, + attrs = [{<<"var">>, <<"FORM_TYPE">>}, + {<<"type">>, <<"hidden">>}], + children = [#xmlel{name = <<"value">>, + children = [{xmlcdata, + ?NS_FLEX_OFFLINE}]}]}, + #xmlel{name = <<"field">>, + attrs = [{<<"var">>, <<"number_of_messages">>}], + children = [#xmlel{name = <<"value">>, + children = [{xmlcdata, N}]}]}]}]; +get_info(Acc, _From, _To, _Node, _Lang) -> + Acc. + +handle_offline_query(#jid{luser = U, lserver = S} = From, + #jid{luser = U, lserver = S} = _To, + #iq{type = Type, sub_el = SubEl} = IQ) -> + case Type of + get -> + case fxml:get_subtag(SubEl, <<"fetch">>) of + #xmlel{} -> + handle_offline_fetch(From); + false -> + handle_offline_items_view(From, SubEl) + end; + set -> + case fxml:get_subtag(SubEl, <<"purge">>) of + #xmlel{} -> + delete_all_msgs(U, S); + false -> + handle_offline_items_remove(From, SubEl) + end + end, + IQ#iq{type = result, sub_el = []}; +handle_offline_query(_From, _To, #iq{sub_el = SubEl} = IQ) -> + IQ#iq{type = error, sub_el = [SubEl, ?ERR_FORBIDDEN]}. + +handle_offline_items_view(JID, #xmlel{children = Items}) -> + {U, S, R} = jid:tolower(JID), + lists:foreach( + fun(Node) -> + case fetch_msg_by_node(JID, Node) of + {ok, OfflineMsg} -> + case offline_msg_to_route(S, OfflineMsg) of + {route, From, To, El} -> + NewEl = set_offline_tag(El, Node), + case ejabberd_sm:get_session_pid(U, S, R) of + Pid when is_pid(Pid) -> + Pid ! {route, From, To, NewEl}; + none -> + ok + end; + error -> + ok + end; + error -> + ok + end + end, get_nodes_from_items(Items, <<"view">>)). + +handle_offline_items_remove(JID, #xmlel{children = Items}) -> + lists:foreach( + fun(Node) -> + remove_msg_by_node(JID, Node) + end, get_nodes_from_items(Items, <<"remove">>)). + +get_nodes_from_items(Items, Action) -> + lists:flatmap( + fun(#xmlel{name = <<"item">>, attrs = Attrs}) -> + case fxml:get_attr_s(<<"action">>, Attrs) of + Action -> + case fxml:get_attr_s(<<"node">>, Attrs) of + <<"">> -> + []; + TS -> + [TS] + end; + _ -> + [] + end; + (_) -> + [] + end, Items). + +set_offline_tag(#xmlel{children = Els} = El, Node) -> + OfflineEl = #xmlel{name = <<"offline">>, + attrs = [{<<"xmlns">>, ?NS_FLEX_OFFLINE}], + children = [#xmlel{name = <<"item">>, + attrs = [{<<"node">>, Node}]}]}, + El#xmlel{children = [OfflineEl|Els]}. + +handle_offline_fetch(#jid{luser = U, lserver = S, lresource = R}) -> + case ejabberd_sm:get_session_pid(U, S, R) of + none -> + ok; + Pid when is_pid(Pid) -> + Pid ! dont_ask_offline, + lists:foreach( + fun({Node, _, Msg}) -> + case offline_msg_to_route(S, Msg) of + {route, From, To, El} -> + NewEl = set_offline_tag(El, Node), + Pid ! {route, From, To, NewEl}; + error -> + ok + end + end, read_message_headers(U, S)) + end. + +fetch_msg_by_node(To, <<Seq:20/binary, "+", From_s/binary>>) -> + case jid:from_string(From_s) of + From = #jid{} -> + case gen_mod:db_type(To#jid.lserver, ?MODULE) of + odbc -> + read_message(From, To, Seq, odbc); + DBType -> + case binary_to_timestamp(Seq) of + undefined -> ok; + TS -> read_message(From, To, TS, DBType) + end + end; + error -> + ok + end. + +remove_msg_by_node(To, <<Seq:20/binary, "+", From_s/binary>>) -> + case jid:from_string(From_s) of + From = #jid{} -> + case gen_mod:db_type(To#jid.lserver, ?MODULE) of + odbc -> + remove_message(From, To, Seq, odbc); + DBType -> + case binary_to_timestamp(Seq) of + undefined -> ok; + TS -> remove_message(From, To, TS, DBType) + end + end; + error -> + ok + end. + need_to_store(LServer, Packet) -> - Type = xml:get_tag_attr_s(<<"type">>, Packet), + Type = fxml:get_tag_attr_s(<<"type">>, Packet), if (Type /= <<"error">>) and (Type /= <<"groupchat">>) and (Type /= <<"headline">>) -> - case gen_mod:get_module_opt( - LServer, ?MODULE, store_empty_body, - fun(V) when is_boolean(V) -> V end, - true) of + case has_offline_tag(Packet) of false -> - xml:get_subtag(Packet, <<"body">>) /= false; + case check_store_hint(Packet) of + store -> + true; + no_store -> + false; + none -> + case gen_mod:get_module_opt( + LServer, ?MODULE, store_empty_body, + fun(V) when is_boolean(V) -> V; + (unless_chat_state) -> unless_chat_state + end, + unless_chat_state) of + false -> + fxml:get_subtag(Packet, <<"body">>) /= false; + unless_chat_state -> + not jlib:is_standalone_chat_state(Packet); + true -> + true + end + end; true -> - true + false end; true -> false @@ -303,52 +530,59 @@ need_to_store(LServer, Packet) -> store_packet(From, To, Packet) -> case need_to_store(To#jid.lserver, Packet) of true -> - case has_no_storage_hint(Packet) of - false -> - case check_event(From, To, Packet) of - true -> - #jid{luser = LUser, lserver = LServer} = To, - TimeStamp = now(), - #xmlel{children = Els} = Packet, - Expire = find_x_expire(TimeStamp, Els), - gen_mod:get_module_proc(To#jid.lserver, ?PROCNAME) ! - #offline_msg{us = {LUser, LServer}, - timestamp = TimeStamp, expire = Expire, - from = From, to = To, packet = Packet}, - stop; - _ -> ok - end; - _ -> ok - end; - false -> ok + case check_event(From, To, Packet) of + true -> + #jid{luser = LUser, lserver = LServer} = To, + TimeStamp = p1_time_compat:timestamp(), + #xmlel{children = Els} = Packet, + Expire = find_x_expire(TimeStamp, Els), + gen_mod:get_module_proc(To#jid.lserver, ?PROCNAME) ! + #offline_msg{us = {LUser, LServer}, + timestamp = TimeStamp, expire = Expire, + from = From, to = To, packet = Packet}, + stop; + _ -> ok + end; + false -> ok end. -has_no_storage_hint(Packet) -> - case xml:get_subtag(Packet, <<"no-store">>) of - #xmlel{attrs = Attrs} -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of - ?NS_HINTS -> - true; - _ -> - false - end; - _ -> - false +check_store_hint(Packet) -> + case has_store_hint(Packet) of + true -> + store; + false -> + case has_no_store_hint(Packet) of + true -> + no_store; + false -> + none + end end. -%% Check if the packet has any content about XEP-0022 or XEP-0085 +has_store_hint(Packet) -> + fxml:get_subtag_with_xmlns(Packet, <<"store">>, ?NS_HINTS) =/= false. + +has_no_store_hint(Packet) -> + fxml:get_subtag_with_xmlns(Packet, <<"no-store">>, ?NS_HINTS) =/= false + orelse + fxml:get_subtag_with_xmlns(Packet, <<"no-storage">>, ?NS_HINTS) =/= false. + +has_offline_tag(Packet) -> + fxml:get_subtag_with_xmlns(Packet, <<"offline">>, ?NS_FLEX_OFFLINE) =/= false. + +%% Check if the packet has any content about XEP-0022 check_event(From, To, Packet) -> #xmlel{name = Name, attrs = Attrs, children = Els} = Packet, case find_x_event(Els) of false -> true; El -> - case xml:get_subtag(El, <<"id">>) of + case fxml:get_subtag(El, <<"id">>) of false -> - case xml:get_subtag(El, <<"offline">>) of + case fxml:get_subtag(El, <<"offline">>) of false -> true; _ -> - ID = case xml:get_tag_attr_s(<<"id">>, Packet) of + ID = case fxml:get_tag_attr_s(<<"id">>, Packet) of <<"">> -> #xmlel{name = <<"id">>, attrs = [], children = []}; @@ -380,12 +614,12 @@ check_event(From, To, Packet) -> end end. -%% Check if the packet has subelements about XEP-0022, XEP-0085 or other +%% Check if the packet has subelements about XEP-0022 find_x_event([]) -> false; find_x_event([{xmlcdata, _} | Els]) -> find_x_event(Els); find_x_event([El | Els]) -> - case xml:get_tag_attr_s(<<"xmlns">>, El) of + case fxml:get_tag_attr_s(<<"xmlns">>, El) of ?NS_EVENT -> El; _ -> find_x_event(Els) end. @@ -394,9 +628,9 @@ find_x_expire(_, []) -> never; find_x_expire(TimeStamp, [{xmlcdata, _} | Els]) -> find_x_expire(TimeStamp, Els); find_x_expire(TimeStamp, [El | Els]) -> - case xml:get_tag_attr_s(<<"xmlns">>, El) of + case fxml:get_tag_attr_s(<<"xmlns">>, El) of ?NS_EXPIRE -> - Val = xml:get_tag_attr_s(<<"seconds">>, El), + Val = fxml:get_tag_attr_s(<<"seconds">>, El), case catch jlib:binary_to_integer(Val) of {'EXIT', _} -> never; Int when Int > 0 -> @@ -411,8 +645,8 @@ find_x_expire(TimeStamp, [El | Els]) -> end. resend_offline_messages(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), US = {LUser, LServer}, F = fun () -> Rs = mnesia:wread({offline_msg, US}), @@ -434,8 +668,8 @@ resend_offline_messages(User, Server) -> end. pop_offline_messages(Ls, User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), pop_offline_messages(Ls, LUser, LServer, gen_mod:db_type(LServer, ?MODULE)). @@ -448,7 +682,7 @@ pop_offline_messages(Ls, LUser, LServer, mnesia) -> end, case mnesia:transaction(F) of {atomic, Rs} -> - TS = now(), + TS = p1_time_compat:timestamp(), Ls ++ lists:map(fun (R) -> offline_msg_to_route(LServer, R) @@ -463,14 +697,11 @@ pop_offline_messages(Ls, LUser, LServer, mnesia) -> _ -> Ls end; pop_offline_messages(Ls, LUser, LServer, odbc) -> - EUser = ejabberd_odbc:escape(LUser), - case odbc_queries:get_and_del_spool_msg_t(LServer, - EUser) - of - {atomic, {selected, [<<"username">>, <<"xml">>], Rs}} -> + case odbc_queries:get_and_del_spool_msg_t(LServer, LUser) of + {atomic, {selected, Rs}} -> Ls ++ - lists:flatmap(fun ([_, XML]) -> - case xml_stream:parse_element(XML) of + lists:flatmap(fun ({_, XML}) -> + case fxml_stream:parse_element(XML) of {error, _Reason} -> []; El -> @@ -494,7 +725,7 @@ pop_offline_messages(Ls, LUser, LServer, riak) -> fun(#offline_msg{timestamp = T}) -> ok = ejabberd_riak:delete(offline_msg, T) end, Rs), - TS = now(), + TS = p1_time_compat:timestamp(), Ls ++ lists:map( fun (R) -> offline_msg_to_route(LServer, R) @@ -515,12 +746,12 @@ pop_offline_messages(Ls, LUser, LServer, riak) -> end. remove_expired_messages(Server) -> - LServer = jlib:nameprep(Server), + LServer = jid:nameprep(Server), remove_expired_messages(LServer, gen_mod:db_type(LServer, ?MODULE)). remove_expired_messages(_LServer, mnesia) -> - TimeStamp = now(), + TimeStamp = p1_time_compat:timestamp(), F = fun () -> mnesia:write_lock_table(offline_msg), mnesia:foldl(fun (Rec, _Acc) -> @@ -540,13 +771,12 @@ remove_expired_messages(_LServer, odbc) -> {atomic, ok}; remove_expired_messages(_LServer, riak) -> {atomic, ok}. remove_old_messages(Days, Server) -> - LServer = jlib:nameprep(Server), + LServer = jid:nameprep(Server), remove_old_messages(Days, LServer, gen_mod:db_type(LServer, ?MODULE)). remove_old_messages(Days, _LServer, mnesia) -> - {MegaSecs, Secs, _MicroSecs} = now(), - S = MegaSecs * 1000000 + Secs - 60 * 60 * 24 * Days, + S = p1_time_compat:system_time(seconds) - 60 * 60 * 24 * Days, MegaSecs1 = S div 1000000, Secs1 = S rem 1000000, TimeStamp = {MegaSecs1, Secs1, 0}, @@ -579,8 +809,8 @@ remove_old_messages(_Days, _LServer, riak) -> {atomic, ok}. remove_user(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), remove_user(LUser, LServer, gen_mod:db_type(LServer, ?MODULE)). @@ -589,8 +819,7 @@ remove_user(LUser, LServer, mnesia) -> F = fun () -> mnesia:delete({offline_msg, US}) end, mnesia:transaction(F); remove_user(LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - odbc_queries:del_spool_msg(LServer, Username); + odbc_queries:del_spool_msg(LServer, LUser); remove_user(LUser, LServer, riak) -> {atomic, ejabberd_riak:delete_by_index(offline_msg, <<"us">>, {LUser, LServer})}. @@ -619,7 +848,7 @@ update_table() -> iolist_to_binary(S)}, from = jid_to_binary(From), to = jid_to_binary(To), - packet = xml:to_xmlel(El)} + packet = fxml:to_xmlel(El)} end); _ -> ?INFO_MSG("Recreating offline_msg table", []), @@ -634,7 +863,7 @@ discard_warn_sender(Msgs) -> packet = Packet}) -> ErrText = <<"Your contact offline message queue is " "full. The message has been discarded.">>, - Lang = xml:get_tag_attr_s(<<"xml:lang">>, Packet), + Lang = fxml:get_tag_attr_s(<<"xml:lang">>, Packet), Err = jlib:make_error_reply(Packet, ?ERRT_RESOURCE_CONSTRAINT(Lang, ErrText)), @@ -661,14 +890,14 @@ get_offline_els(LUser, LServer, DBType) jlib:replace_from_to(From, To, Packet) end, Msgs); get_offline_els(LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - case catch ejabberd_odbc:sql_query(LServer, - [<<"select xml from spool where username='">>, - Username, <<"' order by seq;">>]) of - {selected, [<<"xml">>], Rs} -> + case catch ejabberd_odbc:sql_query( + LServer, + ?SQL("select @(xml)s from spool where " + "username=%(LUser)s order by seq")) of + {selected, Rs} -> lists:flatmap( - fun([XML]) -> - case xml_stream:parse_element(XML) of + fun({XML}) -> + case fxml_stream:parse_element(XML) of #xmlel{} = El -> case offline_msg_to_route(LServer, El) of {route, _, _, NewEl} -> @@ -689,14 +918,131 @@ offline_msg_to_route(LServer, #offline_msg{} = R) -> jlib:add_delay_info(R#offline_msg.packet, LServer, R#offline_msg.timestamp, <<"Offline Storage">>)}; offline_msg_to_route(_LServer, #xmlel{} = El) -> - To = jlib:string_to_jid(xml:get_tag_attr_s(<<"to">>, El)), - From = jlib:string_to_jid(xml:get_tag_attr_s(<<"from">>, El)), + To = jid:from_string(fxml:get_tag_attr_s(<<"to">>, El)), + From = jid:from_string(fxml:get_tag_attr_s(<<"from">>, El)), if (To /= error) and (From /= error) -> {route, From, To, El}; true -> error end. +binary_to_timestamp(TS) -> + case catch jlib:binary_to_integer(TS) of + Int when is_integer(Int) -> + Secs = Int div 1000000, + USec = Int rem 1000000, + MSec = Secs div 1000000, + Sec = Secs rem 1000000, + {MSec, Sec, USec}; + _ -> + undefined + end. + +timestamp_to_binary({MS, S, US}) -> + format_timestamp(integer_to_list((MS * 1000000 + S) * 1000000 + US)). + +format_timestamp(TS) -> + iolist_to_binary(io_lib:format("~20..0s", [TS])). + +offline_msg_to_header(#offline_msg{from = From, timestamp = Int} = Msg) -> + TS = timestamp_to_binary(Int), + From_s = jid:to_string(From), + {<<TS/binary, "+", From_s/binary>>, From_s, Msg}. + +read_message_headers(LUser, LServer) -> + DBType = gen_mod:db_type(LServer, ?MODULE), + read_message_headers(LUser, LServer, DBType). + +read_message_headers(LUser, LServer, mnesia) -> + Msgs = mnesia:dirty_read({offline_msg, {LUser, LServer}}), + Hdrs = lists:map(fun offline_msg_to_header/1, Msgs), + lists:keysort(1, Hdrs); +read_message_headers(LUser, LServer, riak) -> + case ejabberd_riak:get_by_index( + offline_msg, offline_msg_schema(), + <<"us">>, {LUser, LServer}) of + {ok, Rs} -> + Hdrs = lists:map(fun offline_msg_to_header/1, Rs), + lists:keysort(1, Hdrs); + _Err -> + [] + end; +read_message_headers(LUser, LServer, odbc) -> + Username = ejabberd_odbc:escape(LUser), + case catch ejabberd_odbc:sql_query( + LServer, [<<"select xml, seq from spool where username ='">>, + Username, <<"' order by seq;">>]) of + {selected, [<<"xml">>, <<"seq">>], Rows} -> + Hdrs = lists:flatmap( + fun([XML, Seq]) -> + try + #xmlel{} = El = fxml_stream:parse_element(XML), + From = fxml:get_tag_attr_s(<<"from">>, El), + #jid{} = jid:from_string(From), + TS = format_timestamp(Seq), + [{<<TS/binary, "+", From/binary>>, From, El}] + catch _:_ -> [] + end + end, Rows), + lists:keysort(1, Hdrs); + _Err -> + [] + end. + +read_message(_From, To, TS, mnesia) -> + {U, S, _} = jid:tolower(To), + case mnesia:dirty_match_object( + offline_msg, #offline_msg{us = {U, S}, timestamp = TS, _ = '_'}) of + [Msg|_] -> + {ok, Msg}; + _ -> + error + end; +read_message(_From, _To, TS, riak) -> + case ejabberd_riak:get(offline_msg, offline_msg_schema(), TS) of + {ok, Msg} -> + {ok, Msg}; + _ -> + error + end; +read_message(_From, To, Seq, odbc) -> + {LUser, LServer, _} = jid:tolower(To), + Username = ejabberd_odbc:escape(LUser), + SSeq = ejabberd_odbc:escape(Seq), + case ejabberd_odbc:sql_query( + LServer, + [<<"select xml from spool where username='">>, Username, + <<"' and seq='">>, SSeq, <<"';">>]) of + {selected, [<<"xml">>], [[RawXML]|_]} -> + case fxml_stream:parse_element(RawXML) of + #xmlel{} = El -> {ok, El}; + {error, _} -> error + end; + _ -> + error + end. + +remove_message(_From, To, TS, mnesia) -> + {U, S, _} = jid:tolower(To), + Msgs = mnesia:dirty_match_object( + offline_msg, #offline_msg{us = {U, S}, timestamp = TS, _ = '_'}), + lists:foreach( + fun(Msg) -> + mnesia:dirty_delete_object(Msg) + end, Msgs); +remove_message(_From, _To, TS, riak) -> + ejabberd_riak:delete(offline_msg, TS), + ok; +remove_message(_From, To, Seq, odbc) -> + {LUser, LServer, _} = jid:tolower(To), + Username = ejabberd_odbc:escape(LUser), + SSeq = ejabberd_odbc:escape(Seq), + ejabberd_odbc:sql_query( + LServer, + [<<"delete from spool where username='">>, Username, + <<"' and seq='">>, SSeq, <<"';">>]), + ok. + read_all_msgs(LUser, LServer, mnesia) -> US = {LUser, LServer}, lists:keysort(#offline_msg.timestamp, @@ -711,20 +1057,20 @@ read_all_msgs(LUser, LServer, riak) -> [] end; read_all_msgs(LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - case catch ejabberd_odbc:sql_query(LServer, - [<<"select xml from spool where username='">>, - Username, <<"' order by seq;">>]) - of - {selected, [<<"xml">>], Rs} -> - lists:flatmap(fun ([XML]) -> - case xml_stream:parse_element(XML) of - {error, _Reason} -> []; - El -> [El] - end - end, - Rs); - _ -> [] + case catch ejabberd_odbc:sql_query( + LServer, + ?SQL("select @(xml)s from spool where " + "username=%(LUser)s order by seq")) of + {selected, Rs} -> + lists:flatmap( + fun({XML}) -> + case fxml_stream:parse_element(XML) of + {error, _Reason} -> []; + El -> [El] + end + end, + Rs); + _ -> [] end. format_user_queue(Msgs, DBType) when DBType == mnesia; DBType == riak -> @@ -742,8 +1088,8 @@ format_user_queue(Msgs, DBType) when DBType == mnesia; DBType == riak -> [Year, Month, Day, Hour, Minute, Second])), - SFrom = jlib:jid_to_string(From), - STo = jlib:jid_to_string(To), + SFrom = jid:to_string(From), + STo = jid:to_string(To), Attrs2 = jlib:replace_from_to_attrs(SFrom, STo, Attrs), Packet = #xmlel{name = Name, attrs = Attrs2, children = Els}, @@ -772,8 +1118,8 @@ format_user_queue(Msgs, odbc) -> Msgs). user_queue(User, Server, Query, Lang) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), US = {LUser, LServer}, DBType = gen_mod:db_type(LServer, ?MODULE), Res = user_queue_parse_query(LUser, LServer, Query, @@ -866,7 +1212,7 @@ user_queue_parse_query(LUser, LServer, Query, odbc) -> of {selected, [<<"xml">>, <<"seq">>], Rs} -> lists:flatmap(fun ([XML, Seq]) -> - case xml_stream:parse_element(XML) + case fxml_stream:parse_element(XML) of {error, _Reason} -> []; El -> [{El, Seq}] @@ -904,33 +1250,10 @@ user_queue_parse_query(LUser, LServer, Query, odbc) -> end. us_to_list({User, Server}) -> - jlib:jid_to_string({User, Server, <<"">>}). + jid:to_string({User, Server, <<"">>}). get_queue_length(LUser, LServer) -> - get_queue_length(LUser, LServer, - gen_mod:db_type(LServer, ?MODULE)). - -get_queue_length(LUser, LServer, mnesia) -> - length(mnesia:dirty_read({offline_msg, - {LUser, LServer}})); -get_queue_length(LUser, LServer, riak) -> - case ejabberd_riak:count_by_index(offline_msg, - <<"us">>, {LUser, LServer}) of - {ok, N} -> - N; - _ -> - 0 - end; -get_queue_length(LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - case catch ejabberd_odbc:sql_query(LServer, - [<<"select count(*) from spool where username='">>, - Username, <<"';">>]) - of - {selected, [_], [[SCount]]} -> - jlib:binary_to_integer(SCount); - _ -> 0 - end. + count_offline_messages(LUser, LServer). get_messages_subset(User, Host, MsgsAll, DBType) -> Access = gen_mod:get_module_opt(Host, ?MODULE, access_max_user_messages, @@ -955,8 +1278,8 @@ get_messages_subset2(Max, Length, MsgsAll, DBType) {MsgsFirstN, Msgs2} = lists:split(FirstN, MsgsAll), MsgsLastN = lists:nthtail(Length - FirstN - FirstN, Msgs2), - NoJID = jlib:make_jid(<<"...">>, <<"...">>, <<"">>), - IntermediateMsg = #offline_msg{timestamp = now(), + NoJID = jid:make(<<"...">>, <<"...">>, <<"">>), + IntermediateMsg = #offline_msg{timestamp = p1_time_compat:timestamp(), from = NoJID, to = NoJID, packet = #xmlel{name = <<"...">>, attrs = [], @@ -972,8 +1295,8 @@ get_messages_subset2(Max, Length, MsgsAll, odbc) -> MsgsFirstN ++ [IntermediateMsg] ++ MsgsLastN. webadmin_user(Acc, User, Server, Lang) -> - QueueLen = get_queue_length(jlib:nodeprep(User), - jlib:nameprep(Server)), + QueueLen = count_offline_messages(jid:nodeprep(User), + jid:nameprep(Server)), FQueueLen = [?AC(<<"queue/">>, (iolist_to_binary(integer_to_list(QueueLen))))], Acc ++ @@ -984,8 +1307,8 @@ webadmin_user(Acc, User, Server, Lang) -> <<"Remove All Offline Messages">>)]. delete_all_msgs(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), delete_all_msgs(LUser, LServer, gen_mod:db_type(LServer, ?MODULE)). @@ -1003,8 +1326,7 @@ delete_all_msgs(LUser, LServer, riak) -> <<"us">>, {LUser, LServer}), {atomic, Res}; delete_all_msgs(LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - odbc_queries:del_spool_msg(LServer, Username), + odbc_queries:del_spool_msg(LServer, LUser), {atomic, ok}. webadmin_user_parse_query(_, <<"removealloffline">>, @@ -1025,8 +1347,8 @@ webadmin_user_parse_query(Acc, _Action, _User, _Server, %% Returns as integer the number of offline messages for a given user count_offline_messages(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), DBType = gen_mod:db_type(LServer, ?MODULE), count_offline_messages(LUser, LServer, DBType). @@ -1040,15 +1362,13 @@ count_offline_messages(LUser, LServer, mnesia) -> _ -> 0 end; count_offline_messages(LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - case catch odbc_queries:count_records_where(LServer, - <<"spool">>, - <<"where username='", - Username/binary, "'">>) - of - {selected, [_], [[Res]]} -> - jlib:binary_to_integer(Res); - _ -> 0 + case catch ejabberd_odbc:sql_query( + LServer, + ?SQL("select @(count(*))d from spool " + "where username=%(LUser)s")) of + {selected, [{Res}]} -> + Res; + _ -> 0 end; count_offline_messages(LUser, LServer, riak) -> case ejabberd_riak:count_by_index( @@ -1098,7 +1418,7 @@ export(_Server) -> Packet1 = jlib:replace_from_to(From, To, Packet), Packet2 = jlib:add_delay_info(Packet1, LServer, TimeStamp, <<"Offline Storage">>), - XML = ejabberd_odbc:escape(xml:element_to_binary(Packet2)), + XML = ejabberd_odbc:escape(fxml:element_to_binary(Packet2)), [[<<"delete from spool where username='">>, Username, <<"';">>], [<<"insert into spool(username, xml) values ('">>, Username, <<"', '">>, XML, <<"');">>]]; @@ -1109,18 +1429,18 @@ export(_Server) -> import(LServer) -> [{<<"select username, xml from spool;">>, fun([LUser, XML]) -> - El = #xmlel{} = xml_stream:parse_element(XML), - From = #jid{} = jlib:string_to_jid( - xml:get_attr_s(<<"from">>, El#xmlel.attrs)), - To = #jid{} = jlib:string_to_jid( - xml:get_attr_s(<<"to">>, El#xmlel.attrs)), - Stamp = xml:get_path_s(El, [{elem, <<"delay">>}, + El = #xmlel{} = fxml_stream:parse_element(XML), + From = #jid{} = jid:from_string( + fxml:get_attr_s(<<"from">>, El#xmlel.attrs)), + To = #jid{} = jid:from_string( + fxml:get_attr_s(<<"to">>, El#xmlel.attrs)), + Stamp = fxml:get_path_s(El, [{elem, <<"delay">>}, {attr, <<"stamp">>}]), TS = case jlib:datetime_string_to_timestamp(Stamp) of {_, _, _} = Now -> Now; undefined -> - now() + p1_time_compat:timestamp() end, Expire = find_x_expire(TS, El#xmlel.children), #offline_msg{us = {LUser, LServer}, @@ -1135,3 +1455,13 @@ import(_LServer, riak, #offline_msg{us = US, timestamp = TS} = M) -> [{i, TS}, {'2i', [{<<"us">>, US}]}]); import(_, _, _) -> pass. + +mod_opt_type(access_max_user_messages) -> + fun (A) -> A end; +mod_opt_type(db_type) -> fun gen_mod:v_db/1; +mod_opt_type(store_empty_body) -> + fun (V) when is_boolean(V) -> V; + (unless_chat_state) -> unless_chat_state + end; +mod_opt_type(_) -> + [access_max_user_messages, db_type, store_empty_body]. diff --git a/src/mod_ping.erl b/src/mod_ping.erl index f493dccb..e8a977de 100644 --- a/src/mod_ping.erl +++ b/src/mod_ping.erl @@ -5,7 +5,7 @@ %%% Created : 11 Jul 2009 by Brian Cully <bjc@kublai.com> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -27,6 +27,8 @@ -author('bjc@kublai.com'). +-protocol({xep, 199, '2.0'}). + -behavior(gen_mod). -behavior(gen_server). @@ -54,14 +56,14 @@ -export([init/1, terminate/2, handle_call/3, handle_cast/2, handle_info/2, code_change/3]). -%% Hook callbacks -export([iq_ping/3, user_online/3, user_offline/3, - user_send/3]). + user_send/4, mod_opt_type/1]). -record(state, {host = <<"">>, send_pings = ?DEFAULT_SEND_PINGS :: boolean(), ping_interval = ?DEFAULT_PING_INTERVAL :: non_neg_integer(), + ping_ack_timeout = undefined :: non_neg_integer(), timeout_action = none :: none | kill, timers = (?DICT):new() :: ?TDICT}). @@ -105,6 +107,9 @@ init([Host, Opts]) -> PingInterval = gen_mod:get_opt(ping_interval, Opts, fun(I) when is_integer(I), I>0 -> I end, ?DEFAULT_PING_INTERVAL), + PingAckTimeout = gen_mod:get_opt(ping_ack_timeout, Opts, + fun(I) when is_integer(I), I>0 -> I * 1000 end, + undefined), TimeoutAction = gen_mod:get_opt(timeout_action, Opts, fun(none) -> none; (kill) -> kill @@ -130,6 +135,7 @@ init([Host, Opts]) -> #state{host = Host, send_pings = SendPings, ping_interval = PingInterval, timeout_action = TimeoutAction, + ping_ack_timeout = PingAckTimeout, timers = (?DICT):new()}}. terminate(_Reason, #state{host = Host}) -> @@ -168,7 +174,7 @@ handle_cast({iq_pong, JID, timeout}, State) -> JID, case ejabberd_sm:get_session_pid(User, Server, Resource) of - Pid when is_pid(Pid) -> ejabberd_c2s:stop(Pid); + Pid when is_pid(Pid) -> ejabberd_c2s:close(Pid); _ -> ok end; _ -> ok @@ -185,8 +191,8 @@ handle_info({timeout, _TRef, {ping, JID}}, State) -> F = fun (Response) -> gen_server:cast(Pid, {iq_pong, JID, Response}) end, - From = jlib:make_jid(<<"">>, State#state.host, <<"">>), - ejabberd_local:route_iq(From, JID, IQ, F), + From = jid:make(<<"">>, State#state.host, <<"">>), + ejabberd_local:route_iq(From, JID, IQ, F, State#state.ping_ack_timeout), Timers = add_timer(JID, State#state.ping_interval, State#state.timers), {noreply, State#state{timers = Timers}}; @@ -213,14 +219,15 @@ user_online(_SID, JID, _Info) -> user_offline(_SID, JID, _Info) -> stop_ping(JID#jid.lserver, JID). -user_send(JID, _From, _Packet) -> - start_ping(JID#jid.lserver, JID). +user_send(Packet, _C2SState, JID, _From) -> + start_ping(JID#jid.lserver, JID), + Packet. %%==================================================================== %% Internal functions %%==================================================================== add_timer(JID, Interval, Timers) -> - LJID = jlib:jid_tolower(JID), + LJID = jid:tolower(JID), NewTimers = case (?DICT):find(LJID, Timers) of {ok, OldTRef} -> cancel_timer(OldTRef), (?DICT):erase(LJID, Timers); @@ -231,7 +238,7 @@ add_timer(JID, Interval, Timers) -> (?DICT):store(LJID, TRef, NewTimers). del_timer(JID, Timers) -> - LJID = jlib:jid_tolower(JID), + LJID = jid:tolower(JID), case (?DICT):find(LJID, Timers) of {ok, TRef} -> cancel_timer(TRef), (?DICT):erase(LJID, Timers); @@ -244,3 +251,17 @@ cancel_timer(TRef) -> receive {timeout, TRef, _} -> ok after 0 -> ok end; _ -> ok end. + +mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; +mod_opt_type(ping_interval) -> + fun (I) when is_integer(I), I > 0 -> I end; +mod_opt_type(ping_ack_timeout) -> + fun (I) when is_integer(I), I > 0 -> I end; +mod_opt_type(send_pings) -> + fun (B) when is_boolean(B) -> B end; +mod_opt_type(timeout_action) -> + fun (none) -> none; + (kill) -> kill + end; +mod_opt_type(_) -> + [iqdisc, ping_interval, ping_ack_timeout, send_pings, timeout_action]. diff --git a/src/mod_pres_counter.erl b/src/mod_pres_counter.erl index e904ab95..1118b7bb 100644 --- a/src/mod_pres_counter.erl +++ b/src/mod_pres_counter.erl @@ -5,7 +5,7 @@ %%% Created : 23 Sep 2010 by Ahmed Omar %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -27,7 +27,8 @@ -behavior(gen_mod). --export([start/2, stop/1, check_packet/6]). +-export([start/2, stop/1, check_packet/6, + mod_opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -51,7 +52,7 @@ check_packet(_, _User, Server, _PrivacyList, {From, To, #xmlel{name = Name, attrs = Attrs}}, Dir) -> case Name of <<"presence">> -> - IsSubscription = case xml:get_attr_s(<<"type">>, Attrs) + IsSubscription = case fxml:get_attr_s(<<"type">>, Attrs) of <<"subscribe">> -> true; <<"subscribed">> -> true; @@ -77,8 +78,7 @@ update(Server, JID, Dir) -> TimeInterval = gen_mod:get_module_opt(Server, ?MODULE, interval, fun(I) when is_integer(I), I>0 -> I end, 60), - {MegaSecs, Secs, _MicroSecs} = now(), - TimeStamp = MegaSecs * 1000000 + Secs, + TimeStamp = p1_time_compat:system_time(seconds), case read(Dir) of undefined -> write(Dir, @@ -98,14 +98,14 @@ update(Server, JID, Dir) -> in -> ?WARNING_MSG("User ~s is being flooded, ignoring received " "presence subscriptions", - [jlib:jid_to_string(JID)]); + [jid:to_string(JID)]); out -> IP = ejabberd_sm:get_user_ip(JID#jid.luser, JID#jid.lserver, JID#jid.lresource), ?WARNING_MSG("Flooder detected: ~s, on IP: ~s ignoring " "sent presence subscriptions~n", - [jlib:jid_to_string(JID), + [jid:to_string(JID), jlib:ip_to_list(IP)]) end, {stop, deny}; @@ -119,3 +119,9 @@ update(Server, JID, Dir) -> read(K) -> get({pres_counter, K}). write(K, V) -> put({pres_counter, K}, V). + +mod_opt_type(count) -> + fun (I) when is_integer(I), I > 0 -> I end; +mod_opt_type(interval) -> + fun (I) when is_integer(I), I > 0 -> I end; +mod_opt_type(_) -> [count, interval]. diff --git a/src/mod_privacy.erl b/src/mod_privacy.erl index fd3f6024..193befe8 100644 --- a/src/mod_privacy.erl +++ b/src/mod_privacy.erl @@ -5,7 +5,7 @@ %%% Created : 21 Jul 2003 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -27,23 +27,25 @@ -author('alexey@process-one.net'). +-protocol({xep, 16, '1.6'}). + -behaviour(gen_mod). -export([start/2, stop/1, process_iq/3, export/1, import/1, process_iq_set/4, process_iq_get/5, get_user_list/3, check_packet/6, remove_user/2, item_to_raw/1, raw_to_item/1, is_list_needdb/1, updated_list/3, - item_to_xml/1, get_user_lists/2, import/3]). + item_to_xml/1, get_user_lists/2, import/3, + set_privacy_list/1]). -%% For mod_blocking -export([sql_add_privacy_list/2, sql_get_default_privacy_list/2, sql_get_default_privacy_list_t/1, sql_get_privacy_list_data/3, sql_get_privacy_list_data_by_id_t/1, sql_get_privacy_list_id_t/2, - sql_set_default_privacy_list/2, - sql_set_privacy_list/2, privacy_schema/0]). + sql_set_default_privacy_list/2, sql_set_privacy_list/2, + privacy_schema/0, mod_opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -107,12 +109,12 @@ process_iq_get(_, From, _To, #iq{sub_el = SubEl}, #userlist{name = Active}) -> #jid{luser = LUser, lserver = LServer} = From, #xmlel{children = Els} = SubEl, - case xml:remove_cdata(Els) of + case fxml:remove_cdata(Els) of [] -> process_lists_get(LUser, LServer, Active); [#xmlel{name = Name, attrs = Attrs}] -> case Name of <<"list">> -> - ListName = xml:get_attr(<<"name">>, Attrs), + ListName = fxml:get_attr(<<"name">>, Attrs), process_list_get(LUser, LServer, ListName); _ -> {error, ?ERR_BAD_REQUEST} end; @@ -179,16 +181,14 @@ process_lists_get(LUser, LServer, _Active, riak) -> error end; process_lists_get(LUser, LServer, _Active, odbc) -> - Default = case catch sql_get_default_privacy_list(LUser, - LServer) - of - {selected, [<<"name">>], []} -> none; - {selected, [<<"name">>], [[DefName]]} -> DefName; + Default = case catch sql_get_default_privacy_list(LUser, LServer) of + {selected, []} -> none; + {selected, [{DefName}]} -> DefName; _ -> none end, case catch sql_get_privacy_list_names(LUser, LServer) of - {selected, [<<"name">>], Names} -> - LItems = lists:map(fun ([N]) -> + {selected, Names} -> + LItems = lists:map(fun ({N}) -> #xmlel{name = <<"list">>, attrs = [{<<"name">>, N}], children = []} @@ -240,17 +240,11 @@ process_list_get(LUser, LServer, Name, riak) -> error end; process_list_get(LUser, LServer, Name, odbc) -> - case catch sql_get_privacy_list_id(LUser, LServer, Name) - of - {selected, [<<"id">>], []} -> not_found; - {selected, [<<"id">>], [[ID]]} -> - case catch sql_get_privacy_list_data_by_id(ID, LServer) - of - {selected, - [<<"t">>, <<"value">>, <<"action">>, <<"ord">>, - <<"match_all">>, <<"match_iq">>, <<"match_message">>, - <<"match_presence_in">>, <<"match_presence_out">>], - RItems} -> + case catch sql_get_privacy_list_id(LUser, LServer, Name) of + {selected, []} -> not_found; + {selected, [{ID}]} -> + case catch sql_get_privacy_list_data_by_id(ID, LServer) of + {selected, RItems} -> lists:flatmap(fun raw_to_item/1, RItems); _ -> error end; @@ -321,7 +315,7 @@ type_to_list(Type) -> value_to_list(Type, Val) -> case Type of - jid -> jlib:jid_to_string(Val); + jid -> jid:to_string(Val); group -> Val; subscription -> case Val of @@ -341,14 +335,14 @@ list_to_action(S) -> process_iq_set(_, From, _To, #iq{sub_el = SubEl}) -> #jid{luser = LUser, lserver = LServer} = From, #xmlel{children = Els} = SubEl, - case xml:remove_cdata(Els) of + case fxml:remove_cdata(Els) of [#xmlel{name = Name, attrs = Attrs, children = SubEls}] -> - ListName = xml:get_attr(<<"name">>, Attrs), + ListName = fxml:get_attr(<<"name">>, Attrs), case Name of <<"list">> -> process_list_set(LUser, LServer, ListName, - xml:remove_cdata(SubEls)); + fxml:remove_cdata(SubEls)); <<"active">> -> process_active_set(LUser, LServer, ListName); <<"default">> -> @@ -403,9 +397,9 @@ process_default_set(LUser, LServer, {value, Name}, odbc) -> F = fun () -> case sql_get_privacy_list_names_t(LUser) of - {selected, [<<"name">>], []} -> not_found; - {selected, [<<"name">>], Names} -> - case lists:member([Name], Names) of + {selected, []} -> not_found; + {selected, Names} -> + case lists:member({Name}, Names) of true -> sql_set_default_privacy_list(LUser, Name), ok; false -> not_found end @@ -471,17 +465,11 @@ process_active_set(LUser, LServer, Name, riak) -> error end; process_active_set(LUser, LServer, Name, odbc) -> - case catch sql_get_privacy_list_id(LUser, LServer, Name) - of - {selected, [<<"id">>], []} -> error; - {selected, [<<"id">>], [[ID]]} -> - case catch sql_get_privacy_list_data_by_id(ID, LServer) - of - {selected, - [<<"t">>, <<"value">>, <<"action">>, <<"ord">>, - <<"match_all">>, <<"match_iq">>, <<"match_message">>, - <<"match_presence_in">>, <<"match_presence_out">>], - RItems} -> + case catch sql_get_privacy_list_id(LUser, LServer, Name) of + {selected, []} -> error; + {selected, [{ID}]} -> + case catch sql_get_privacy_list_data_by_id(ID, LServer) of + {selected, RItems} -> lists:flatmap(fun raw_to_item/1, RItems); _ -> error end; @@ -518,9 +506,9 @@ remove_privacy_list(LUser, LServer, Name, riak) -> remove_privacy_list(LUser, LServer, Name, odbc) -> F = fun () -> case sql_get_default_privacy_list_t(LUser) of - {selected, [<<"name">>], []} -> + {selected, []} -> sql_remove_privacy_list(LUser, Name), ok; - {selected, [<<"name">>], [[Default]]} -> + {selected, [{Default}]} -> if Name == Default -> conflict; true -> sql_remove_privacy_list(LUser, Name), ok end @@ -528,6 +516,35 @@ remove_privacy_list(LUser, LServer, Name, odbc) -> end, odbc_queries:sql_transaction(LServer, F). +set_privacy_list(#privacy{us = {_, LServer}} = Privacy) -> + DBType = gen_mod:db_type(LServer, ?MODULE), + set_privacy_list(Privacy, DBType). + +set_privacy_list(Privacy, mnesia) -> + mnesia:dirty_write(Privacy); +set_privacy_list(Privacy, riak) -> + ejabberd_riak:put(Privacy, privacy_schema()); +set_privacy_list(#privacy{us = {LUser, LServer}, + default = Default, + lists = Lists}, odbc) -> + F = fun() -> + lists:foreach( + fun({Name, List}) -> + sql_add_privacy_list(LUser, Name), + {selected, [<<"id">>], [[I]]} = + sql_get_privacy_list_id_t(LUser, Name), + RItems = lists:map(fun item_to_raw/1, List), + sql_set_privacy_list(I, RItems), + if is_binary(Default) -> + sql_set_default_privacy_list(LUser, Default), + ok; + true -> + ok + end + end, Lists) + end, + odbc_queries:sql_transaction(LServer, F). + set_privacy_list(LUser, LServer, Name, List, mnesia) -> F = fun () -> case mnesia:wread({privacy, {LUser, LServer}}) of @@ -559,12 +576,12 @@ set_privacy_list(LUser, LServer, Name, List, odbc) -> RItems = lists:map(fun item_to_raw/1, List), F = fun () -> ID = case sql_get_privacy_list_id_t(LUser, Name) of - {selected, [<<"id">>], []} -> + {selected, []} -> sql_add_privacy_list(LUser, Name), - {selected, [<<"id">>], [[I]]} = + {selected, [{I}]} = sql_get_privacy_list_id_t(LUser, Name), I; - {selected, [<<"id">>], [[I]]} -> I + {selected, [{I}]} -> I end, sql_set_privacy_list(ID, RItems), ok @@ -580,14 +597,13 @@ process_list_set(LUser, LServer, {value, Name}, Els) -> of {atomic, conflict} -> {error, ?ERR_CONFLICT}; {atomic, ok} -> - ejabberd_sm:route(jlib:make_jid(LUser, LServer, + ejabberd_sm:route(jid:make(LUser, LServer, <<"">>), - jlib:make_jid(LUser, LServer, <<"">>), - {broadcast, - {privacy_list, - #userlist{name = Name, - list = []}, - Name}}), + jid:make(LUser, LServer, <<"">>), + {broadcast, {privacy_list, + #userlist{name = Name, + list = []}, + Name}}), {result, []}; _ -> {error, ?ERR_INTERNAL_SERVER_ERROR} end; @@ -597,15 +613,14 @@ process_list_set(LUser, LServer, {value, Name}, Els) -> of {atomic, ok} -> NeedDb = is_list_needdb(List), - ejabberd_sm:route(jlib:make_jid(LUser, LServer, + ejabberd_sm:route(jid:make(LUser, LServer, <<"">>), - jlib:make_jid(LUser, LServer, <<"">>), - {broadcast, - {privacy_list, - #userlist{name = Name, - list = List, - needdb = NeedDb}, - Name}}), + jid:make(LUser, LServer, <<"">>), + {broadcast, {privacy_list, + #userlist{name = Name, + list = List, + needdb = NeedDb}, + Name}}), {result, []}; _ -> {error, ?ERR_INTERNAL_SERVER_ERROR} end @@ -622,10 +637,10 @@ parse_items([#xmlel{name = <<"item">>, attrs = Attrs, children = SubEls} | Els], Res) -> - Type = xml:get_attr(<<"type">>, Attrs), - Value = xml:get_attr(<<"value">>, Attrs), - SAction = xml:get_attr(<<"action">>, Attrs), - SOrder = xml:get_attr(<<"order">>, Attrs), + Type = fxml:get_attr(<<"type">>, Attrs), + Value = fxml:get_attr(<<"value">>, Attrs), + SAction = fxml:get_attr(<<"action">>, Attrs), + SOrder = fxml:get_attr(<<"order">>, Attrs), Action = case catch list_to_action(element(2, SAction)) of {'EXIT', _} -> false; @@ -646,11 +661,11 @@ parse_items([#xmlel{name = <<"item">>, attrs = Attrs, {{value, T}, {value, V}} -> case T of <<"jid">> -> - case jlib:string_to_jid(V) of + case jid:from_string(V) of error -> false; JID -> I1#listitem{type = jid, - value = jlib:jid_tolower(JID)} + value = jid:tolower(JID)} end; <<"group">> -> I1#listitem{type = group, value = V}; <<"subscription">> -> @@ -675,7 +690,7 @@ parse_items([#xmlel{name = <<"item">>, attrs = Attrs, case I2 of false -> false; _ -> - case parse_matches(I2, xml:remove_cdata(SubEls)) of + case parse_matches(I2, fxml:remove_cdata(SubEls)) of false -> false; I3 -> parse_items(Els, [I3 | Res]) end @@ -716,8 +731,8 @@ is_list_needdb(Items) -> Items). get_user_list(Acc, User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), {Default, Items} = get_user_list(Acc, LUser, LServer, gen_mod:db_type(LServer, ?MODULE)), NeedDb = is_list_needdb(Items), @@ -756,16 +771,11 @@ get_user_list(_, LUser, LServer, riak) -> get_user_list(_, LUser, LServer, odbc) -> case catch sql_get_default_privacy_list(LUser, LServer) of - {selected, [<<"name">>], []} -> {none, []}; - {selected, [<<"name">>], [[Default]]} -> + {selected, []} -> {none, []}; + {selected, [{Default}]} -> case catch sql_get_privacy_list_data(LUser, LServer, - Default) - of - {selected, - [<<"t">>, <<"value">>, <<"action">>, <<"ord">>, - <<"match_all">>, <<"match_iq">>, <<"match_message">>, - <<"match_presence_in">>, <<"match_presence_out">>], - RItems} -> + Default) of + {selected, RItems} -> {Default, lists:flatmap(fun raw_to_item/1, RItems)}; _ -> {none, []} end; @@ -773,8 +783,8 @@ get_user_list(_, LUser, LServer, odbc) -> end. get_user_lists(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), get_user_lists(LUser, LServer, gen_mod:db_type(LServer, ?MODULE)). get_user_lists(LUser, LServer, mnesia) -> @@ -793,26 +803,21 @@ get_user_lists(LUser, LServer, riak) -> end; get_user_lists(LUser, LServer, odbc) -> Default = case catch sql_get_default_privacy_list(LUser, LServer) of - {selected, [<<"name">>], []} -> + {selected, []} -> none; - {selected, [<<"name">>], [[DefName]]} -> + {selected, [{DefName}]} -> DefName; _ -> none end, case catch sql_get_privacy_list_names(LUser, LServer) of - {selected, [<<"name">>], Names} -> + {selected, Names} -> Lists = lists:flatmap( - fun([Name]) -> + fun({Name}) -> case catch sql_get_privacy_list_data( LUser, LServer, Name) of - {selected, - [<<"t">>, <<"value">>, <<"action">>, - <<"ord">>, <<"match_all">>, <<"match_iq">>, - <<"match_message">>, <<"match_presence_in">>, - <<"match_presence_out">>], - RItems} -> + {selected, RItems} -> [{Name, lists:flatmap(fun raw_to_item/1, RItems)}]; _ -> [] @@ -853,7 +858,7 @@ check_packet(_, User, Server, <<"message">> -> message; <<"iq">> -> iq; <<"presence">> -> - case xml:get_attr_s(<<"type">>, Attrs) of + case fxml:get_attr_s(<<"type">>, Attrs) of %% notification <<"">> -> presence; <<"unavailable">> -> presence; @@ -870,13 +875,13 @@ check_packet(_, User, Server, {_, _} -> other end, LJID = case Dir of - in -> jlib:jid_tolower(From); - out -> jlib:jid_tolower(To) + in -> jid:tolower(From); + out -> jid:tolower(To) end, {Subscription, Groups} = case NeedDb of true -> ejabberd_hooks:run_fold(roster_get_jid_info, - jlib:nameprep(Server), + jid:nameprep(Server), {none, []}, [User, Server, LJID]); @@ -945,8 +950,8 @@ is_type_match(Type, Value, JID, Subscription, Groups) -> end. remove_user(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), remove_user(LUser, LServer, gen_mod:db_type(LServer, ?MODULE)). @@ -965,16 +970,16 @@ updated_list(_, #userlist{name = OldName} = Old, true -> Old end. -raw_to_item([SType, SValue, SAction, SOrder, SMatchAll, - SMatchIQ, SMatchMessage, SMatchPresenceIn, - SMatchPresenceOut] = Row) -> +raw_to_item({SType, SValue, SAction, Order, MatchAll, + MatchIQ, MatchMessage, MatchPresenceIn, + MatchPresenceOut} = Row) -> try {Type, Value} = case SType of <<"n">> -> {none, none}; <<"j">> -> - case jlib:string_to_jid(SValue) of + case jid:from_string(SValue) of #jid{} = JID -> - {jid, jlib:jid_tolower(JID)} + {jid, jid:tolower(JID)} end; <<"g">> -> {group, SValue}; <<"s">> -> @@ -989,12 +994,6 @@ raw_to_item([SType, SValue, SAction, SOrder, SMatchAll, <<"a">> -> allow; <<"d">> -> deny end, - Order = jlib:binary_to_integer(SOrder), - MatchAll = ejabberd_odbc:to_bool(SMatchAll), - MatchIQ = ejabberd_odbc:to_bool(SMatchIQ), - MatchMessage = ejabberd_odbc:to_bool(SMatchMessage), - MatchPresenceIn = ejabberd_odbc:to_bool(SMatchPresenceIn), - MatchPresenceOut = ejabberd_odbc:to_bool(SMatchPresenceOut), [#listitem{type = Type, value = Value, action = Action, order = Order, match_all = MatchAll, match_iq = MatchIQ, match_message = MatchMessage, @@ -1014,7 +1013,7 @@ item_to_raw(#listitem{type = Type, value = Value, none -> {<<"n">>, <<"">>}; jid -> {<<"j">>, - ejabberd_odbc:escape(jlib:jid_to_string(Value))}; + ejabberd_odbc:escape(jid:to_string(Value))}; group -> {<<"g">>, ejabberd_odbc:escape(Value)}; subscription -> case Value of @@ -1028,58 +1027,29 @@ item_to_raw(#listitem{type = Type, value = Value, allow -> <<"a">>; deny -> <<"d">> end, - SOrder = iolist_to_binary(integer_to_list(Order)), - SMatchAll = if MatchAll -> <<"1">>; - true -> <<"0">> - end, - SMatchIQ = if MatchIQ -> <<"1">>; - true -> <<"0">> - end, - SMatchMessage = if MatchMessage -> <<"1">>; - true -> <<"0">> - end, - SMatchPresenceIn = if MatchPresenceIn -> <<"1">>; - true -> <<"0">> - end, - SMatchPresenceOut = if MatchPresenceOut -> <<"1">>; - true -> <<"0">> - end, - [SType, SValue, SAction, SOrder, SMatchAll, SMatchIQ, - SMatchMessage, SMatchPresenceIn, SMatchPresenceOut]. + {SType, SValue, SAction, Order, MatchAll, MatchIQ, + MatchMessage, MatchPresenceIn, MatchPresenceOut}. sql_get_default_privacy_list(LUser, LServer) -> - Username = ejabberd_odbc:escape(LUser), - odbc_queries:get_default_privacy_list(LServer, - Username). + odbc_queries:get_default_privacy_list(LServer, LUser). sql_get_default_privacy_list_t(LUser) -> - Username = ejabberd_odbc:escape(LUser), - odbc_queries:get_default_privacy_list_t(Username). + odbc_queries:get_default_privacy_list_t(LUser). sql_get_privacy_list_names(LUser, LServer) -> - Username = ejabberd_odbc:escape(LUser), - odbc_queries:get_privacy_list_names(LServer, Username). + odbc_queries:get_privacy_list_names(LServer, LUser). sql_get_privacy_list_names_t(LUser) -> - Username = ejabberd_odbc:escape(LUser), - odbc_queries:get_privacy_list_names_t(Username). + odbc_queries:get_privacy_list_names_t(LUser). sql_get_privacy_list_id(LUser, LServer, Name) -> - Username = ejabberd_odbc:escape(LUser), - SName = ejabberd_odbc:escape(Name), - odbc_queries:get_privacy_list_id(LServer, Username, - SName). + odbc_queries:get_privacy_list_id(LServer, LUser, Name). sql_get_privacy_list_id_t(LUser, Name) -> - Username = ejabberd_odbc:escape(LUser), - SName = ejabberd_odbc:escape(Name), - odbc_queries:get_privacy_list_id_t(Username, SName). + odbc_queries:get_privacy_list_id_t(LUser, Name). sql_get_privacy_list_data(LUser, LServer, Name) -> - Username = ejabberd_odbc:escape(LUser), - SName = ejabberd_odbc:escape(Name), - odbc_queries:get_privacy_list_data(LServer, Username, - SName). + odbc_queries:get_privacy_list_data(LServer, LUser, Name). sql_get_privacy_list_data_t(LUser, Name) -> Username = ejabberd_odbc:escape(LUser), @@ -1093,33 +1063,22 @@ sql_get_privacy_list_data_by_id_t(ID) -> odbc_queries:get_privacy_list_data_by_id_t(ID). sql_set_default_privacy_list(LUser, Name) -> - Username = ejabberd_odbc:escape(LUser), - SName = ejabberd_odbc:escape(Name), - odbc_queries:set_default_privacy_list(Username, SName). + odbc_queries:set_default_privacy_list(LUser, Name). sql_unset_default_privacy_list(LUser, LServer) -> - Username = ejabberd_odbc:escape(LUser), - odbc_queries:unset_default_privacy_list(LServer, - Username). + odbc_queries:unset_default_privacy_list(LServer, LUser). sql_remove_privacy_list(LUser, Name) -> - Username = ejabberd_odbc:escape(LUser), - SName = ejabberd_odbc:escape(Name), - odbc_queries:remove_privacy_list(Username, SName). + odbc_queries:remove_privacy_list(LUser, Name). sql_add_privacy_list(LUser, Name) -> - Username = ejabberd_odbc:escape(LUser), - SName = ejabberd_odbc:escape(Name), - odbc_queries:add_privacy_list(Username, SName). + odbc_queries:add_privacy_list(LUser, Name). sql_set_privacy_list(ID, RItems) -> odbc_queries:set_privacy_list(ID, RItems). sql_del_privacy_lists(LUser, LServer) -> - Username = ejabberd_odbc:escape(LUser), - Server = ejabberd_odbc:escape(LServer), - odbc_queries:del_privacy_lists(LServer, Server, - Username). + odbc_queries:del_privacy_lists(LServer, LUser). update_table() -> Fields = record_info(fields, privacy), @@ -1165,7 +1124,7 @@ update_table() -> end. export(Server) -> - case ejabberd_odbc:sql_query(jlib:nameprep(Server), + case catch ejabberd_odbc:sql_query(jid:nameprep(Server), [<<"select id from privacy_list order by " "id desc limit 1;">>]) of {selected, [<<"id">>], [[I]]} -> @@ -1255,3 +1214,7 @@ import(_LServer, riak, #privacy{} = P) -> ejabberd_riak:put(P, privacy_schema()); import(_, _, _) -> pass. + +mod_opt_type(db_type) -> fun gen_mod:v_db/1; +mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; +mod_opt_type(_) -> [db_type, iqdisc]. diff --git a/src/mod_private.erl b/src/mod_private.erl index e127c202..f3dceeaa 100644 --- a/src/mod_private.erl +++ b/src/mod_private.erl @@ -5,7 +5,7 @@ %%% Created : 16 Jan 2003 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -27,10 +27,13 @@ -author('alexey@process-one.net'). +-protocol({xep, 49, '1.2'}). + -behaviour(gen_mod). -export([start/2, stop/1, process_sm_iq/3, import/3, - remove_user/2, get_data/2, export/1, import/1]). + remove_user/2, get_data/2, export/1, import/1, + mod_opt_type/1, set_data/3]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -60,15 +63,12 @@ start(Host, Opts) -> end, ejabberd_hooks:add(remove_user, Host, ?MODULE, remove_user, 50), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, - ?NS_PRIVATE, ?MODULE, process_sm_iq, IQDisc), gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_PRIVATE, ?MODULE, process_sm_iq, IQDisc). stop(Host) -> ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 50), - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_PRIVATE), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_PRIVATE). @@ -82,19 +82,7 @@ process_sm_iq(#jid{luser = LUser, lserver = LServer}, IQ#iq{type = error, sub_el = [IQ#iq.sub_el, ?ERR_NOT_ACCEPTABLE]}; Data -> - DBType = gen_mod:db_type(LServer, ?MODULE), - F = fun () -> - lists:foreach(fun (Datum) -> - set_data(LUser, LServer, - Datum, DBType) - end, - Data) - end, - case DBType of - odbc -> ejabberd_odbc:sql_transaction(LServer, F); - mnesia -> mnesia:transaction(F); - riak -> F() - end, + set_data(LUser, LServer, Data), IQ#iq{type = result, sub_el = []} end; _ -> @@ -137,23 +125,35 @@ filter_xmlels(Xmlels) -> filter_xmlels(Xmlels, []). filter_xmlels([], Data) -> lists:reverse(Data); filter_xmlels([#xmlel{attrs = Attrs} = Xmlel | Xmlels], Data) -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of + case fxml:get_attr_s(<<"xmlns">>, Attrs) of <<"">> -> []; XmlNS -> filter_xmlels(Xmlels, [{XmlNS, Xmlel} | Data]) end; filter_xmlels([_ | Xmlels], Data) -> filter_xmlels(Xmlels, Data). +set_data(LUser, LServer, Data) -> + DBType = gen_mod:db_type(LServer, ?MODULE), + F = fun () -> + lists:foreach(fun (Datum) -> + set_data(LUser, LServer, + Datum, DBType) + end, + Data) + end, + case DBType of + odbc -> ejabberd_odbc:sql_transaction(LServer, F); + mnesia -> mnesia:transaction(F); + riak -> F() + end. + set_data(LUser, LServer, {XmlNS, Xmlel}, mnesia) -> mnesia:write(#private_storage{usns = {LUser, LServer, XmlNS}, xml = Xmlel}); set_data(LUser, LServer, {XMLNS, El}, odbc) -> - Username = ejabberd_odbc:escape(LUser), - LXMLNS = ejabberd_odbc:escape(XMLNS), - SData = ejabberd_odbc:escape(xml:element_to_binary(El)), - odbc_queries:set_private_data(LServer, Username, LXMLNS, - SData); + SData = fxml:element_to_binary(El), + odbc_queries:set_private_data(LServer, LUser, XMLNS, SData); set_data(LUser, LServer, {XMLNS, El}, riak) -> ejabberd_riak:put(#private_storage{usns = {LUser, LServer, XMLNS}, xml = El}, @@ -181,13 +181,11 @@ get_data(LUser, LServer, mnesia, end; get_data(LUser, LServer, odbc, [{XMLNS, El} | Els], Res) -> - Username = ejabberd_odbc:escape(LUser), - LXMLNS = ejabberd_odbc:escape(XMLNS), case catch odbc_queries:get_private_data(LServer, - Username, LXMLNS) + LUser, XMLNS) of - {selected, [<<"data">>], [[SData]]} -> - case xml_stream:parse_element(SData) of + {selected, [{SData}]} -> + case fxml_stream:parse_element(SData) of Data when is_record(Data, xmlel) -> get_data(LUser, LServer, odbc, Els, [Data | Res]) end; @@ -214,12 +212,11 @@ get_all_data(LUser, LServer, mnesia) -> xml = '$1'}, [], ['$1']}])); get_all_data(LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - case catch odbc_queries:get_private_data(LServer, Username) of - {selected, [<<"namespace">>, <<"data">>], Res} -> + case catch odbc_queries:get_private_data(LServer, LUser) of + {selected, Res} -> lists:flatmap( - fun([_, SData]) -> - case xml_stream:parse_element(SData) of + fun({_, SData}) -> + case fxml_stream:parse_element(SData) of #xmlel{} = El -> [El]; _ -> @@ -243,8 +240,8 @@ private_storage_schema() -> {record_info(fields, private_storage), #private_storage{}}. remove_user(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), remove_user(LUser, LServer, gen_mod:db_type(Server, ?MODULE)). @@ -266,9 +263,7 @@ remove_user(LUser, LServer, mnesia) -> end, mnesia:transaction(F); remove_user(LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - odbc_queries:del_user_private_storage(LServer, - Username); + odbc_queries:del_user_private_storage(LServer, LUser); remove_user(LUser, LServer, riak) -> {atomic, ejabberd_riak:delete_by_index(private_storage, <<"us">>, {LUser, LServer})}. @@ -284,7 +279,7 @@ update_table() -> R#private_storage{usns = {iolist_to_binary(U), iolist_to_binary(S), iolist_to_binary(NS)}, - xml = xml:to_xmlel(El)} + xml = fxml:to_xmlel(El)} end); _ -> ?INFO_MSG("Recreating private_storage table", []), @@ -299,7 +294,7 @@ export(_Server) -> Username = ejabberd_odbc:escape(LUser), LXMLNS = ejabberd_odbc:escape(XMLNS), SData = - ejabberd_odbc:escape(xml:element_to_binary(Data)), + ejabberd_odbc:escape(fxml:element_to_binary(Data)), odbc_queries:set_private_data_sql(Username, LXMLNS, SData); (_Host, _R) -> @@ -309,7 +304,7 @@ export(_Server) -> import(LServer) -> [{<<"select username, namespace, data from private_storage;">>, fun([LUser, XMLNS, XML]) -> - El = #xmlel{} = xml_stream:parse_element(XML), + El = #xmlel{} = fxml_stream:parse_element(XML), #private_storage{usns = {LUser, LServer, XMLNS}, xml = El} end}]. @@ -322,3 +317,7 @@ import(_LServer, riak, #private_storage{usns = {LUser, LServer, _}} = PS) -> [{'2i', [{<<"us">>, {LUser, LServer}}]}]); import(_, _, _) -> pass. + +mod_opt_type(db_type) -> fun gen_mod:v_db/1; +mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; +mod_opt_type(_) -> [db_type, iqdisc]. diff --git a/src/mod_proxy65.erl b/src/mod_proxy65.erl index 6eced10b..2737de7a 100644 --- a/src/mod_proxy65.erl +++ b/src/mod_proxy65.erl @@ -5,7 +5,7 @@ %%% Created : 12 Oct 2006 by Evgeniy Khramtsov <xram@jabber.ru> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -27,6 +27,8 @@ -author('xram@jabber.ru'). +-protocol({xep, 65, '1.8'}). + -behaviour(gen_mod). -behaviour(supervisor). @@ -37,8 +39,7 @@ %% supervisor callbacks. -export([init/1]). -%% API. --export([start_link/2]). +-export([start_link/2, mod_opt_type/1]). -define(PROCNAME, ejabberd_mod_proxy65). @@ -82,3 +83,35 @@ init([Host, Opts]) -> {ok, {{one_for_one, 10, 1}, [StreamManager, StreamSupervisor, Service]}}. + +mod_opt_type(auth_type) -> + fun (plain) -> plain; + (anonymous) -> anonymous + end; +mod_opt_type(recbuf) -> + fun (I) when is_integer(I), I > 0 -> I end; +mod_opt_type(shaper) -> + fun (A) when is_atom(A) -> A end; +mod_opt_type(sndbuf) -> + fun (I) when is_integer(I), I > 0 -> I end; +mod_opt_type(access) -> + fun (A) when is_atom(A) -> A end; +mod_opt_type(host) -> fun iolist_to_binary/1; +mod_opt_type(hostname) -> fun iolist_to_binary/1; +mod_opt_type(ip) -> + fun (S) -> + {ok, Addr} = + inet_parse:address(binary_to_list(iolist_to_binary(S))), + Addr + end; +mod_opt_type(name) -> fun iolist_to_binary/1; +mod_opt_type(port) -> + fun (P) when is_integer(P), P > 0, P < 65536 -> P end; +mod_opt_type(max_connections) -> + fun (I) when is_integer(I), I > 0 -> I; + (infinity) -> infinity + end; +mod_opt_type(_) -> + [auth_type, recbuf, shaper, sndbuf, + access, host, hostname, ip, name, port, + max_connections]. diff --git a/src/mod_proxy65_lib.erl b/src/mod_proxy65_lib.erl index 6c596768..3cacbf98 100644 --- a/src/mod_proxy65_lib.erl +++ b/src/mod_proxy65_lib.erl @@ -5,7 +5,7 @@ %%% Created : 12 Oct 2006 by Evgeniy Khramtsov <xram@jabber.ru> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/mod_proxy65_service.erl b/src/mod_proxy65_service.erl index 1e7735c5..8b4644ba 100644 --- a/src/mod_proxy65_service.erl +++ b/src/mod_proxy65_service.erl @@ -5,7 +5,7 @@ %%% Created : 12 Oct 2006 by Evgeniy Khramtsov <xram@jabber.ru> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -33,9 +33,8 @@ -export([init/1, handle_info/2, handle_call/3, handle_cast/2, terminate/2, code_change/3]). -%% API. --export([start_link/2, add_listener/2, transform_module_options/1, - delete_listener/1]). +-export([start_link/2, add_listener/2, + transform_module_options/1, delete_listener/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -64,7 +63,7 @@ start_link(Host, Opts) -> init([Host, Opts]) -> State = parse_options(Host, Opts), - ejabberd_router:register_route(State#state.myhost), + ejabberd_router:register_route(State#state.myhost, Host), {ok, State}. terminate(_Reason, #state{myhost = MyHost}) -> @@ -176,19 +175,19 @@ process_iq(InitiatorJID, #state{acl = ACL, serverhost = ServerHost}) -> case acl:match_rule(ServerHost, ACL, InitiatorJID) of allow -> - ActivateEl = xml:get_path_s(SubEl, + ActivateEl = fxml:get_path_s(SubEl, [{elem, <<"activate">>}]), - SID = xml:get_tag_attr_s(<<"sid">>, SubEl), + SID = fxml:get_tag_attr_s(<<"sid">>, SubEl), case catch - jlib:string_to_jid(xml:get_tag_cdata(ActivateEl)) + jid:from_string(fxml:get_tag_cdata(ActivateEl)) of TargetJID when is_record(TargetJID, jid), SID /= <<"">>, byte_size(SID) =< 128, TargetJID /= InitiatorJID -> Target = - jlib:jid_to_string(jlib:jid_tolower(TargetJID)), + jid:to_string(jid:tolower(TargetJID)), Initiator = - jlib:jid_to_string(jlib:jid_tolower(InitiatorJID)), + jid:to_string(jid:tolower(InitiatorJID)), SHA1 = p1_sha:sha(<<SID/binary, Initiator/binary, Target/binary>>), case mod_proxy65_sm:activate_stream(SHA1, InitiatorJID, TargetJID, ServerHost) @@ -247,7 +246,7 @@ iq_vcard(Lang) -> [{xmlcdata, <<(translate:translate(Lang, <<"ejabberd SOCKS5 Bytestreams module">>))/binary, - "\nCopyright (c) 2003-2015 ProcessOne">>}]}]. + "\nCopyright (c) 2003-2016 ProcessOne">>}]}]. parse_options(ServerHost, Opts) -> MyHost = gen_mod:get_opt_host(ServerHost, Opts, diff --git a/src/mod_proxy65_sm.erl b/src/mod_proxy65_sm.erl index 367f7f0b..d86b06c4 100644 --- a/src/mod_proxy65_sm.erl +++ b/src/mod_proxy65_sm.erl @@ -5,7 +5,7 @@ %%% Created : 12 Oct 2006 by Evgeniy Khramtsov <xram@jabber.ru> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -33,7 +33,6 @@ -export([init/1, handle_info/2, handle_call/3, handle_cast/2, terminate/2, code_change/3]). -%% API. -export([start_link/2, register_stream/1, unregister_stream/1, activate_stream/4]). @@ -65,8 +64,9 @@ start_link(Host, Opts) -> []). init([Opts]) -> - mnesia:create_table(bytestream, [{ram_copies, [node()]}, - {attributes, record_info(fields, bytestream)}]), + mnesia:create_table(bytestream, + [{ram_copies, [node()]}, + {attributes, record_info(fields, bytestream)}]), mnesia:add_table_copy(bytestream, node(), ram_copies), MaxConnections = gen_mod:get_opt(max_connections, Opts, fun(I) when is_integer(I), I>0 -> diff --git a/src/mod_proxy65_stream.erl b/src/mod_proxy65_stream.erl index db894de7..84017329 100644 --- a/src/mod_proxy65_stream.erl +++ b/src/mod_proxy65_stream.erl @@ -4,7 +4,7 @@ %%% Purpose : Bytestream process. %%% Created : 12 Oct 2006 by Evgeniy Khramtsov <xram@jabber.ru> %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -37,7 +37,6 @@ wait_for_request/2, wait_for_activation/2, stream_established/2]). -%% API. -export([start/2, stop/1, start_link/3, activate/2, relay/3, socket_type/0]). @@ -121,8 +120,8 @@ activate({P1, J1}, {P2, J2}) -> {S1, S2} when is_port(S1), is_port(S2) -> P1 ! {activate, P2, S2, J1, J2}, P2 ! {activate, P1, S1, J1, J2}, - JID1 = jlib:jid_to_string(J1), - JID2 = jlib:jid_to_string(J2), + JID1 = jid:to_string(J1), + JID2 = jid:to_string(J2), ?INFO_MSG("(~w:~w) Activated bytestream for ~s " "-> ~s", [P1, P2, JID1, JID2]), diff --git a/src/mod_pubsub.erl b/src/mod_pubsub.erl index 80f6c05a..6531ed87 100644 --- a/src/mod_pubsub.erl +++ b/src/mod_pubsub.erl @@ -1,40 +1,28 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. -%%% +%%%---------------------------------------------------------------------- +%%% File : mod_pubsub.erl +%%% Author : Christophe Romain <christophe.romain@process-one.net> +%%% Purpose : Publish Subscribe service (XEP-0060) +%%% Created : 1 Dec 2007 by Christophe Romain <christophe.romain@process-one.net> %%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. -%%% %%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Christophe Romain <christophe.romain@process-one.net> -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== - -%%% @doc The module <strong>{@module}</strong> is the core of the PubSub -%%% extension. It relies on PubSub plugins for a large part of its functions. +%%% 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. %%% -%%% @headerfile "pubsub.hrl" +%%% 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. %%% -%%% @reference See <a href="http://www.xmpp.org/extensions/xep-0060.html">XEP-0060: Pubsub</a> for -%%% the latest version of the PubSub specification. -%%% This module uses version 1.12 of the specification as a base. -%%% Most of the specification is implemented. -%%% Functions concerning configuration should be rewritten. +%%% 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. %%% +%%%---------------------------------------------------------------------- + %%% Support for subscription-options and multi-subscribe features was %%% added by Brian Cully (bjc AT kublai.com). Subscriptions and options are %%% stored in the pubsub_subscription table, with a link to them provided @@ -47,7 +35,9 @@ -behaviour(gen_mod). -behaviour(gen_server). -author('christophe.romain@process-one.net'). --version('1.13-1'). +-protocol({xep, 60, '1.13-1'}). +-protocol({xep, 163, '1.2'}). +-protocol({xep, 248, '0.2'}). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -60,7 +50,7 @@ -define(PEPNODE, <<"pep">>). %% exports for hooks --export([presence_probe/3, caps_update/3, +-export([presence_probe/3, caps_add/3, caps_update/3, in_subscription/6, out_subscription/4, on_user_offline/3, remove_user/2, disco_local_identity/5, disco_local_features/5, @@ -73,7 +63,7 @@ %% exports for console debug manual use -export([create_node/5, create_node/7, delete_node/3, subscribe_node/5, unsubscribe_node/5, publish_item/6, - delete_item/4, send_items/7, get_items/2, get_item/3, + delete_item/4, delete_item/5, send_items/7, get_items/2, get_item/3, get_cached_item/2, get_configure/5, set_configure/5, tree_action/3, node_action/4, node_call/4]). @@ -81,15 +71,15 @@ -export([subscription_to_string/1, affiliation_to_string/1, string_to_subscription/1, string_to_affiliation/1, extended_error/2, extended_error/3, service_jid/1, - tree/1, tree/2, plugin/2, config/3, host/1, serverhost/1]). + tree/1, tree/2, plugin/2, plugins/1, config/3, + host/1, serverhost/1]). %% API and gen_server callbacks -export([start_link/2, start/2, stop/1, init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -%% calls for parallel sending of last items --export([send_loop/1]). +-export([send_loop/1, mod_opt_type/1]). -define(PROCNAME, ejabberd_mod_pubsub). -define(LOOPNAME, ejabberd_mod_pubsub_loop). @@ -190,6 +180,8 @@ ignore_pep_from_offline = true, last_item_cache = false, max_items_node = ?MAXITEMS, + max_subscriptions_node = undefined, + default_node_config = [], nodetree = <<"nodetree_", (?STDTREE)/binary>>, plugins = [?STDNODE], db_type @@ -204,6 +196,8 @@ ignore_pep_from_offline :: boolean(), last_item_cache :: boolean(), max_items_node :: non_neg_integer(), + max_subscriptions_node :: non_neg_integer()|undefined, + default_node_config :: [{atom(), binary()|boolean()|integer()|atom()}], nodetree :: binary(), plugins :: [binary(),...], db_type :: atom() @@ -247,6 +241,7 @@ stop(Host) -> init([ServerHost, Opts]) -> ?DEBUG("pubsub init ~p ~p", [ServerHost, Opts]), Host = gen_mod:get_opt_host(ServerHost, Opts, <<"pubsub.@HOST@">>), + ejabberd_router:register_route(Host, ServerHost), Access = gen_mod:get_opt(access_createnode, Opts, fun(A) when is_atom(A) -> A end, all), PepOffline = gen_mod:get_opt(ignore_pep_from_offline, Opts, @@ -257,21 +252,31 @@ init([ServerHost, Opts]) -> fun(A) when is_boolean(A) -> A end, false), MaxItemsNode = gen_mod:get_opt(max_items_node, Opts, fun(A) when is_integer(A) andalso A >= 0 -> A end, ?MAXITEMS), + MaxSubsNode = gen_mod:get_opt(max_subscriptions_node, Opts, + fun(A) when is_integer(A) andalso A >= 0 -> A end, undefined), + DefaultNodeCfg = gen_mod:get_opt(default_node_config, Opts, + fun(A) when is_list(A) -> filter_node_options(A) end, []), pubsub_index:init(Host, ServerHost, Opts), - ets:new(gen_mod:get_module_proc(ServerHost, config), [set, named_table]), {Plugins, NodeTree, PepMapping} = init_plugins(Host, ServerHost, Opts), mnesia:create_table(pubsub_last_item, [{ram_copies, [node()]}, {attributes, record_info(fields, pubsub_last_item)}]), mod_disco:register_feature(ServerHost, ?NS_PUBSUB), - ets:insert(gen_mod:get_module_proc(ServerHost, config), {nodetree, NodeTree}), - ets:insert(gen_mod:get_module_proc(ServerHost, config), {plugins, Plugins}), - ets:insert(gen_mod:get_module_proc(ServerHost, config), {last_item_cache, LastItemCache}), - ets:insert(gen_mod:get_module_proc(ServerHost, config), {max_items_node, MaxItemsNode}), - ets:insert(gen_mod:get_module_proc(ServerHost, config), {pep_mapping, PepMapping}), - ets:insert(gen_mod:get_module_proc(ServerHost, config), {ignore_pep_from_offline, PepOffline}), - ets:insert(gen_mod:get_module_proc(ServerHost, config), {host, Host}), - ets:insert(gen_mod:get_module_proc(ServerHost, config), {access, Access}), + lists:foreach( + fun(H) -> + T = gen_mod:get_module_proc(H, config), + ets:new(T, [set, named_table]), + ets:insert(T, {nodetree, NodeTree}), + ets:insert(T, {plugins, Plugins}), + ets:insert(T, {last_item_cache, LastItemCache}), + ets:insert(T, {max_items_node, MaxItemsNode}), + ets:insert(T, {max_subscriptions_node, MaxSubsNode}), + ets:insert(T, {default_node_config, DefaultNodeCfg}), + ets:insert(T, {pep_mapping, PepMapping}), + ets:insert(T, {ignore_pep_from_offline, PepOffline}), + ets:insert(T, {host, Host}), + ets:insert(T, {access, Access}) + end, [Host, ServerHost]), ejabberd_hooks:add(sm_remove_connection_hook, ServerHost, ?MODULE, on_user_offline, 75), ejabberd_hooks:add(disco_local_identity, ServerHost, @@ -292,6 +297,8 @@ init([ServerHost, Opts]) -> ?MODULE, remove_user, 50), case lists:member(?PEPNODE, Plugins) of true -> + ejabberd_hooks:add(caps_add, ServerHost, + ?MODULE, caps_add, 80), ejabberd_hooks:add(caps_update, ServerHost, ?MODULE, caps_update, 80), ejabberd_hooks:add(disco_sm_identity, ServerHost, @@ -307,11 +314,9 @@ init([ServerHost, Opts]) -> false -> ok end, - ejabberd_router:register_route(Host), pubsub_migrate:update_node_database(Host, ServerHost), pubsub_migrate:update_state_database(Host, ServerHost), pubsub_migrate:update_lastitem_database(Host, ServerHost), - init_nodes(Host, ServerHost, NodeTree, Plugins), {_, State} = init_send_loop(ServerHost), {ok, State}. @@ -385,22 +390,14 @@ terminate_plugins(Host, ServerHost, Plugins, TreePlugin) -> TreePlugin:terminate(Host, ServerHost), ok. -init_nodes(Host, ServerHost, _NodeTree, Plugins) -> - case lists:member(<<"hometree">>, Plugins) of - true -> - create_node(Host, ServerHost, <<"/home">>, service_jid(Host), <<"hometree">>), - create_node(Host, ServerHost, <<"/home/", ServerHost/binary>>, service_jid(Host), <<"hometree">>); - false -> ok - end. - send_loop(State) -> receive {presence, JID, Pid} -> Host = State#state.host, ServerHost = State#state.server_host, DBType = State#state.db_type, - LJID = jlib:jid_tolower(JID), - BJID = jlib:jid_remove_resource(LJID), + LJID = jid:tolower(JID), + BJID = jid:remove_resource(LJID), lists:foreach( fun(PType) -> Subs = get_subscriptions_for_send_last(Host, PType, DBType, JID, LJID, BJID), @@ -422,7 +419,7 @@ send_loop(State) -> fun({U, S, R}) when S == ServerHost -> case user_resources(U, S) of [] -> %% offline - PeerJID = jlib:make_jid(U, S, R), + PeerJID = jid:make(U, S, R), self() ! {presence, User, Server, [Resource], PeerJID}; _ -> %% online % this is already handled by presence probe @@ -443,7 +440,7 @@ send_loop(State) -> {presence, User, Server, Resources, JID} -> spawn(fun() -> Host = State#state.host, - Owner = jlib:jid_remove_resource(jlib:jid_tolower(JID)), + Owner = jid:remove_resource(jid:tolower(JID)), lists:foreach(fun(#pubsub_node{nodeid = {_, Node}, type = Type, id = Nidx, options = Options}) -> case match_option(Options, send_last_published_item, on_sub_and_presence) of true -> @@ -489,7 +486,7 @@ send_loop(State) -> -> [xmlel()] ). disco_local_identity(Acc, _From, To, <<>>, _Lang) -> - case lists:member(?PEPNODE, plugins(To#jid.lserver)) of + case lists:member(?PEPNODE, plugins(host(To#jid.lserver))) of true -> [#xmlel{name = <<"identity">>, attrs = [{<<"category">>, <<"pubsub">>}, @@ -511,7 +508,7 @@ disco_local_identity(Acc, _From, _To, _Node, _Lang) -> -> [binary(),...] ). disco_local_features(Acc, _From, To, <<>>, _Lang) -> - Host = To#jid.lserver, + Host = host(To#jid.lserver), Feats = case Acc of {result, I} -> I; _ -> [] @@ -539,7 +536,7 @@ disco_local_items(Acc, _From, _To, _Node, _Lang) -> Acc. disco_sm_identity(empty, From, To, Node, Lang) -> disco_sm_identity([], From, To, Node, Lang); disco_sm_identity(Acc, From, To, Node, _Lang) -> - disco_identity(jlib:jid_tolower(jlib:jid_remove_resource(To)), Node, From) + disco_identity(jid:tolower(jid:remove_resource(To)), Node, From) ++ Acc. disco_identity(_Host, <<>>, _From) -> @@ -588,7 +585,7 @@ disco_sm_features(empty, From, To, Node, Lang) -> disco_sm_features({result, OtherFeatures} = _Acc, From, To, Node, _Lang) -> {result, OtherFeatures ++ - disco_features(jlib:jid_tolower(jlib:jid_remove_resource(To)), Node, From)}; + disco_features(jid:tolower(jid:remove_resource(To)), Node, From)}; disco_sm_features(Acc, _From, _To, _Node, _Lang) -> Acc. disco_features(Host, <<>>, _From) -> @@ -623,7 +620,7 @@ disco_sm_items(empty, From, To, Node, Lang) -> disco_sm_items({result, []}, From, To, Node, Lang); disco_sm_items({result, OtherItems}, From, To, Node, _Lang) -> {result, lists:usort(OtherItems ++ - disco_items(jlib:jid_tolower(jlib:jid_remove_resource(To)), Node, From))}; + disco_items(jid:tolower(jid:remove_resource(To)), Node, From))}; disco_sm_items(Acc, _From, _To, _Node, _Lang) -> Acc. -spec(disco_items/3 :: @@ -642,7 +639,7 @@ disco_items(Host, <<>>, From) -> {result, _} -> [#xmlel{name = <<"item">>, attrs = [{<<"node">>, (Node)}, - {<<"jid">>, jlib:jid_to_string(Host)} + {<<"jid">>, jid:to_string(Host)} | case get_option(Options, title) of false -> []; [Title] -> [{<<"name">>, Title}] @@ -666,7 +663,7 @@ disco_items(Host, Node, From) -> case get_allowed_items_call(Host, Nidx, From, Type, Options, Owners) of {result, Items} -> {result, [#xmlel{name = <<"item">>, - attrs = [{<<"jid">>, jlib:jid_to_string(Host)}, + attrs = [{<<"jid">>, jid:to_string(Host)}, {<<"name">>, ItemId}]} || #pubsub_item{itemid = {ItemId, _}} <- Items]}; _ -> @@ -682,12 +679,24 @@ disco_items(Host, Node, From) -> %% presence hooks handling functions %% -caps_update(#jid{luser = U, lserver = S, lresource = R}, #jid{lserver = Host} = JID, _Features) +caps_add(#jid{luser = U, lserver = S, lresource = R}, #jid{lserver = Host} = JID, _Features) when Host =/= S -> + %% When a remote contact goes online while the local user is offline, the + %% remote contact won't receive last items from the local user even if + %% ignore_pep_from_offline is set to false. To work around this issue a bit, + %% we'll also send the last items to remote contacts when the local user + %% connects. That's the reason to use the caps_add hook instead of the + %% presence_probe_hook for remote contacts: The latter is only called when a + %% contact becomes available; the former is also executed when the local + %% user goes online (because that triggers the contact to send a presence + %% packet with CAPS). presence(Host, {presence, U, S, [R], JID}); -caps_update(_From, _To, _Feature) -> +caps_add(_From, _To, _Feature) -> ok. +caps_update(#jid{luser = U, lserver = S, lresource = R}, #jid{lserver = Host} = JID, _Features) -> + presence(Host, {presence, U, S, [R], JID}). + presence_probe(#jid{luser = U, lserver = S, lresource = R} = JID, JID, Pid) -> presence(S, {presence, JID, Pid}), presence(S, {presence, U, S, [R], JID}); @@ -699,7 +708,7 @@ presence_probe(#jid{luser = U, lserver = S, lresource = R}, #jid{lserver = S} = presence(S, {presence, U, S, [R], JID}); presence_probe(_Host, _JID, _Pid) -> %% ignore presence_probe from remote contacts, - %% those are handled via caps_update + %% those are handled via caps_add ok. presence(ServerHost, Presence) -> @@ -714,8 +723,8 @@ presence(ServerHost, Presence) -> %% out_subscription(User, Server, JID, subscribed) -> - Owner = jlib:make_jid(User, Server, <<>>), - {PUser, PServer, PResource} = jlib:jid_tolower(JID), + Owner = jid:make(User, Server, <<>>), + {PUser, PServer, PResource} = jid:tolower(JID), PResources = case PResource of <<>> -> user_resources(PUser, PServer); _ -> [PResource] @@ -726,54 +735,57 @@ out_subscription(_, _, _, _) -> true. in_subscription(_, User, Server, Owner, unsubscribed, _) -> - unsubscribe_user(jlib:make_jid(User, Server, <<>>), Owner), + unsubscribe_user(jid:make(User, Server, <<>>), Owner), true; in_subscription(_, _, _, _, _, _) -> true. unsubscribe_user(Entity, Owner) -> - BJID = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), - Host = host(element(2, BJID)), spawn(fun () -> - lists:foreach(fun (PType) -> - {result, Subs} = node_action(Host, PType, - get_entity_subscriptions, - [Host, Entity]), - lists:foreach(fun - ({#pubsub_node{options = Options, - owners = O, - id = Nidx}, - subscribed, _, JID}) -> - case match_option(Options, access_model, presence) of - true -> - Owners = node_owners_action(Host, PType, Nidx, O), - case lists:member(BJID, Owners) of - true -> - node_action(Host, PType, - unsubscribe_node, - [Nidx, Entity, JID, all]); - false -> - {result, ok} - end; - _ -> - {result, ok} - end; - (_) -> - ok - end, - Subs) - end, - plugins(Host)) + [unsubscribe_user(ServerHost, Entity, Owner) || + ServerHost <- lists:usort(lists:foldl( + fun(UserHost, Acc) -> + case gen_mod:is_loaded(UserHost, mod_pubsub) of + true -> [UserHost|Acc]; + false -> Acc + end + end, [], [Entity#jid.lserver, Owner#jid.lserver]))] end). +unsubscribe_user(Host, Entity, Owner) -> + BJID = jid:tolower(jid:remove_resource(Owner)), + lists:foreach(fun (PType) -> + {result, Subs} = node_action(Host, PType, + get_entity_subscriptions, + [Host, Entity]), + lists:foreach(fun + ({#pubsub_node{options = Options, + owners = O, + id = Nidx}, + subscribed, _, JID}) -> + Unsubscribe = match_option(Options, access_model, presence) + andalso lists:member(BJID, node_owners_action(Host, PType, Nidx, O)), + case Unsubscribe of + true -> + node_action(Host, PType, + unsubscribe_node, [Nidx, Entity, JID, all]); + false -> + ok + end; + (_) -> + ok + end, + Subs) + end, + plugins(Host)). %% ------- %% user remove hook handling function %% remove_user(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - Entity = jlib:make_jid(LUser, LServer, <<>>), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + Entity = jid:make(LUser, LServer, <<>>), Host = host(LServer), HomeTreeBase = <<"/home/", LServer/binary, "/", LUser/binary>>, spawn(fun () -> @@ -865,9 +877,10 @@ handle_info(_Info, State) -> %% @private terminate(_Reason, #state{host = Host, server_host = ServerHost, nodetree = TreePlugin, plugins = Plugins}) -> - ejabberd_router:unregister_route(Host), case lists:member(?PEPNODE, Plugins) of true -> + ejabberd_hooks:delete(caps_add, ServerHost, + ?MODULE, caps_add, 80), ejabberd_hooks:delete(caps_update, ServerHost, ?MODULE, caps_update, 80), ejabberd_hooks:delete(disco_sm_identity, ServerHost, @@ -908,7 +921,8 @@ terminate(_Reason, Pid -> Pid ! stop end, - terminate_plugins(Host, ServerHost, Plugins, TreePlugin). + terminate_plugins(Host, ServerHost, Plugins, TreePlugin), + ejabberd_router:unregister_route(Host). %%-------------------------------------------------------------------- %% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} @@ -941,7 +955,7 @@ do_route(ServerHost, Access, Plugins, Host, From, To, Packet) -> case jlib:iq_query_info(Packet) of #iq{type = get, xmlns = ?NS_DISCO_INFO, sub_el = SubEl, lang = Lang} = IQ -> #xmlel{attrs = QAttrs} = SubEl, - Node = xml:get_attr_s(<<"node">>, QAttrs), + Node = fxml:get_attr_s(<<"node">>, QAttrs), Info = ejabberd_hooks:run_fold(disco_info, ServerHost, [], [ServerHost, ?MODULE, <<>>, <<>>]), @@ -958,7 +972,7 @@ do_route(ServerHost, Access, Plugins, Host, From, To, Packet) -> ejabberd_router:route(To, From, Res); #iq{type = get, xmlns = ?NS_DISCO_ITEMS, sub_el = SubEl} = IQ -> #xmlel{attrs = QAttrs} = SubEl, - Node = xml:get_attr_s(<<"node">>, QAttrs), + Node = fxml:get_attr_s(<<"node">>, QAttrs), Res = case iq_disco_items(Host, Node, From, jlib:rsm_decode(IQ)) of {result, IQRes} -> jlib:iq_to_xml(IQ#iq{type = result, @@ -1012,7 +1026,7 @@ do_route(ServerHost, Access, Plugins, Host, From, To, Packet) -> ok end; <<"message">> -> - case xml:get_attr_s(<<"type">>, Attrs) of + case fxml:get_attr_s(<<"type">>, Attrs) of <<"error">> -> ok; _ -> @@ -1030,7 +1044,7 @@ do_route(ServerHost, Access, Plugins, Host, From, To, Packet) -> ok end; _ -> - case xml:get_attr_s(<<"type">>, Attrs) of + case fxml:get_attr_s(<<"type">>, Attrs) of <<"error">> -> ok; <<"result">> -> @@ -1190,7 +1204,7 @@ iq_disco_items(Host, Item, From, RSM) -> ). iq_sm(From, To, #iq{type = Type, sub_el = SubEl, xmlns = XMLNS, lang = Lang} = IQ) -> ServerHost = To#jid.lserver, - LOwner = jlib:jid_tolower(jlib:jid_remove_resource(To)), + LOwner = jid:tolower(jid:remove_resource(To)), Res = case XMLNS of ?NS_PUBSUB -> iq_pubsub(LOwner, ServerHost, From, Type, SubEl, Lang); @@ -1210,7 +1224,7 @@ iq_get_vcard(Lang) -> #xmlel{name = <<"DESC">>, attrs = [], children = [{xmlcdata, <<(translate:translate(Lang, <<"ejabberd Publish-Subscribe module">>))/binary, - "\nCopyright (c) 2004-2015 ProcessOne">>}]}]. + "\nCopyright (c) 2004-2016 ProcessOne">>}]}]. -spec(iq_pubsub/6 :: ( @@ -1226,7 +1240,7 @@ iq_get_vcard(Lang) -> ). iq_pubsub(Host, ServerHost, From, IQType, SubEl, Lang) -> - iq_pubsub(Host, ServerHost, From, IQType, SubEl, Lang, all, plugins(ServerHost)). + iq_pubsub(Host, ServerHost, From, IQType, SubEl, Lang, all, plugins(Host)). -spec(iq_pubsub/8 :: ( @@ -1245,16 +1259,16 @@ iq_pubsub(Host, ServerHost, From, IQType, SubEl, Lang) -> iq_pubsub(Host, ServerHost, From, IQType, SubEl, Lang, Access, Plugins) -> #xmlel{children = SubEls} = SubEl, - case xml:remove_cdata(SubEls) of + case fxml:remove_cdata(SubEls) of [#xmlel{name = Name, attrs = Attrs, children = Els} | Rest] -> - Node = xml:get_attr_s(<<"node">>, Attrs), + Node = fxml:get_attr_s(<<"node">>, Attrs), case {IQType, Name} of {set, <<"create">>} -> Config = case Rest of [#xmlel{name = <<"configure">>, children = C}] -> C; _ -> [] end, - Type = case xml:get_attr_s(<<"type">>, Attrs) of + Type = case fxml:get_attr_s(<<"type">>, Attrs) of <<>> -> hd(Plugins); T -> T end, @@ -1266,10 +1280,10 @@ iq_pubsub(Host, ServerHost, From, IQType, SubEl, Lang, Access, Plugins) -> create_node(Host, ServerHost, Node, From, Type, Access, Config) end; {set, <<"publish">>} -> - case xml:remove_cdata(Els) of + case fxml:remove_cdata(Els) of [#xmlel{name = <<"item">>, attrs = ItemAttrs, children = Payload}] -> - ItemId = xml:get_attr_s(<<"id">>, ItemAttrs), + ItemId = fxml:get_attr_s(<<"id">>, ItemAttrs), publish_item(Host, ServerHost, Node, From, ItemId, Payload, Access); [] -> {error, @@ -1279,14 +1293,14 @@ iq_pubsub(Host, ServerHost, From, IQType, SubEl, Lang, Access, Plugins) -> extended_error(?ERR_BAD_REQUEST, <<"invalid-payload">>)} end; {set, <<"retract">>} -> - ForceNotify = case xml:get_attr_s(<<"notify">>, Attrs) of + ForceNotify = case fxml:get_attr_s(<<"notify">>, Attrs) of <<"1">> -> true; <<"true">> -> true; _ -> false end, - case xml:remove_cdata(Els) of + case fxml:remove_cdata(Els) of [#xmlel{name = <<"item">>, attrs = ItemAttrs}] -> - ItemId = xml:get_attr_s(<<"id">>, ItemAttrs), + ItemId = fxml:get_attr_s(<<"id">>, ItemAttrs), delete_item(Host, Node, From, ItemId, ForceNotify); _ -> {error, @@ -1297,37 +1311,37 @@ iq_pubsub(Host, ServerHost, From, IQType, SubEl, Lang, Access, Plugins) -> [#xmlel{name = <<"options">>, children = C}] -> C; _ -> [] end, - JID = xml:get_attr_s(<<"jid">>, Attrs), + JID = fxml:get_attr_s(<<"jid">>, Attrs), subscribe_node(Host, Node, From, JID, Config); {set, <<"unsubscribe">>} -> - JID = xml:get_attr_s(<<"jid">>, Attrs), - SubId = xml:get_attr_s(<<"subid">>, Attrs), + JID = fxml:get_attr_s(<<"jid">>, Attrs), + SubId = fxml:get_attr_s(<<"subid">>, Attrs), unsubscribe_node(Host, Node, From, JID, SubId); {get, <<"items">>} -> - MaxItems = xml:get_attr_s(<<"max_items">>, Attrs), - SubId = xml:get_attr_s(<<"subid">>, Attrs), + MaxItems = fxml:get_attr_s(<<"max_items">>, Attrs), + SubId = fxml:get_attr_s(<<"subid">>, Attrs), ItemIds = lists:foldl(fun (#xmlel{name = <<"item">>, attrs = ItemAttrs}, Acc) -> - case xml:get_attr_s(<<"id">>, ItemAttrs) of + case fxml:get_attr_s(<<"id">>, ItemAttrs) of <<>> -> Acc; ItemId -> [ItemId | Acc] end; (_, Acc) -> Acc end, - [], xml:remove_cdata(Els)), + [], fxml:remove_cdata(Els)), get_items(Host, Node, From, SubId, MaxItems, ItemIds, jlib:rsm_decode(SubEl)); {get, <<"subscriptions">>} -> get_subscriptions(Host, Node, From, Plugins); {get, <<"affiliations">>} -> get_affiliations(Host, Node, From, Plugins); {get, <<"options">>} -> - SubId = xml:get_attr_s(<<"subid">>, Attrs), - JID = xml:get_attr_s(<<"jid">>, Attrs), + SubId = fxml:get_attr_s(<<"subid">>, Attrs), + JID = fxml:get_attr_s(<<"jid">>, Attrs), get_options(Host, Node, JID, SubId, Lang); {set, <<"options">>} -> - SubId = xml:get_attr_s(<<"subid">>, Attrs), - JID = xml:get_attr_s(<<"jid">>, Attrs), + SubId = fxml:get_attr_s(<<"subid">>, Attrs), + JID = fxml:get_attr_s(<<"jid">>, Attrs), set_options(Host, Node, JID, SubId, Els); _ -> {error, ?ERR_FEATURE_NOT_IMPLEMENTED} @@ -1352,10 +1366,10 @@ iq_pubsub(Host, ServerHost, From, IQType, SubEl, Lang, Access, Plugins) -> ). iq_pubsub_owner(Host, ServerHost, From, IQType, SubEl, Lang) -> #xmlel{children = SubEls} = SubEl, - Action = xml:remove_cdata(SubEls), + Action = fxml:remove_cdata(SubEls), case Action of [#xmlel{name = Name, attrs = Attrs, children = Els}] -> - Node = xml:get_attr_s(<<"node">>, Attrs), + Node = fxml:get_attr_s(<<"node">>, Attrs), case {IQType, Name} of {get, <<"configure">>} -> get_configure(Host, ServerHost, Node, From, Lang); @@ -1370,11 +1384,11 @@ iq_pubsub_owner(Host, ServerHost, From, IQType, SubEl, Lang) -> {get, <<"subscriptions">>} -> get_subscriptions(Host, Node, From); {set, <<"subscriptions">>} -> - set_subscriptions(Host, Node, From, xml:remove_cdata(Els)); + set_subscriptions(Host, Node, From, fxml:remove_cdata(Els)); {get, <<"affiliations">>} -> get_affiliations(Host, Node, From); {set, <<"affiliations">>} -> - set_affiliations(Host, Node, From, xml:remove_cdata(Els)); + set_affiliations(Host, Node, From, fxml:remove_cdata(Els)); _ -> {error, ?ERR_FEATURE_NOT_IMPLEMENTED} end; @@ -1484,7 +1498,7 @@ get_pending_nodes(Host, Owner, Plugins) -> %% subscriptions on Host and Node.</p> send_pending_auth_events(Host, Node, Owner) -> ?DEBUG("Sending pending auth events for ~s on ~s:~s", - [jlib:jid_to_string(Owner), Host, Node]), + [jid:to_string(Owner), Host, Node]), Action = fun (#pubsub_node{id = Nidx, type = Type}) -> case lists:member(<<"get-pending">>, plugin_features(Host, Type)) of true -> @@ -1499,8 +1513,8 @@ send_pending_auth_events(Host, Node, Owner) -> case transaction(Host, Node, Action, sync_dirty) of {result, {N, Subs}} -> lists:foreach(fun - ({J, pending, _SubId}) -> send_authorization_request(N, jlib:make_jid(J)); - ({J, pending}) -> send_authorization_request(N, jlib:make_jid(J)); + ({J, pending, _SubId}) -> send_authorization_request(N, jid:make(J)); + ({J, pending}) -> send_authorization_request(N, jid:make(J)); (_) -> ok end, Subs), @@ -1563,7 +1577,7 @@ send_authorization_request(#pubsub_node{nodeid = {Host, Node}, type = Type, id = [#xmlel{name = <<"value">>, attrs = [], children = - [{xmlcdata, jlib:jid_to_string(Subscriber)}]}]}, + [{xmlcdata, jid:to_string(Subscriber)}]}]}, #xmlel{name = <<"field">>, attrs = [{<<"var">>, @@ -1579,7 +1593,7 @@ send_authorization_request(#pubsub_node{nodeid = {Host, Node}, type = Type, id = children = [{xmlcdata, <<"false">>}]}]}]}]}, lists:foreach(fun (Owner) -> - ejabberd_router:route(service_jid(Host), jlib:make_jid(Owner), Stanza) + ejabberd_router:route(service_jid(Host), jid:make(Owner), Stanza) end, node_owners_action(Host, Type, Nidx, O)). @@ -1587,9 +1601,9 @@ find_authorization_response(Packet) -> #xmlel{children = Els} = Packet, XData1 = lists:map(fun (#xmlel{name = <<"x">>, attrs = XAttrs} = XEl) -> - case xml:get_attr_s(<<"xmlns">>, XAttrs) of + case fxml:get_attr_s(<<"xmlns">>, XAttrs) of ?NS_XDATA -> - case xml:get_attr_s(<<"type">>, XAttrs) of + case fxml:get_attr_s(<<"type">>, XAttrs) of <<"cancel">> -> none; _ -> jlib:parse_xdata_submit(XEl) end; @@ -1599,7 +1613,7 @@ find_authorization_response(Packet) -> (_) -> none end, - xml:remove_cdata(Els)), + fxml:remove_cdata(Els)), XData = lists:filter(fun (E) -> E /= none end, XData1), case XData of [invalid] -> @@ -1624,7 +1638,7 @@ send_authorization_approval(Host, JID, SNode, Subscription) -> [{<<"subscription">>, subscription_to_string(S)}] end, Stanza = event_stanza(<<"subscription">>, - [{<<"jid">>, jlib:jid_to_string(JID)} + [{<<"jid">>, jid:to_string(JID)} | nodeAttr(SNode)] ++ SubAttrs), ejabberd_router:route(service_jid(Host), JID, Stanza). @@ -1637,8 +1651,8 @@ handle_authorization_response(Host, From, To, Packet, XFields) -> {{value, {_, [Node]}}, {value, {_, [SSubscriber]}}, {value, {_, [SAllow]}}} -> - FromLJID = jlib:jid_tolower(jlib:jid_remove_resource(From)), - Subscriber = jlib:string_to_jid(SSubscriber), + FromLJID = jid:tolower(jid:remove_resource(From)), + Subscriber = jid:from_string(SSubscriber), Allow = case SAllow of <<"1">> -> true; <<"true">> -> true; @@ -1765,6 +1779,20 @@ update_auth(Host, Node, Type, Nidx, Subscriber, Allow, Subs) -> %%<li>nodetree create_node checks if nodeid already exists</li> %%<li>node plugin create_node just sets default affiliation/subscription</li> %%</ul> +-spec(create_node/5 :: + ( + Host :: mod_pubsub:host(), + ServerHost :: binary(), + Node :: <<>> | mod_pubsub:nodeId(), + Owner :: jid(), + Type :: binary()) + -> {result, [xmlel(),...]} + %%% + | {error, xmlel()} + ). +create_node(Host, ServerHost, Node, Owner, Type) -> + create_node(Host, ServerHost, Node, Owner, Type, all, []). + -spec(create_node/7 :: ( Host :: mod_pubsub:host(), @@ -1778,8 +1806,6 @@ update_auth(Host, Node, Type, Nidx, Subscriber, Allow, Subs) -> %%% | {error, xmlel()} ). -create_node(Host, ServerHost, Node, Owner, Type) -> - create_node(Host, ServerHost, Node, Owner, Type, all, []). create_node(Host, ServerHost, <<>>, Owner, Type, Access, Configuration) -> case lists:member(<<"instant-nodes">>, plugin_features(Host, Type)) of true -> @@ -1798,7 +1824,7 @@ create_node(Host, ServerHost, <<>>, Owner, Type, Access, Configuration) -> end; create_node(Host, ServerHost, Node, Owner, GivenType, Access, Configuration) -> Type = select_type(ServerHost, Host, Node, GivenType), - ParseOptions = case xml:remove_cdata(Configuration) of + ParseOptions = case fxml:remove_cdata(Configuration) of [] -> {result, node_options(Host, Type)}; [#xmlel{name = <<"x">>} = XEl] -> @@ -2001,6 +2027,21 @@ subscribe_node(Host, Node, From, JID, Configuration) -> AccessModel = get_option(Options, access_model), SendLast = get_option(Options, send_last_published_item), AllowedGroups = get_option(Options, roster_groups_allowed, []), + CanSubscribe = case get_max_subscriptions_node(Host) of + Max when is_integer(Max) -> + case node_call(Host, Type, get_node_subscriptions, [Nidx]) of + {result, NodeSubs} -> + SubsNum = lists:foldl( + fun ({_, subscribed, _}, Acc) -> Acc+1; + (_, Acc) -> Acc + end, 0, NodeSubs), + SubsNum < Max; + _ -> + true + end; + _ -> + true + end, if not SubscribeFeature -> {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, <<"subscribe">>)}; @@ -2013,6 +2054,10 @@ subscribe_node(Host, Node, From, JID, Configuration) -> SubOpts == invalid -> {error, extended_error(?ERR_BAD_REQUEST, <<"invalid-options">>)}; + not CanSubscribe -> + %% fallback to closest XEP compatible result, assume we are not allowed to subscribe + {error, + extended_error(?ERR_NOT_ALLOWED, <<"closed-node">>)}; true -> Owners = node_owners_call(Host, Type, Nidx, O), {PS, RG} = get_presence_and_roster_permissions(Host, Subscriber, @@ -2034,7 +2079,7 @@ subscribe_node(Host, Node, From, JID, Configuration) -> [#xmlel{name = <<"pubsub">>, attrs = [{<<"xmlns">>, ?NS_PUBSUB}], children = [#xmlel{name = <<"subscription">>, - attrs = [{<<"jid">>, jlib:jid_to_string(Subscriber)} + attrs = [{<<"jid">>, jid:to_string(Subscriber)} | SubAttrs]}]}] end, case transaction(Host, Node, Action, sync_dirty) of @@ -2365,7 +2410,10 @@ purge_node(Host, Node, Owner) -> ). get_items(Host, Node, From, SubId, SMaxItems, ItemIds, RSM) -> MaxItems = if SMaxItems == <<>> -> - get_max_items_node(Host); + case get_max_items_node(Host) of + undefined -> ?MAXITEMS; + Max -> Max + end; true -> case catch jlib:binary_to_integer(SMaxItems) of {'EXIT', _} -> {error, ?ERR_BAD_REQUEST}; @@ -2439,7 +2487,7 @@ get_item(Host, Node, ItemId) -> get_allowed_items_call(Host, Nidx, From, Type, Options, Owners) -> case get_allowed_items_call(Host, Nidx, From, Type, Options, Owners, none) of - {result, {I, none}} -> {result, I}; + {result, {Items, _RSM}} -> {result, Items}; Error -> Error end. get_allowed_items_call(Host, Nidx, From, Type, Options, Owners, RSM) -> @@ -2488,18 +2536,18 @@ send_items(Host, Node, Nidx, Type, Options, LJID, last) -> undefined -> ok; LastItem -> - Stanza = items_event_stanza(Node, [LastItem]), - dispatch_items(Host, LJID, Node, Options, Stanza) + Stanza = items_event_stanza(Node, Options, [LastItem]), + dispatch_items(Host, LJID, Node, Stanza) end; send_items(Host, Node, Nidx, Type, Options, LJID, Number) when Number > 0 -> - Stanza = items_event_stanza(Node, get_last_items(Host, Type, Nidx, Number, LJID)), - dispatch_items(Host, LJID, Node, Options, Stanza); + Stanza = items_event_stanza(Node, Options, get_last_items(Host, Type, Nidx, Number, LJID)), + dispatch_items(Host, LJID, Node, Stanza); send_items(Host, Node, _Nidx, _Type, Options, LJID, _) -> - Stanza = items_event_stanza(Node, []), - dispatch_items(Host, LJID, Node, Options, Stanza). + Stanza = items_event_stanza(Node, Options, []), + dispatch_items(Host, LJID, Node, Stanza). -dispatch_items({FromU, FromS, FromR} = From, {ToU, ToS, ToR} = To, Node, - Options, Stanza) -> +dispatch_items({FromU, FromS, FromR} = From, {ToU, ToS, ToR} = To, + Node, Stanza) -> C2SPid = case ejabberd_sm:get_session_pid(ToU, ToS, ToR) of ToPid when is_pid(ToPid) -> ToPid; _ -> @@ -2511,17 +2559,13 @@ dispatch_items({FromU, FromS, FromR} = From, {ToU, ToS, ToR} = To, Node, end, if C2SPid == undefined -> ok; true -> - NotificationType = get_option(Options, notification_type, headline), - Message = add_message_type(Stanza, NotificationType), ejabberd_c2s:send_filtered(C2SPid, {pep_message, <<Node/binary, "+notify">>}, - service_jid(From), jlib:make_jid(To), - Message) + service_jid(From), jid:make(To), + Stanza) end; -dispatch_items(From, To, _Node, Options, Stanza) -> - NotificationType = get_option(Options, notification_type, headline), - Message = add_message_type(Stanza, NotificationType), - ejabberd_router:route(service_jid(From), jlib:make_jid(To), Message). +dispatch_items(From, To, _Node, Stanza) -> + ejabberd_router:route(service_jid(From), jid:make(To), Stanza). %% @doc <p>Return the list of affiliations as an XMPP response.</p> -spec(get_affiliations/4 :: @@ -2609,7 +2653,7 @@ get_affiliations(Host, Node, JID) -> []; ({AJID, Aff}) -> [#xmlel{name = <<"affiliation">>, - attrs = [{<<"jid">>, jlib:jid_to_string(AJID)}, + attrs = [{<<"jid">>, jid:to_string(AJID)}, {<<"affiliation">>, affiliation_to_string(Aff)}]}] end, Affs), @@ -2633,17 +2677,17 @@ get_affiliations(Host, Node, JID) -> | {error, xmlel()} ). set_affiliations(Host, Node, From, EntitiesEls) -> - Owner = jlib:jid_tolower(jlib:jid_remove_resource(From)), + Owner = jid:tolower(jid:remove_resource(From)), Entities = lists:foldl(fun (_, error) -> error; (El, Acc) -> case El of #xmlel{name = <<"affiliation">>, attrs = Attrs} -> - JID = jlib:string_to_jid(xml:get_attr_s(<<"jid">>, Attrs)), - Affiliation = string_to_affiliation(xml:get_attr_s(<<"affiliation">>, Attrs)), + JID = jid:from_string(fxml:get_attr_s(<<"jid">>, Attrs)), + Affiliation = string_to_affiliation(fxml:get_attr_s(<<"affiliation">>, Attrs)), if (JID == error) or (Affiliation == false) -> error; - true -> [{jlib:jid_tolower(JID), Affiliation} | Acc] + true -> [{jid:tolower(JID), Affiliation} | Acc] end end end, @@ -2656,7 +2700,7 @@ set_affiliations(Host, Node, From, EntitiesEls) -> Owners = node_owners_call(Host, Type, Nidx, O), case lists:member(Owner, Owners) of true -> - OwnerJID = jlib:make_jid(Owner), + OwnerJID = jid:make(Owner), FilteredEntities = case Owners of [Owner] -> [E || E <- Entities, element(1, E) =/= OwnerJID]; _ -> Entities @@ -2665,13 +2709,13 @@ set_affiliations(Host, Node, From, EntitiesEls) -> node_call(Host, Type, set_affiliation, [Nidx, JID, Affiliation]), case Affiliation of owner -> - NewOwner = jlib:jid_tolower(jlib:jid_remove_resource(JID)), + NewOwner = jid:tolower(jid:remove_resource(JID)), NewOwners = [NewOwner | Owners], tree_call(Host, set_node, [N#pubsub_node{owners = NewOwners}]); none -> - OldOwner = jlib:jid_tolower(jlib:jid_remove_resource(JID)), + OldOwner = jid:tolower(jid:remove_resource(JID)), case lists:member(OldOwner, Owners) of true -> NewOwners = Owners -- [OldOwner], @@ -2745,7 +2789,7 @@ read_sub(Host, Node, Nidx, Subscriber, SubId, Lang) -> [XdataEl] end, OptionsEl = #xmlel{name = <<"options">>, - attrs = [{<<"jid">>, jlib:jid_to_string(Subscriber)}, + attrs = [{<<"jid">>, jid:to_string(Subscriber)}, {<<"subid">>, SubId} | nodeAttr(Node)], children = Children}, @@ -2821,7 +2865,7 @@ get_subscriptions(Host, Node, JID, Plugins) when is_list(Plugins) -> unsupported, <<"retrieve-subscriptions">>)}, Acc}; true -> - Subscriber = jlib:jid_remove_resource(JID), + Subscriber = jid:remove_resource(JID), {result, Subs} = node_action(Host, Type, get_entity_subscriptions, [Host, Subscriber]), @@ -2855,14 +2899,14 @@ get_subscriptions(Host, Node, JID, Plugins) when is_list(Plugins) -> <<>> -> [#xmlel{name = <<"subscription">>, attrs = - [{<<"jid">>, jlib:jid_to_string(SubJID)}, + [{<<"jid">>, jid:to_string(SubJID)}, {<<"subid">>, SubId}, {<<"subscription">>, subscription_to_string(Sub)} | nodeAttr(SubsNode)]}]; SubsNode -> [#xmlel{name = <<"subscription">>, attrs = - [{<<"jid">>, jlib:jid_to_string(SubJID)}, + [{<<"jid">>, jid:to_string(SubJID)}, {<<"subid">>, SubId}, {<<"subscription">>, subscription_to_string(Sub)}]}]; _ -> @@ -2873,13 +2917,13 @@ get_subscriptions(Host, Node, JID, Plugins) when is_list(Plugins) -> <<>> -> [#xmlel{name = <<"subscription">>, attrs = - [{<<"jid">>, jlib:jid_to_string(SubJID)}, + [{<<"jid">>, jid:to_string(SubJID)}, {<<"subscription">>, subscription_to_string(Sub)} | nodeAttr(SubsNode)]}]; SubsNode -> [#xmlel{name = <<"subscription">>, attrs = - [{<<"jid">>, jlib:jid_to_string(SubJID)}, + [{<<"jid">>, jid:to_string(SubJID)}, {<<"subscription">>, subscription_to_string(Sub)}]}]; _ -> [] @@ -2919,12 +2963,12 @@ get_subscriptions(Host, Node, JID) -> ({AJID, Sub}) -> [#xmlel{name = <<"subscription">>, attrs = - [{<<"jid">>, jlib:jid_to_string(AJID)}, + [{<<"jid">>, jid:to_string(AJID)}, {<<"subscription">>, subscription_to_string(Sub)}]}]; ({AJID, Sub, SubId}) -> [#xmlel{name = <<"subscription">>, attrs = - [{<<"jid">>, jlib:jid_to_string(AJID)}, + [{<<"jid">>, jid:to_string(AJID)}, {<<"subscription">>, subscription_to_string(Sub)}, {<<"subid">>, SubId}]}] end, @@ -2963,18 +3007,18 @@ get_subscriptions_for_send_last(_Host, _PType, _, _JID, _LJID, _BJID) -> []. set_subscriptions(Host, Node, From, EntitiesEls) -> - Owner = jlib:jid_tolower(jlib:jid_remove_resource(From)), + Owner = jid:tolower(jid:remove_resource(From)), Entities = lists:foldl(fun (_, error) -> error; (El, Acc) -> case El of #xmlel{name = <<"subscription">>, attrs = Attrs} -> - JID = jlib:string_to_jid(xml:get_attr_s(<<"jid">>, Attrs)), - Sub = string_to_subscription(xml:get_attr_s(<<"subscription">>, Attrs)), - SubId = xml:get_attr_s(<<"subid">>, Attrs), + JID = jid:from_string(fxml:get_attr_s(<<"jid">>, Attrs)), + Sub = string_to_subscription(fxml:get_attr_s(<<"subscription">>, Attrs)), + SubId = fxml:get_attr_s(<<"subid">>, Attrs), if (JID == error) or (Sub == false) -> error; - true -> [{jlib:jid_tolower(JID), Sub, SubId} | Acc] + true -> [{jid:tolower(JID), Sub, SubId} | Acc] end end end, @@ -2990,10 +3034,10 @@ set_subscriptions(Host, Node, From, EntitiesEls) -> attrs = [{<<"xmlns">>, ?NS_PUBSUB}], children = [#xmlel{name = <<"subscription">>, - attrs = [{<<"jid">>, jlib:jid_to_string(JID)}, + attrs = [{<<"jid">>, jid:to_string(JID)}, {<<"subscription">>, subscription_to_string(Sub)} | nodeAttr(Node)]}]}]}, - ejabberd_router:route(service_jid(Host), jlib:make_jid(JID), Stanza) + ejabberd_router:route(service_jid(Host), jid:make(JID), Stanza) end, Action = fun (#pubsub_node{type = Type, id = Nidx, owners = O}) -> Owners = node_owners_call(Host, Type, Nidx, O), @@ -3065,7 +3109,7 @@ get_roster_info(OwnerUser, OwnerServer, {SubscriberUser, SubscriberServer, _}, A Groups), {PresenceSubscription, RosterGroup}; get_roster_info(OwnerUser, OwnerServer, JID, AllowedGroups) -> - get_roster_info(OwnerUser, OwnerServer, jlib:jid_tolower(JID), AllowedGroups). + get_roster_info(OwnerUser, OwnerServer, jid:tolower(JID), AllowedGroups). string_to_affiliation(<<"owner">>) -> owner; string_to_affiliation(<<"publisher">>) -> publisher; @@ -3130,7 +3174,7 @@ sub_option_can_deliver(nodes, _, {subscription_type, items}) -> false; sub_option_can_deliver(_, _, {subscription_depth, all}) -> true; sub_option_can_deliver(_, Depth, {subscription_depth, D}) -> Depth =< D; sub_option_can_deliver(_, _, {deliver, false}) -> false; -sub_option_can_deliver(_, _, {expire, When}) -> now() < When; +sub_option_can_deliver(_, _, {expire, When}) -> p1_time_compat:timestamp() < When; sub_option_can_deliver(_, _, _) -> true. -spec(presence_can_deliver/2 :: @@ -3142,17 +3186,15 @@ sub_option_can_deliver(_, _, _) -> true. presence_can_deliver(_, false) -> true; presence_can_deliver({User, Server, Resource}, true) -> - case mnesia:dirty_match_object({session, '_', '_', {User, Server}, '_', '_'}) of + case ejabberd_sm:get_user_present_resources(User, Server) of [] -> false; Ss -> lists:foldl(fun (_, true) -> true; - ({session, _, _, _, undefined, _}, _Acc) -> - false; - ({session, {_, _, R}, _, _, _Priority, _}, _Acc) -> - case Resource of + ({_, R}, _Acc) -> + case Resource of <<>> -> true; R -> true; _ -> false @@ -3225,23 +3267,25 @@ payload_xmlelements([#xmlel{} | Tail], Count) -> payload_xmlelements([_ | Tail], Count) -> payload_xmlelements(Tail, Count). -items_event_stanza(Node, Items) -> +items_event_stanza(Node, Options, Items) -> MoreEls = case Items of [LastItem] -> {ModifNow, ModifUSR} = LastItem#pubsub_item.modification, DateTime = calendar:now_to_datetime(ModifNow), {T_string, Tz_string} = jlib:timestamp_to_iso(DateTime, utc), [#xmlel{name = <<"delay">>, attrs = [{<<"xmlns">>, ?NS_DELAY}, - {<<"from">>, jlib:jid_to_string(ModifUSR)}, + {<<"from">>, jid:to_string(ModifUSR)}, {<<"stamp">>, <<T_string/binary, Tz_string/binary>>}], children = [{xmlcdata, <<>>}]}]; _ -> [] end, - event_stanza_with_els([#xmlel{name = <<"items">>, - attrs = [{<<"type">>, <<"headline">>} | nodeAttr(Node)], + BaseStanza = event_stanza_with_els([#xmlel{name = <<"items">>, + attrs = nodeAttr(Node), children = itemsEls(Items)}], - MoreEls). + MoreEls), + NotificationType = get_option(Options, notification_type, headline), + add_message_type(BaseStanza, NotificationType). event_stanza(Els) -> event_stanza_with_els(Els, []). @@ -3264,9 +3308,14 @@ broadcast_publish_item(Host, Node, Nidx, Type, NodeOptions, ItemId, From, Payloa true -> Payload; false -> [] end, + Attrs = case get_option(NodeOptions, itemreply, none) of + owner -> itemAttr(ItemId); %% owner not supported + publisher -> itemAttr(ItemId, {<<"publisher">>, jid:to_string(From)}); + none -> itemAttr(ItemId) + end, Stanza = event_stanza( [#xmlel{name = <<"items">>, attrs = nodeAttr(Node), - children = [#xmlel{name = <<"item">>, attrs = itemAttr(ItemId), + children = [#xmlel{name = <<"item">>, attrs = Attrs, children = Content}]}]), broadcast_stanza(Host, From, Node, Nidx, Type, NodeOptions, SubsByDepth, items, Stanza, true), @@ -3392,12 +3441,19 @@ get_node_subs_by_depth(Host, Node, From) -> [{Depth, [{N, get_node_subs(Host, N)} || N <- Nodes]} || {Depth, Nodes} <- ParentTree]. get_node_subs(Host, #pubsub_node{type = Type, id = Nidx}) -> + WithOptions = lists:member(<<"subscription-options">>, plugin_features(Host, Type)), case node_call(Host, Type, get_node_subscriptions, [Nidx]) of - {result, Subs} -> get_options_for_subs(Host, Nidx, Subs); + {result, Subs} -> get_options_for_subs(Host, Nidx, Subs, WithOptions); Other -> Other end. -get_options_for_subs(Host, Nidx, Subs) -> +get_options_for_subs(_Host, _Nidx, Subs, false) -> + lists:foldl(fun({JID, subscribed, SubID}, Acc) -> + [{JID, SubID, []} | Acc]; + (_, Acc) -> + Acc + end, [], Subs); +get_options_for_subs(Host, Nidx, Subs, true) -> SubModule = subscription_plugin(Host), lists:foldl(fun({JID, subscribed, SubID}, Acc) -> case SubModule:get_subscription(JID, Nidx, SubID) of @@ -3434,7 +3490,7 @@ broadcast_stanza(Host, _Node, _Nidx, _Type, NodeOptions, SubsByDepth, NotifyType add_shim_headers(Stanza, subid_shim(SubIDs)) end, lists:foreach(fun(To) -> - ejabberd_router:route(From, jlib:make_jid(To), StanzaToSend) + ejabberd_router:route(From, jid:make(To), StanzaToSend) end, LJIDs) end, SubIDsByJID). @@ -3451,9 +3507,9 @@ broadcast_stanza({LUser, LServer, LResource}, Publisher, Node, Nidx, Type, NodeO %% See XEP-0163 1.1 section 4.3.1 ejabberd_c2s:broadcast(C2SPid, {pep_message, <<((Node))/binary, "+notify">>}, - _Sender = jlib:make_jid(LUser, LServer, <<"">>), + _Sender = jid:make(LUser, LServer, <<"">>), _StanzaToSend = add_extended_headers(Stanza, - _ReplyTo = extended_headers([jlib:jid_to_string(Publisher)]))); + _ReplyTo = extended_headers([jid:to_string(Publisher)]))); _ -> ?DEBUG("~p@~p has no session; can't deliver ~p to contacts", [LUser, LServer, BaseStanza]) end; @@ -3469,10 +3525,9 @@ subscribed_nodes_by_jid(NotifyType, SubsByDepth) -> NodeOptions = Node#pubsub_node.options, lists:foldl(fun({LJID, SubID, SubOptions}, {JIDs, Recipients}) -> case is_to_deliver(LJID, NotifyType, Depth, NodeOptions, SubOptions) of - true -> - %% If is to deliver : + true -> case state_can_deliver(LJID, SubOptions) of - [] -> {JIDs, Recipients}; + [] -> {JIDs, Recipients}; JIDsToDeliver -> lists:foldl( fun(JIDToDeliver, {JIDsAcc, RecipientsAcc}) -> @@ -3483,11 +3538,14 @@ subscribed_nodes_by_jid(NotifyType, SubsByDepth) -> %% - add the Jid to JIDs list co-accumulator ; %% - create a tuple of the Jid, Nidx, and SubID (as list), %% and add the tuple to the Recipients list co-accumulator - {[JIDToDeliver | JIDsAcc], [{JIDToDeliver, NodeName, [SubID]} | RecipientsAcc]}; + {[JIDToDeliver | JIDsAcc], + [{JIDToDeliver, NodeName, [SubID]} + | RecipientsAcc]}; true -> %% - if the JIDs co-accumulator contains the Jid %% get the tuple containing the Jid from the Recipient list co-accumulator - {_, {JIDToDeliver, NodeName1, SubIDs}} = lists:keysearch(JIDToDeliver, 1, RecipientsAcc), + {_, {JIDToDeliver, NodeName1, SubIDs}} = + lists:keysearch(JIDToDeliver, 1, RecipientsAcc), %% delete the tuple from the Recipients list % v1 : Recipients1 = lists:keydelete(LJID, 1, Recipients), % v2 : Recipients1 = lists:keyreplace(LJID, 1, Recipients, {LJID, Nidx1, [SubID | SubIDs]}), @@ -3496,7 +3554,11 @@ subscribed_nodes_by_jid(NotifyType, SubsByDepth) -> % v1.1 : {JIDs, lists:append(Recipients1, [{LJID, Nidx1, lists:append(SubIDs, [SubID])}])} % v1.2 : {JIDs, [{LJID, Nidx1, [SubID | SubIDs]} | Recipients1]} % v2: {JIDs, Recipients1} - {JIDsAcc, lists:keyreplace(JIDToDeliver, 1, RecipientsAcc, {JIDToDeliver, NodeName1, [SubID | SubIDs]})} + {JIDsAcc, + lists:keyreplace(JIDToDeliver, 1, + RecipientsAcc, + {JIDToDeliver, NodeName1, + [SubID | SubIDs]})} end end, {JIDs, Recipients}, JIDsToDeliver) end; @@ -3583,6 +3645,12 @@ get_option(Options, Var, Def) -> end. node_options(Host, Type) -> + case config(Host, default_node_config) of + undefined -> node_plugin_options(Host, Type); + [] -> node_plugin_options(Host, Type); + Config -> Config + end. +node_plugin_options(Host, Type) -> Module = plugin(Host, Type), case catch Module:options() of {'EXIT', {undef, _}} -> @@ -3591,6 +3659,11 @@ node_options(Host, Type) -> Result -> Result end. +filter_node_options(Options) -> + lists:foldl(fun({Key, Val}, Acc) -> + DefaultValue = proplists:get_value(Key, Options, Val), + [{Key, DefaultValue}|Acc] + end, [], node_flat:options()). node_owners_action(Host, Type, Nidx, []) -> case gen_mod:db_type(serverhost(Host), ?MODULE) of @@ -3633,9 +3706,9 @@ max_items(Host, Options) -> case get_option(Options, persist_items) of true -> case get_option(Options, max_items) of - false -> unlimited; - Result when Result < 0 -> 0; - Result -> Result + I when is_integer(I), I < 0 -> 0; + I when is_integer(I) -> I; + _ -> ?MAXITEMS end; false -> case get_option(Options, send_last_published_item) of @@ -3662,13 +3735,13 @@ max_items(Host, Options) -> -define(INTEGER_CONFIG_FIELD(Label, Var), ?STRINGXFIELD(Label, <<"pubsub#", (atom_to_binary(Var, latin1))/binary>>, - (integer_to_binary(get_option(Options, Var))))). + (jlib:integer_to_binary(get_option(Options, Var))))). -define(JLIST_CONFIG_FIELD(Label, Var, Opts), ?LISTXFIELD(Label, <<"pubsub#", (atom_to_binary(Var, latin1))/binary>>, - (jlib:jid_to_string(get_option(Options, Var))), - [jlib:jid_to_string(O) || O <- Opts])). + (jid:to_string(get_option(Options, Var))), + [jid:to_string(O) || O <- Opts])). -define(ALIST_CONFIG_FIELD(Label, Var, Opts), ?LISTXFIELD(Label, @@ -3723,7 +3796,9 @@ get_configure_xfields(_Type, Options, Lang, Groups) -> ?BOOL_CONFIG_FIELD(<<"Only deliver notifications to available users">>, presence_based_delivery), ?NLIST_CONFIG_FIELD(<<"The collections with which a node is affiliated">>, - collection)]. + collection), + ?ALIST_CONFIG_FIELD(<<"Whether owners or publisher should receive replies to items">>, + itemreply, [none, owner, publisher])]. %%<p>There are several reasons why the node configuration request might fail:</p> %%<ul> @@ -3734,9 +3809,9 @@ get_configure_xfields(_Type, Options, Lang, Groups) -> %%<li>The specified node does not exist.</li> %%</ul> set_configure(Host, Node, From, Els, Lang) -> - case xml:remove_cdata(Els) of + case fxml:remove_cdata(Els) of [#xmlel{name = <<"x">>} = XEl] -> - case {xml:get_tag_attr_s(<<"xmlns">>, XEl), xml:get_tag_attr_s(<<"type">>, XEl)} of + case {fxml:get_tag_attr_s(<<"xmlns">>, XEl), fxml:get_tag_attr_s(<<"type">>, XEl)} of {?NS_XDATA, <<"cancel">>} -> {result, []}; {?NS_XDATA, <<"submit">>} -> @@ -3806,8 +3881,12 @@ add_opt(Key, Value, Opts) -> -define(SET_INTEGER_XOPT(Opt, Val, Min, Max), case catch jlib:binary_to_integer(Val) of - IVal when is_integer(IVal), IVal >= Min, IVal =< Max -> - set_xoption(Host, Opts, add_opt(Opt, IVal, NewOpts)); + IVal when is_integer(IVal), IVal >= Min -> + if (Max =:= undefined) orelse (IVal =< Max) -> + set_xoption(Host, Opts, add_opt(Opt, IVal, NewOpts)); + true -> + {error, ?ERR_NOT_ACCEPTABLE} + end; _ -> {error, ?ERR_NOT_ACCEPTABLE} end). @@ -3873,27 +3952,28 @@ set_xoption(Host, [{<<"pubsub#collection">>, Value} | Opts], NewOpts) -> set_xoption(Host, [{<<"pubsub#node">>, [Value]} | Opts], NewOpts) -> % NewValue = string_to_node(Value), ?SET_LIST_XOPT(node, Value); +set_xoption(Host, [{<<"pubsub#itemreply">>, [Val]} | Opts], NewOpts) -> + ?SET_ALIST_XOPT(itemreply, Val, [none, owner, publisher]); set_xoption(Host, [_ | Opts], NewOpts) -> set_xoption(Host, Opts, NewOpts). -get_max_items_node({_, ServerHost, _}) -> - get_max_items_node(ServerHost); get_max_items_node(Host) -> - config(serverhost(Host), max_items_node, ?MAXITEMS). + config(Host, max_items_node, undefined). + +get_max_subscriptions_node(Host) -> + config(Host, max_subscriptions_node, undefined). %%%% last item cache handling -is_last_item_cache_enabled({_, ServerHost, _}) -> - is_last_item_cache_enabled(ServerHost); is_last_item_cache_enabled(Host) -> - config(serverhost(Host), last_item_cache, false). + config(Host, last_item_cache, false). set_cached_item({_, ServerHost, _}, Nidx, ItemId, Publisher, Payload) -> set_cached_item(ServerHost, Nidx, ItemId, Publisher, Payload); set_cached_item(Host, Nidx, ItemId, Publisher, Payload) -> case is_last_item_cache_enabled(Host) of true -> mnesia:dirty_write({pubsub_last_item, Nidx, ItemId, - {now(), jlib:jid_tolower(jlib:jid_remove_resource(Publisher))}, + {p1_time_compat:timestamp(), jid:tolower(jid:remove_resource(Publisher))}, Payload}); _ -> ok end. @@ -3936,23 +4016,19 @@ get_cached_item(Host, Nidx) -> host(ServerHost) -> config(ServerHost, host, <<"pubsub.", ServerHost/binary>>). -serverhost({_U, Server, _R})-> - Server; +serverhost({_U, ServerHost, _R})-> + serverhost(ServerHost); serverhost(Host) -> - case binary:match(Host, <<"pubsub.">>) of - {0,7} -> - [_,ServerHost] = binary:split(Host, <<".">>), - ServerHost; - _ -> - Host - end. + ejabberd_router:host_of_route(Host). tree(Host) -> - case config(serverhost(Host), nodetree) of + case config(Host, nodetree) of undefined -> tree(Host, ?STDTREE); Tree -> Tree end. +tree(_Host, <<"virtual">>) -> + nodetree_virtual; % special case, virtual does not use any backend tree(Host, Name) -> case gen_mod:db_type(serverhost(Host), ?MODULE) of mnesia -> jlib:binary_to_atom(<<"nodetree_", Name/binary>>); @@ -3968,7 +4044,7 @@ plugin(Host, Name) -> end. plugins(Host) -> - case config(serverhost(Host), plugins) of + case config(Host, plugins) of undefined -> [?STDNODE]; [] -> [?STDNODE]; Plugins -> Plugins @@ -3983,6 +4059,9 @@ subscription_plugin(Host) -> config(ServerHost, Key) -> config(ServerHost, Key, undefined). + +config({_User, Host, _Resource}, Key, Default) -> + config(Host, Key, Default); config(ServerHost, Key, Default) -> case catch ets:lookup(gen_mod:get_module_proc(ServerHost, config), Key) of [{Key, Value}] -> Value; @@ -3999,14 +4078,14 @@ select_type(ServerHost, Host, Node, Type) -> _ -> Type end, - ConfiguredTypes = plugins(ServerHost), + ConfiguredTypes = plugins(Host), case lists:member(SelectedType, ConfiguredTypes) of true -> SelectedType; false -> hd(ConfiguredTypes) end. select_type(ServerHost, Host, Node) -> - select_type(ServerHost, Host, Node, hd(plugins(ServerHost))). + select_type(ServerHost, Host, Node, hd(plugins(Host))). feature(<<"rsm">>) -> ?NS_RSM; feature(Feature) -> <<(?NS_PUBSUB)/binary, "#", Feature/binary>>. @@ -4189,11 +4268,11 @@ extended_error(#xmlel{name = Error, attrs = Attrs, children = SubEls}, Ext, ExtA children = lists:reverse([#xmlel{name = Ext, attrs = ExtAttrs} | SubEls])}. string_to_ljid(JID) -> - case jlib:string_to_jid(JID) of + case jid:from_string(JID) of error -> {<<>>, <<>>, <<>>}; J -> - case jlib:jid_tolower(J) of + case jid:tolower(J) of error -> {<<>>, <<>>, <<>>}; J1 -> J1 end @@ -4201,13 +4280,14 @@ string_to_ljid(JID) -> -spec(uniqid/0 :: () -> mod_pubsub:itemId()). uniqid() -> - {T1, T2, T3} = now(), + {T1, T2, T3} = p1_time_compat:timestamp(), iolist_to_binary(io_lib:fwrite("~.16B~.16B~.16B", [T1, T2, T3])). nodeAttr(Node) -> [{<<"node">>, Node}]. itemAttr([]) -> []; itemAttr(ItemId) -> [{<<"id">>, ItemId}]. +itemAttr(ItemId, From) -> [{<<"id">>, ItemId}, From]. itemsEls(Items) -> [#xmlel{name = <<"item">>, attrs = itemAttr(ItemId), children = Payload} @@ -4262,7 +4342,7 @@ extended_headers(Jids) -> || Jid <- Jids]. on_user_offline(_, JID, _) -> - {User, Server, Resource} = jlib:jid_tolower(JID), + {User, Server, Resource} = jid:tolower(JID), case user_resources(User, Server) of [] -> purge_offline({User, Server, Resource}); _ -> true @@ -4343,3 +4423,30 @@ purge_offline(Host, LJID, Node) -> Error -> Error end. + +mod_opt_type(access_createnode) -> + fun (A) when is_atom(A) -> A end; +mod_opt_type(db_type) -> fun gen_mod:v_db/1; +mod_opt_type(host) -> fun iolist_to_binary/1; +mod_opt_type(ignore_pep_from_offline) -> + fun (A) when is_boolean(A) -> A end; +mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; +mod_opt_type(last_item_cache) -> + fun (A) when is_boolean(A) -> A end; +mod_opt_type(max_items_node) -> + fun (A) when is_integer(A) andalso A >= 0 -> A end; +mod_opt_type(max_subscriptions_node) -> + fun (A) when is_integer(A) andalso A >= 0 -> A end; +mod_opt_type(default_node_config) -> + fun (A) when is_list(A) -> A end; +mod_opt_type(nodetree) -> + fun (A) when is_binary(A) -> A end; +mod_opt_type(pep_mapping) -> + fun (A) when is_list(A) -> A end; +mod_opt_type(plugins) -> + fun (A) when is_list(A) -> A end; +mod_opt_type(_) -> + [access_createnode, db_type, host, + ignore_pep_from_offline, iqdisc, last_item_cache, + max_items_node, nodetree, pep_mapping, plugins, + max_subscriptions_node, default_node_config]. diff --git a/src/mod_register.erl b/src/mod_register.erl index cd68af93..56c5f720 100644 --- a/src/mod_register.erl +++ b/src/mod_register.erl @@ -5,7 +5,7 @@ %%% Created : 8 Dec 2002 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,14 +25,19 @@ -module(mod_register). +-behaviour(ejabberd_config). + -author('alexey@process-one.net'). +-protocol({xep, 77, '2.4'}). + -behaviour(gen_mod). -export([start/2, stop/1, stream_feature_register/2, unauthenticated_iq_register/4, try_register/5, process_iq/3, send_registration_notifications/3, - transform_options/1, transform_module_options/1]). + transform_options/1, transform_module_options/1, + mod_opt_type/1, opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -67,11 +72,19 @@ stop(Host) -> gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_REGISTER). -stream_feature_register(Acc, _Host) -> - [#xmlel{name = <<"register">>, - attrs = [{<<"xmlns">>, ?NS_FEATURE_IQREGISTER}], - children = []} - | Acc]. +stream_feature_register(Acc, Host) -> + AF = gen_mod:get_module_opt(Host, ?MODULE, access_from, + fun(A) when is_atom(A) -> A end, + all), + case (AF /= none) and lists:keymember(<<"mechanisms">>, 2, Acc) of + true -> + [#xmlel{name = <<"register">>, + attrs = [{<<"xmlns">>, ?NS_FEATURE_IQREGISTER}], + children = []} + | Acc]; + false -> + Acc + end. unauthenticated_iq_register(_Acc, Server, #iq{xmlns = ?NS_REGISTER} = IQ, IP) -> @@ -79,19 +92,19 @@ unauthenticated_iq_register(_Acc, Server, {A, _Port} -> A; _ -> undefined end, - ResIQ = process_iq(jlib:make_jid(<<"">>, <<"">>, + ResIQ = process_iq(jid:make(<<"">>, <<"">>, <<"">>), - jlib:make_jid(<<"">>, Server, <<"">>), IQ, Address), - Res1 = jlib:replace_from_to(jlib:make_jid(<<"">>, + jid:make(<<"">>, Server, <<"">>), IQ, Address), + Res1 = jlib:replace_from_to(jid:make(<<"">>, Server, <<"">>), - jlib:make_jid(<<"">>, <<"">>, <<"">>), + jid:make(<<"">>, <<"">>, <<"">>), jlib:iq_to_xml(ResIQ)), jlib:remove_attr(<<"to">>, Res1); unauthenticated_iq_register(Acc, _Server, _IQ, _IP) -> Acc. process_iq(From, To, IQ) -> - process_iq(From, To, IQ, jlib:jid_tolower(From)). + process_iq(From, To, IQ, jid:tolower(From)). process_iq(From, To, #iq{type = Type, lang = Lang, sub_el = SubEl, id = ID} = @@ -108,9 +121,9 @@ process_iq(From, To, end, case Type of set -> - UTag = xml:get_subtag(SubEl, <<"username">>), - PTag = xml:get_subtag(SubEl, <<"password">>), - RTag = xml:get_subtag(SubEl, <<"remove">>), + UTag = fxml:get_subtag(SubEl, <<"username">>), + PTag = fxml:get_subtag(SubEl, <<"password">>), + RTag = fxml:get_subtag(SubEl, <<"remove">>), Server = To#jid.lserver, Access = gen_mod:get_module_opt(Server, ?MODULE, access, fun(A) when is_atom(A) -> A end, @@ -119,14 +132,14 @@ process_iq(From, To, acl:match_rule(Server, Access, From), if (UTag /= false) and (RTag /= false) and AllowRemove -> - User = xml:get_tag_cdata(UTag), + User = fxml:get_tag_cdata(UTag), case From of #jid{user = User, lserver = Server} -> ejabberd_auth:remove_user(User, Server), IQ#iq{type = result, sub_el = []}; _ -> if PTag /= false -> - Password = xml:get_tag_cdata(PTag), + Password = fxml:get_tag_cdata(PTag), case ejabberd_auth:remove_user(User, Server, Password) of @@ -161,9 +174,9 @@ process_iq(From, To, resource = Resource} -> ResIQ = #iq{type = result, xmlns = ?NS_REGISTER, id = ID, sub_el = []}, - ejabberd_router:route(jlib:make_jid(User, Server, + ejabberd_router:route(jid:make(User, Server, Resource), - jlib:make_jid(User, Server, + jid:make(User, Server, Resource), jlib:iq_to_xml(ResIQ)), ejabberd_auth:remove_user(User, Server), @@ -172,8 +185,8 @@ process_iq(From, To, IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]} end; (UTag /= false) and (PTag /= false) -> - User = xml:get_tag_cdata(UTag), - Password = xml:get_tag_cdata(PTag), + User = fxml:get_tag_cdata(UTag), + Password = fxml:get_tag_cdata(PTag), try_register_or_set_password(User, Server, Password, From, IQ, SubEl, Source, Lang, not IsCaptchaEnabled); @@ -275,7 +288,7 @@ process_iq(From, To, [#xmlel{name = <<"query">>, attrs = [{<<"xmlns">>, - <<"jabber:iq:register">>}], + ?NS_REGISTER}], children = [TopInstrEl | CaptchaEls]}]}; {error, limit} -> @@ -297,7 +310,7 @@ process_iq(From, To, [#xmlel{name = <<"query">>, attrs = [{<<"xmlns">>, - <<"jabber:iq:register">>}], + ?NS_REGISTER}], children = [#xmlel{name = <<"instructions">>, attrs = [], @@ -363,10 +376,10 @@ try_set_password(User, Server, Password, IQ, SubEl, end. try_register(User, Server, Password, SourceRaw, Lang) -> - case jlib:is_nodename(User) of + case jid:is_nodename(User) of false -> {error, ?ERR_BAD_REQUEST}; _ -> - JID = jlib:make_jid(User, Server, <<"">>), + JID = jid:make(User, Server, <<"">>), Access = gen_mod:get_module_opt(Server, ?MODULE, access, fun(A) when is_atom(A) -> A end, all), @@ -398,6 +411,8 @@ try_register(User, Server, Password, SourceRaw, Lang) -> {error, ?ERR_JID_MALFORMED}; {error, not_allowed} -> {error, ?ERR_NOT_ALLOWED}; + {error, too_many_users} -> + {error, ?ERR_NOT_ALLOWED}; {error, _Reason} -> {error, ?ERR_INTERNAL_SERVER_ERROR} end @@ -429,7 +444,7 @@ send_welcome_message(JID) -> of {<<"">>, <<"">>} -> ok; {Subj, Body} -> - ejabberd_router:route(jlib:make_jid(<<"">>, Host, + ejabberd_router:route(jid:make(<<"">>, Host, <<"">>), JID, #xmlel{name = <<"message">>, @@ -451,7 +466,7 @@ send_registration_notifications(Mod, UJID, Source) -> case gen_mod:get_module_opt( Host, Mod, registration_watchers, fun(Ss) -> - [#jid{} = jlib:string_to_jid(iolist_to_binary(S)) + [#jid{} = jid:from_string(iolist_to_binary(S)) || S <- Ss] end, []) of [] -> ok; @@ -460,13 +475,13 @@ send_registration_notifications(Mod, UJID, Source) -> iolist_to_binary(io_lib:format("[~s] The account ~s was registered from " "IP address ~s on node ~w using ~p.", [get_time_string(), - jlib:jid_to_string(UJID), + jid:to_string(UJID), ip_to_string(Source), node(), Mod])), lists:foreach( fun(JID) -> ejabberd_router:route( - jlib:make_jid(<<"">>, Host, <<"">>), + jid:make(<<"">>, Host, <<"">>), JID, #xmlel{name = <<"message">>, attrs = [{<<"type">>, <<"chat">>}], @@ -497,8 +512,7 @@ check_timeout(Source) -> infinity end, 600), if is_integer(Timeout) -> - {MSec, Sec, _USec} = now(), - Priority = -(MSec * 1000000 + Sec), + Priority = -p1_time_compat:system_time(seconds), CleanPriority = Priority + Timeout, F = fun () -> Treap = case mnesia:read(mod_register_ip, treap, write) @@ -585,7 +599,7 @@ write_time({{Y, Mo, D}, {H, Mi, S}}) -> [Y, Mo, D, H, Mi, S]). process_xdata_submit(El) -> - case xml:get_subtag(El, <<"x">>) of + case fxml:get_subtag(El, <<"x">>) of false -> error; Xdata -> Fields = jlib:parse_xdata_submit(Xdata), @@ -598,7 +612,7 @@ process_xdata_submit(El) -> end. is_strong_password(Server, Password) -> - LServer = jlib:nameprep(Server), + LServer = jid:nameprep(Server), case gen_mod:get_module_opt(LServer, ?MODULE, password_strength, fun(N) when is_number(N), N>=0 -> N end, 0) of @@ -661,7 +675,7 @@ transform_module_options(Opts) -> %%% may_remove_resource({_, _, _} = From) -> - jlib:jid_remove_resource(From); + jid:remove_resource(From); may_remove_resource(From) -> From. get_ip_access(Host) -> @@ -680,3 +694,37 @@ check_ip_access(undefined, _IPAccess) -> deny; check_ip_access(IPAddress, IPAccess) -> acl:match_rule(global, IPAccess, IPAddress). + +mod_opt_type(access) -> + fun (A) when is_atom(A) -> A end; +mod_opt_type(access_from) -> + fun (A) when is_atom(A) -> A end; +mod_opt_type(captcha_protected) -> + fun (B) when is_boolean(B) -> B end; +mod_opt_type(ip_access) -> + fun (A) when is_atom(A) -> A end; +mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; +mod_opt_type(password_strength) -> + fun (N) when is_number(N), N >= 0 -> N end; +mod_opt_type(registration_watchers) -> + fun (Ss) -> + [#jid{} = jid:from_string(iolist_to_binary(S)) + || S <- Ss] + end; +mod_opt_type(welcome_message) -> + fun (Opts) -> + S = proplists:get_value(subject, Opts, <<>>), + B = proplists:get_value(body, Opts, <<>>), + {iolist_to_binary(S), iolist_to_binary(B)} + end; +mod_opt_type(_) -> + [access, access_from, captcha_protected, ip_access, + iqdisc, password_strength, registration_watchers, + welcome_message]. + +opt_type(registration_timeout) -> + fun (TO) when is_integer(TO), TO > 0 -> TO; + (infinity) -> infinity; + (unlimited) -> infinity + end; +opt_type(_) -> [registration_timeout]. diff --git a/src/mod_register_web.erl b/src/mod_register_web.erl index b415d85e..d9314031 100644 --- a/src/mod_register_web.erl +++ b/src/mod_register_web.erl @@ -5,7 +5,7 @@ %%% Created : 4 May 2008 by Badlop <badlop@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -55,7 +55,7 @@ -behaviour(gen_mod). --export([start/2, stop/1, process/2]). +-export([start/2, stop/1, process/2, mod_opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -100,7 +100,7 @@ process([<<"new">>], lang = Lang, host = _HTTPHost}) -> case form_new_post(Q) of {success, ok, {Username, Host, _Password}} -> - Jid = jlib:make_jid(Username, Host, <<"">>), + Jid = jid:make(Username, Host, <<"">>), mod_register:send_registration_notifications(?MODULE, Jid, Ip), Text = (?T(<<"Your Jabber account was successfully " "created.">>)), @@ -113,8 +113,8 @@ process([<<"new">>], end; process([<<"delete">>], #request{method = 'POST', q = Q, lang = Lang, - host = Host}) -> - case form_del_post(Q, Host) of + host = _HTTPHost}) -> + case form_del_post(Q) of {atomic, ok} -> Text = (?T(<<"Your Jabber account was successfully " "deleted.">>)), @@ -129,8 +129,8 @@ process([<<"delete">>], %% should include the host where the POST was sent. process([<<"change_password">>], #request{method = 'POST', q = Q, lang = Lang, - host = Host}) -> - case form_changepass_post(Q, Host) of + host = _HTTPHost}) -> + case form_changepass_post(Q) of {atomic, ok} -> Text = (?T(<<"The password of your Jabber account " "was successfully changed.">>)), @@ -250,8 +250,8 @@ form_new_get(Host, Lang, IP) -> "a Jabber client.">>), ?XCT(<<"li">>, <<"Some Jabber clients can store your password " - "in your computer. Use that feature only " - "if you trust your computer is safe.">>), + "in the computer, but you should do this only " + "in your personal computer for safety reasons.">>), ?XCT(<<"li">>, <<"Memorize your password, or write it " "in a paper placed in a safe place. In " @@ -282,9 +282,9 @@ form_new_post(Q) -> case catch get_register_parameters(Q) of [Username, Host, Password, Password, Id, Key] -> form_new_post(Username, Host, Password, {Id, Key}); - [_Username, _Password, _Password2, false, false] -> + [_Username, _Host, _Password, _Password2, false, false] -> {error, passwords_not_identical}; - [_Username, _Password, _Password2, Id, Key] -> + [_Username, _Host, _Password, _Password2, Id, Key] -> ejabberd_captcha:check_captcha(Id, Key), {error, passwords_not_identical}; _ -> {error, wrong_parameters} @@ -361,7 +361,8 @@ form_changepass_get(Host, Lang) -> ?INPUTS(<<"text">>, <<"username">>, <<"">>, <<"20">>)]), ?XE(<<"li">>, - [?CT(<<"Server:">>), ?C(<<" ">>), ?C(Host)]), + [?CT(<<"Server:">>), ?C(<<" ">>), + ?INPUTS(<<"text">>, <<"host">>, Host, <<"20">>)]), ?XE(<<"li">>, [?CT(<<"Old Password:">>), ?C(<<" ">>), ?INPUTS(<<"password">>, <<"passwordold">>, <<"">>, @@ -386,12 +387,12 @@ form_changepass_get(Host, Lang) -> %%% Formulary change password POST %%%---------------------------------------------------------------------- -form_changepass_post(Q, Host) -> +form_changepass_post(Q) -> case catch get_changepass_parameters(Q) of - [Username, PasswordOld, Password, Password] -> + [Username, Host, PasswordOld, Password, Password] -> try_change_password(Username, Host, PasswordOld, Password); - [_Username, _PasswordOld, _Password, _Password2] -> + [_Username, _Host, _PasswordOld, _Password, _Password2] -> {error, passwords_not_identical}; _ -> {error, wrong_parameters} end. @@ -405,7 +406,7 @@ get_changepass_parameters(Q) -> {value, {_Key, Value}} = lists:keysearch(Key, 1, Q), Value end, - [<<"username">>, <<"passwordold">>, <<"password">>, + [<<"username">>, <<"host">>, <<"passwordold">>, <<"password">>, <<"password2">>]). try_change_password(Username, Host, PasswordOld, @@ -470,7 +471,8 @@ form_del_get(Host, Lang) -> ?INPUTS(<<"text">>, <<"username">>, <<"">>, <<"20">>)]), ?XE(<<"li">>, - [?CT(<<"Server:">>), ?C(<<" ">>), ?C(Host)]), + [?CT(<<"Server:">>), ?C(<<" ">>), + ?INPUTS(<<"text">>, <<"host">>, Host, <<"20">>)]), ?XE(<<"li">>, [?CT(<<"Password:">>), ?C(<<" ">>), ?INPUTS(<<"password">>, <<"password">>, <<"">>, @@ -491,7 +493,7 @@ register_account(Username, Host, Password) -> Access = gen_mod:get_module_opt(Host, mod_register, access, fun(A) when is_atom(A) -> A end, all), - case jlib:make_jid(Username, Host, <<"">>) of + case jid:make(Username, Host, <<"">>) of error -> {error, invalid_jid}; JID -> case acl:match_rule(Host, Access, JID) of @@ -513,9 +515,9 @@ register_account2(Username, Host, Password) -> %%% Formulary delete POST %%%---------------------------------------------------------------------- -form_del_post(Q, Host) -> +form_del_post(Q) -> case catch get_unregister_parameters(Q) of - [Username, Password] -> + [Username, Host, Password] -> try_unregister_account(Username, Host, Password); _ -> {error, wrong_parameters} end. @@ -529,7 +531,7 @@ get_unregister_parameters(Q) -> {value, {_Key, Value}} = lists:keysearch(Key, 1, Q), Value end, - [<<"username">>, <<"password">>]). + [<<"username">>, <<"host">>, <<"password">>]). try_unregister_account(Username, Host, Password) -> try unregister_account(Username, Host, Password) of @@ -574,3 +576,5 @@ get_error_text({error, passwords_not_identical}) -> <<"The passwords are different">>; get_error_text({error, wrong_parameters}) -> <<"Wrong parameters in the web formulary">>. + +mod_opt_type(_) -> []. diff --git a/src/mod_roster.erl b/src/mod_roster.erl index 2f5d771c..997544b1 100644 --- a/src/mod_roster.erl +++ b/src/mod_roster.erl @@ -5,7 +5,7 @@ %%% Created : 11 Dec 2002 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -34,19 +34,23 @@ -module(mod_roster). +-protocol({xep, 237, '1.3'}). +-protocol({xep, 321, '0.1'}). + -author('alexey@process-one.net'). -behaviour(gen_mod). --export([start/2, stop/1, process_iq/3, export/1, import/1, - process_local_iq/3, get_user_roster/2, import/3, - get_subscription_lists/3, get_roster/2, +-export([start/2, stop/1, process_iq/3, export/1, + import/1, process_local_iq/3, get_user_roster/2, + import/3, get_subscription_lists/3, get_roster/2, get_in_pending_subscriptions/3, in_subscription/6, out_subscription/4, set_items/3, remove_user/2, get_jid_info/4, item_to_xml/1, webadmin_page/3, webadmin_user/4, get_versioning_feature/2, roster_versioning_enabled/1, roster_version/2, - record_to_string/1, groups_to_string/1]). + record_to_string/1, groups_to_string/1, + mod_opt_type/1, set_roster/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -100,8 +104,6 @@ start(Host, Opts) -> webadmin_page, 50), ejabberd_hooks:add(webadmin_user, Host, ?MODULE, webadmin_user, 50), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, - ?NS_ROSTER, ?MODULE, process_iq, IQDisc), gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_ROSTER, ?MODULE, process_iq, IQDisc). @@ -128,7 +130,6 @@ stop(Host) -> webadmin_page, 50), ejabberd_hooks:delete(webadmin_user, Host, ?MODULE, webadmin_user, 50), - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_ROSTER), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_ROSTER). @@ -202,11 +203,9 @@ read_roster_version(LUser, LServer, mnesia) -> [] -> error end; read_roster_version(LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - case odbc_queries:get_roster_version(LServer, Username) - of - {selected, [<<"version">>], [[Version]]} -> Version; - {selected, [<<"version">>], []} -> error + case odbc_queries:get_roster_version(LServer, LUser) of + {selected, [{Version}]} -> Version; + {selected, []} -> error end; read_roster_version(LServer, LUser, riak) -> case ejabberd_riak:get(roster_version, roster_version_schema(), @@ -222,7 +221,7 @@ write_roster_version_t(LUser, LServer) -> write_roster_version(LUser, LServer, true). write_roster_version(LUser, LServer, InTransaction) -> - Ver = p1_sha:sha(term_to_binary(now())), + Ver = p1_sha:sha(term_to_binary(p1_time_compat:unique_integer())), write_roster_version(LUser, LServer, InTransaction, Ver, gen_mod:db_type(LServer, ?MODULE)), Ver. @@ -255,7 +254,7 @@ write_roster_version(LUser, LServer, _InTransaction, Ver, ejabberd_riak:put(#roster_version{us = US, version = Ver}, roster_version_schema()). -%% Load roster from DB only if neccesary. +%% Load roster from DB only if neccesary. %% It is neccesary if %% - roster versioning is disabled in server OR %% - roster versioning is not used by the client OR @@ -266,7 +265,7 @@ process_iq_get(From, To, #iq{sub_el = SubEl} = IQ) -> LServer = From#jid.lserver, US = {LUser, LServer}, try {ItemsToSend, VersionToSend} = case - {xml:get_tag_attr(<<"ver">>, SubEl), + {fxml:get_tag_attr(<<"ver">>, SubEl), roster_versioning_enabled(LServer), roster_version_on_db(LServer)} of @@ -368,51 +367,49 @@ get_roster(LUser, LServer, riak) -> _Err -> [] end; get_roster(LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - case catch odbc_queries:get_roster(LServer, Username) of - {selected, - [<<"username">>, <<"jid">>, <<"nick">>, - <<"subscription">>, <<"ask">>, <<"askmessage">>, - <<"server">>, <<"subscribe">>, <<"type">>], - Items} - when is_list(Items) -> - JIDGroups = case catch - odbc_queries:get_roster_jid_groups(LServer, - Username) - of - {selected, [<<"jid">>, <<"grp">>], JGrps} - when is_list(JGrps) -> - JGrps; - _ -> [] - end, - GroupsDict = lists:foldl(fun ([J, G], Acc) -> - dict:append(J, G, Acc) - end, - dict:new(), JIDGroups), - RItems = lists:flatmap(fun (I) -> - case raw_to_record(LServer, I) of - %% Bad JID in database: - error -> []; - R -> - SJID = - jlib:jid_to_string(R#roster.jid), - Groups = case dict:find(SJID, - GroupsDict) - of - {ok, Gs} -> Gs; - error -> [] - end, - [R#roster{groups = Groups}] - end - end, - Items), - RItems; - _ -> [] + case catch odbc_queries:get_roster(LServer, LUser) of + {selected, Items} when is_list(Items) -> + JIDGroups = case catch odbc_queries:get_roster_jid_groups( + LServer, LUser) of + {selected, JGrps} + when is_list(JGrps) -> + JGrps; + _ -> [] + end, + GroupsDict = lists:foldl(fun({J, G}, Acc) -> + dict:append(J, G, Acc) + end, + dict:new(), JIDGroups), + RItems = + lists:flatmap( + fun(I) -> + case raw_to_record(LServer, I) of + %% Bad JID in database: + error -> []; + R -> + SJID = jid:to_string(R#roster.jid), + Groups = case dict:find(SJID, GroupsDict) of + {ok, Gs} -> Gs; + error -> [] + end, + [R#roster{groups = Groups}] + end + end, + Items), + RItems; + _ -> [] end. +set_roster(#roster{us = {LUser, LServer}, jid = LJID} = Item) -> + transaction( + LServer, + fun() -> + roster_subscribe_t(LUser, LServer, LJID, Item) + end). + item_to_xml(Item) -> Attrs1 = [{<<"jid">>, - jlib:jid_to_string(Item#roster.jid)}], + jid:to_string(Item#roster.jid)}], Attrs2 = case Item#roster.name of <<"">> -> Attrs1; Name -> [{<<"name">>, Name} | Attrs1] @@ -452,14 +449,8 @@ get_roster_by_jid_t(LUser, LServer, LJID, mnesia) -> xs = []} end; get_roster_by_jid_t(LUser, LServer, LJID, odbc) -> - Username = ejabberd_odbc:escape(LUser), - SJID = ejabberd_odbc:escape(jlib:jid_to_string(LJID)), - {selected, - [<<"username">>, <<"jid">>, <<"nick">>, - <<"subscription">>, <<"ask">>, <<"askmessage">>, - <<"server">>, <<"subscribe">>, <<"type">>], - Res} = - odbc_queries:get_roster_by_jid(LServer, Username, SJID), + {selected, Res} = + odbc_queries:get_roster_by_jid(LServer, LUser, jid:to_string(LJID)), case Res of [] -> #roster{usj = {LUser, LServer, LJID}, @@ -508,14 +499,14 @@ process_iq_set(From, To, #iq{sub_el = SubEl, id = Id} = IQ) -> process_item_set(From, To, #xmlel{attrs = Attrs, children = Els}, Managed) -> - JID1 = jlib:string_to_jid(xml:get_attr_s(<<"jid">>, + JID1 = jid:from_string(fxml:get_attr_s(<<"jid">>, Attrs)), #jid{user = User, luser = LUser, lserver = LServer} = From, case JID1 of error -> ok; _ -> - LJID = jlib:jid_tolower(JID1), + LJID = jid:tolower(JID1), F = fun () -> Item = get_roster_by_jid_t(LUser, LServer, LJID), Item1 = process_item_attrs_managed(Item, Attrs, Managed), @@ -551,7 +542,7 @@ process_item_set(_From, _To, _, _Managed) -> ok. process_item_attrs(Item, [{Attr, Val} | Attrs]) -> case Attr of <<"jid">> -> - case jlib:string_to_jid(Val) of + case jid:from_string(Val) of error -> process_item_attrs(Item, Attrs); JID1 -> JID = {JID1#jid.luser, JID1#jid.lserver, @@ -577,10 +568,10 @@ process_item_els(Item, | Els]) -> case Name of <<"group">> -> - Groups = [xml:get_cdata(SEls) | Item#roster.groups], + Groups = [fxml:get_cdata(SEls) | Item#roster.groups], process_item_els(Item#roster{groups = Groups}, Els); _ -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of + case fxml:get_attr_s(<<"xmlns">>, Attrs) of <<"">> -> process_item_els(Item, Els); _ -> XEls = [#xmlel{name = Name, attrs = Attrs, @@ -594,8 +585,8 @@ process_item_els(Item, [{xmlcdata, _} | Els]) -> process_item_els(Item, []) -> Item. push_item(User, Server, From, Item) -> - ejabberd_sm:route(jlib:make_jid(<<"">>, <<"">>, <<"">>), - jlib:make_jid(User, Server, <<"">>), + ejabberd_sm:route(jid:make(<<"">>, <<"">>, <<"">>), + jid:make(User, Server, <<"">>), {broadcast, {item, Item#roster.jid, Item#roster.subscription}}), case roster_versioning_enabled(Server) of @@ -628,7 +619,7 @@ push_item(User, Server, Resource, From, Item, attrs = [{<<"xmlns">>, ?NS_ROSTER} | ExtraAttrs], children = [item_to_xml(Item)]}]}, ejabberd_router:route(From, - jlib:make_jid(User, Server, Resource), + jid:make(User, Server, Resource), jlib:iq_to_xml(ResIQ)). push_item_version(Server, User, From, Item, @@ -640,8 +631,8 @@ push_item_version(Server, User, From, Item, ejabberd_sm:get_user_resources(User, Server)). get_subscription_lists(Acc, User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), DBType = gen_mod:db_type(LServer, ?MODULE), Items = get_subscription_lists(Acc, LUser, LServer, DBType), @@ -654,14 +645,8 @@ get_subscription_lists(_, LUser, LServer, mnesia) -> _ -> [] end; get_subscription_lists(_, LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - case catch odbc_queries:get_roster(LServer, Username) of - {selected, - [<<"username">>, <<"jid">>, <<"nick">>, - <<"subscription">>, <<"ask">>, <<"askmessage">>, - <<"server">>, <<"subscribe">>, <<"type">>], - Items} - when is_list(Items) -> + case catch odbc_queries:get_roster(LServer, LUser) of + {selected, Items} when is_list(Items) -> lists:map(fun(I) -> raw_to_record(LServer, I) end, Items); _ -> [] end; @@ -703,12 +688,9 @@ roster_subscribe_t(LUser, LServer, LJID, Item) -> roster_subscribe_t(_LUser, _LServer, _LJID, Item, mnesia) -> mnesia:write(Item); -roster_subscribe_t(LUser, LServer, LJID, Item, odbc) -> - ItemVals = record_to_string(Item), - Username = ejabberd_odbc:escape(LUser), - SJID = ejabberd_odbc:escape(jlib:jid_to_string(LJID)), - odbc_queries:roster_subscribe(LServer, Username, SJID, - ItemVals); +roster_subscribe_t(_LUser, _LServer, _LJID, Item, odbc) -> + ItemVals = record_to_row(Item), + odbc_queries:roster_subscribe(ItemVals); roster_subscribe_t(LUser, LServer, _LJID, Item, riak) -> ejabberd_riak:put(Item, roster_schema(), [{'2i', [{<<"us">>, {LUser, LServer}}]}]). @@ -742,30 +724,18 @@ get_roster_by_jid_with_groups_t(LUser, LServer, LJID, end; get_roster_by_jid_with_groups_t(LUser, LServer, LJID, odbc) -> - Username = ejabberd_odbc:escape(LUser), - SJID = ejabberd_odbc:escape(jlib:jid_to_string(LJID)), - case odbc_queries:get_roster_by_jid(LServer, Username, - SJID) - of - {selected, - [<<"username">>, <<"jid">>, <<"nick">>, - <<"subscription">>, <<"ask">>, <<"askmessage">>, - <<"server">>, <<"subscribe">>, <<"type">>], - [I]} -> - R = raw_to_record(LServer, I), - Groups = case odbc_queries:get_roster_groups(LServer, - Username, SJID) - of - {selected, [<<"grp">>], JGrps} when is_list(JGrps) -> - [JGrp || [JGrp] <- JGrps]; - _ -> [] - end, - R#roster{groups = Groups}; - {selected, - [<<"username">>, <<"jid">>, <<"nick">>, - <<"subscription">>, <<"ask">>, <<"askmessage">>, - <<"server">>, <<"subscribe">>, <<"type">>], - []} -> + SJID = jid:to_string(LJID), + case odbc_queries:get_roster_by_jid(LServer, LUser, SJID) of + {selected, [I]} -> + R = raw_to_record(LServer, I), + Groups = + case odbc_queries:get_roster_groups(LServer, LUser, SJID) of + {selected, JGrps} when is_list(JGrps) -> + [JGrp || {JGrp} <- JGrps]; + _ -> [] + end, + R#roster{groups = Groups}; + {selected, []} -> #roster{usj = {LUser, LServer, LJID}, us = {LUser, LServer}, jid = LJID} end; @@ -782,9 +752,9 @@ get_roster_by_jid_with_groups_t(LUser, LServer, LJID, riak) -> process_subscription(Direction, User, Server, JID1, Type, Reason) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - LJID = jlib:jid_tolower(JID1), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + LJID = jid:tolower(JID1), F = fun () -> Item = get_roster_by_jid_with_groups_t(LUser, LServer, LJID), @@ -835,7 +805,7 @@ process_subscription(Direction, User, Server, JID1, subscribed -> <<"subscribed">>; unsubscribed -> <<"unsubscribed">> end, - ejabberd_router:route(jlib:make_jid(User, Server, + ejabberd_router:route(jid:make(User, Server, <<"">>), JID1, #xmlel{name = <<"presence">>, @@ -849,7 +819,7 @@ process_subscription(Direction, User, Server, JID1, ok; true -> push_item(User, Server, - jlib:make_jid(User, Server, <<"">>), Item) + jid:make(User, Server, <<"">>), Item) end, true; none -> false @@ -973,8 +943,8 @@ in_auto_reply(both, none, unsubscribe) -> unsubscribed; in_auto_reply(_, _, _) -> none. remove_user(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), send_unsubscription_to_rosteritems(LUser, LServer), remove_user(LUser, LServer, gen_mod:db_type(LServer, ?MODULE)). @@ -987,8 +957,7 @@ remove_user(LUser, LServer, mnesia) -> end, mnesia:transaction(F); remove_user(LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - odbc_queries:del_user_roster_t(LServer, Username), + odbc_queries:del_user_roster_t(LServer, LUser), ok; remove_user(LUser, LServer, riak) -> {atomic, ejabberd_riak:delete_by_index(roster, <<"us">>, {LUser, LServer})}. @@ -998,13 +967,12 @@ remove_user(LUser, LServer, riak) -> %% Both or To, send a "unsubscribe" presence stanza. send_unsubscription_to_rosteritems(LUser, LServer) -> RosterItems = get_user_roster([], {LUser, LServer}), - From = jlib:make_jid({LUser, LServer, <<"">>}), + From = jid:make({LUser, LServer, <<"">>}), lists:foreach(fun (RosterItem) -> send_unsubscribing_presence(From, RosterItem) end, RosterItems). -%% @spec (From::jid(), Item::roster()) -> ok send_unsubscribing_presence(From, Item) -> IsTo = case Item#roster.subscription of both -> true; @@ -1017,14 +985,14 @@ send_unsubscribing_presence(From, Item) -> _ -> false end, if IsTo -> - send_presence_type(jlib:jid_remove_resource(From), - jlib:make_jid(Item#roster.jid), + send_presence_type(jid:remove_resource(From), + jid:make(Item#roster.jid), <<"unsubscribe">>); true -> ok end, if IsFrom -> - send_presence_type(jlib:jid_remove_resource(From), - jlib:make_jid(Item#roster.jid), + send_presence_type(jid:remove_resource(From), + jid:make(Item#roster.jid), <<"unsubscribed">>); true -> ok end, @@ -1039,8 +1007,8 @@ send_presence_type(From, To, Type) -> set_items(User, Server, SubEl) -> #xmlel{children = Els} = SubEl, - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), F = fun () -> lists:foreach(fun (El) -> process_item_set_t(LUser, LServer, El) @@ -1057,11 +1025,10 @@ update_roster_t(_LUser, _LServer, _LJID, Item, mnesia) -> mnesia:write(Item); update_roster_t(LUser, LServer, LJID, Item, odbc) -> - Username = ejabberd_odbc:escape(LUser), - SJID = ejabberd_odbc:escape(jlib:jid_to_string(LJID)), - ItemVals = record_to_string(Item), - ItemGroups = groups_to_string(Item), - odbc_queries:update_roster(LServer, Username, SJID, ItemVals, + SJID = jid:to_string(LJID), + ItemVals = record_to_row(Item), + ItemGroups = Item#roster.groups, + odbc_queries:update_roster(LServer, LUser, SJID, ItemVals, ItemGroups); update_roster_t(LUser, LServer, _LJID, Item, riak) -> ejabberd_riak:put(Item, roster_schema(), @@ -1074,15 +1041,14 @@ del_roster_t(LUser, LServer, LJID) -> del_roster_t(LUser, LServer, LJID, mnesia) -> mnesia:delete({roster, {LUser, LServer, LJID}}); del_roster_t(LUser, LServer, LJID, odbc) -> - Username = ejabberd_odbc:escape(LUser), - SJID = ejabberd_odbc:escape(jlib:jid_to_string(LJID)), - odbc_queries:del_roster(LServer, Username, SJID); + SJID = jid:to_string(LJID), + odbc_queries:del_roster(LServer, LUser, SJID); del_roster_t(LUser, LServer, LJID, riak) -> ejabberd_riak:delete(roster, {LUser, LServer, LJID}). process_item_set_t(LUser, LServer, #xmlel{attrs = Attrs, children = Els}) -> - JID1 = jlib:string_to_jid(xml:get_attr_s(<<"jid">>, + JID1 = jid:from_string(fxml:get_attr_s(<<"jid">>, Attrs)), case JID1 of error -> ok; @@ -1105,7 +1071,7 @@ process_item_set_t(_LUser, _LServer, _) -> ok. process_item_attrs_ws(Item, [{Attr, Val} | Attrs]) -> case Attr of <<"jid">> -> - case jlib:string_to_jid(Val) of + case jid:from_string(Val) of error -> process_item_attrs_ws(Item, Attrs); JID1 -> JID = {JID1#jid.luser, JID1#jid.lserver, @@ -1140,13 +1106,13 @@ process_item_attrs_ws(Item, [{Attr, Val} | Attrs]) -> process_item_attrs_ws(Item, []) -> Item. get_in_pending_subscriptions(Ls, User, Server) -> - LServer = jlib:nameprep(Server), + LServer = jid:nameprep(Server), get_in_pending_subscriptions(Ls, User, Server, gen_mod:db_type(LServer, ?MODULE)). get_in_pending_subscriptions(Ls, User, Server, DBType) when DBType == mnesia; DBType == riak -> - JID = jlib:make_jid(User, Server, <<"">>), + JID = jid:make(User, Server, <<"">>), Result = get_roster(JID#jid.luser, JID#jid.lserver, DBType), Ls ++ lists:map(fun (R) -> Message = R#roster.askmessage, @@ -1156,8 +1122,8 @@ get_in_pending_subscriptions(Ls, User, Server, DBType) #xmlel{name = <<"presence">>, attrs = [{<<"from">>, - jlib:jid_to_string(R#roster.jid)}, - {<<"to">>, jlib:jid_to_string(JID)}, + jid:to_string(R#roster.jid)}, + {<<"to">>, jid:to_string(JID)}, {<<"type">>, <<"subscribe">>}], children = [#xmlel{name = <<"status">>, @@ -1174,25 +1140,19 @@ get_in_pending_subscriptions(Ls, User, Server, DBType) end, Result)); get_in_pending_subscriptions(Ls, User, Server, odbc) -> - JID = jlib:make_jid(User, Server, <<"">>), + JID = jid:make(User, Server, <<"">>), LUser = JID#jid.luser, LServer = JID#jid.lserver, - Username = ejabberd_odbc:escape(LUser), - case catch odbc_queries:get_roster(LServer, Username) of - {selected, - [<<"username">>, <<"jid">>, <<"nick">>, - <<"subscription">>, <<"ask">>, <<"askmessage">>, - <<"server">>, <<"subscribe">>, <<"type">>], - Items} - when is_list(Items) -> + case catch odbc_queries:get_roster(LServer, LUser) of + {selected, Items} when is_list(Items) -> Ls ++ lists:map(fun (R) -> Message = R#roster.askmessage, #xmlel{name = <<"presence">>, attrs = [{<<"from">>, - jlib:jid_to_string(R#roster.jid)}, - {<<"to">>, jlib:jid_to_string(JID)}, + jid:to_string(R#roster.jid)}, + {<<"to">>, jid:to_string(JID)}, {<<"type">>, <<"subscribe">>}], children = [#xmlel{name = <<"status">>, @@ -1219,8 +1179,8 @@ get_in_pending_subscriptions(Ls, User, Server, odbc) -> %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% read_subscription_and_groups(User, Server, LJID) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), read_subscription_and_groups(LUser, LServer, LJID, gen_mod:db_type(LServer, ?MODULE)). @@ -1236,12 +1196,9 @@ read_subscription_and_groups(LUser, LServer, LJID, end; read_subscription_and_groups(LUser, LServer, LJID, odbc) -> - Username = ejabberd_odbc:escape(LUser), - SJID = ejabberd_odbc:escape(jlib:jid_to_string(LJID)), - case catch odbc_queries:get_subscription(LServer, - Username, SJID) - of - {selected, [<<"subscription">>], [[SSubscription]]} -> + SJID = jid:to_string(LJID), + case catch odbc_queries:get_subscription(LServer, LUser, SJID) of + {selected, [{SSubscription}]} -> Subscription = case SSubscription of <<"B">> -> both; <<"T">> -> to; @@ -1249,11 +1206,11 @@ read_subscription_and_groups(LUser, LServer, LJID, _ -> none end, Groups = case catch - odbc_queries:get_rostergroup_by_jid(LServer, Username, + odbc_queries:get_rostergroup_by_jid(LServer, LUser, SJID) of - {selected, [<<"grp">>], JGrps} when is_list(JGrps) -> - [JGrp || [JGrp] <- JGrps]; + {selected, JGrps} when is_list(JGrps) -> + [JGrp || {JGrp} <- JGrps]; _ -> [] end, {Subscription, Groups}; @@ -1270,11 +1227,11 @@ read_subscription_and_groups(LUser, LServer, LJID, end. get_jid_info(_, User, Server, JID) -> - LJID = jlib:jid_tolower(JID), + LJID = jid:tolower(JID), case read_subscription_and_groups(User, Server, LJID) of {Subscription, Groups} -> {Subscription, Groups}; error -> - LRJID = jlib:jid_tolower(jlib:jid_remove_resource(JID)), + LRJID = jid:tolower(jid:remove_resource(JID)), if LRJID == LJID -> {none, []}; true -> case read_subscription_and_groups(User, Server, LRJID) @@ -1290,10 +1247,16 @@ get_jid_info(_, User, Server, JID) -> raw_to_record(LServer, [User, SJID, Nick, SSubscription, SAsk, SAskMessage, _SServer, _SSubscribe, _SType]) -> - case jlib:string_to_jid(SJID) of + raw_to_record(LServer, + {User, SJID, Nick, SSubscription, SAsk, SAskMessage, + _SServer, _SSubscribe, _SType}); +raw_to_record(LServer, + {User, SJID, Nick, SSubscription, SAsk, SAskMessage, + _SServer, _SSubscribe, _SType}) -> + case jid:from_string(SJID) of error -> error; JID -> - LJID = jlib:jid_tolower(JID), + LJID = jid:tolower(JID), Subscription = case SSubscription of <<"B">> -> both; <<"T">> -> to; @@ -1319,7 +1282,7 @@ record_to_string(#roster{us = {User, _Server}, ask = Ask, askmessage = AskMessage}) -> Username = ejabberd_odbc:escape(User), SJID = - ejabberd_odbc:escape(jlib:jid_to_string(jlib:jid_tolower(JID))), + ejabberd_odbc:escape(jid:to_string(jid:tolower(JID))), Nick = ejabberd_odbc:escape(Name), SSubscription = case Subscription of both -> <<"B">>; @@ -1339,11 +1302,32 @@ record_to_string(#roster{us = {User, _Server}, [Username, SJID, Nick, SSubscription, SAsk, SAskMessage, <<"N">>, <<"">>, <<"item">>]. +record_to_row( + #roster{us = {LUser, _LServer}, + jid = JID, name = Name, subscription = Subscription, + ask = Ask, askmessage = AskMessage}) -> + SJID = jid:to_string(jid:tolower(JID)), + SSubscription = case Subscription of + both -> <<"B">>; + to -> <<"T">>; + from -> <<"F">>; + none -> <<"N">> + end, + SAsk = case Ask of + subscribe -> <<"S">>; + unsubscribe -> <<"U">>; + both -> <<"B">>; + out -> <<"O">>; + in -> <<"I">>; + none -> <<"N">> + end, + {LUser, SJID, Name, SSubscription, SAsk, AskMessage}. + groups_to_string(#roster{us = {User, _Server}, jid = JID, groups = Groups}) -> Username = ejabberd_odbc:escape(User), SJID = - ejabberd_odbc:escape(jlib:jid_to_string(jlib:jid_tolower(JID))), + ejabberd_odbc:escape(jid:to_string(jid:tolower(JID))), lists:foldl(fun (<<"">>, Acc) -> Acc; (Group, Acc) -> G = ejabberd_odbc:escape(Group), @@ -1383,7 +1367,7 @@ update_roster_table() -> groups = [iolist_to_binary(G) || G <- Gs], askmessage = try iolist_to_binary(Ask) catch _:_ -> <<"">> end, - xs = [xml:to_xmlel(X) || X <- Xs]} + xs = [fxml:to_xmlel(X) || X <- Xs]} end); _ -> ?INFO_MSG("Recreating roster table", []), @@ -1391,7 +1375,7 @@ update_roster_table() -> end. %% Convert roster table to support virtual host -%% Convert roster table: xattrs fields become +%% Convert roster table: xattrs fields become update_roster_version_table() -> Fields = record_info(fields, roster_version), case mnesia:table_info(roster_version, attributes) of @@ -1417,8 +1401,8 @@ webadmin_page(_, Host, webadmin_page(Acc, _, _) -> Acc. user_roster(User, Server, Query, Lang) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), US = {LUser, LServer}, Items1 = get_roster(LUser, LServer), Res = user_roster_parse_query(User, Server, Items1, @@ -1504,7 +1488,7 @@ user_roster(User, Server, Query, Lang) -> <<"Add Jabber ID">>)]))]. build_contact_jid_td(RosterJID) -> - ContactJID = jlib:make_jid(RosterJID), + ContactJID = jid:make(RosterJID), JIDURI = case {ContactJID#jid.luser, ContactJID#jid.lserver} of @@ -1520,10 +1504,10 @@ build_contact_jid_td(RosterJID) -> case JIDURI of <<>> -> ?XAC(<<"td">>, [{<<"class">>, <<"valign">>}], - (jlib:jid_to_string(RosterJID))); + (jid:to_string(RosterJID))); URI when is_binary(URI) -> ?XAE(<<"td">>, [{<<"class">>, <<"valign">>}], - [?AC(JIDURI, (jlib:jid_to_string(RosterJID)))]) + [?AC(JIDURI, (jid:to_string(RosterJID)))]) end. user_roster_parse_query(User, Server, Items, Query) -> @@ -1531,7 +1515,7 @@ user_roster_parse_query(User, Server, Items, Query) -> {value, _} -> case lists:keysearch(<<"newjid">>, 1, Query) of {value, {_, SJID}} -> - case jlib:string_to_jid(SJID) of + case jid:from_string(SJID) of JID when is_record(JID, jid) -> user_roster_subscribe_jid(User, Server, JID), ok; error -> error @@ -1550,7 +1534,7 @@ user_roster_parse_query(User, Server, Items, Query) -> user_roster_subscribe_jid(User, Server, JID) -> out_subscription(User, Server, JID, subscribe), - UJID = jlib:make_jid(User, Server, <<"">>), + UJID = jid:make(User, Server, <<"">>), ejabberd_router:route(UJID, JID, #xmlel{name = <<"presence">>, attrs = [{<<"type">>, <<"subscribe">>}], @@ -1565,10 +1549,10 @@ user_roster_item_parse_query(User, Server, Items, 1, Query) of {value, _} -> - JID1 = jlib:make_jid(JID), + JID1 = jid:make(JID), out_subscription(User, Server, JID1, subscribed), - UJID = jlib:make_jid(User, Server, <<"">>), + UJID = jid:make(User, Server, <<"">>), ejabberd_router:route(UJID, JID1, #xmlel{name = <<"presence">>, @@ -1583,7 +1567,7 @@ user_roster_item_parse_query(User, Server, Items, 1, Query) of {value, _} -> - UJID = jlib:make_jid(User, Server, + UJID = jid:make(User, Server, <<"">>), process_iq_set(UJID, UJID, #iq{type = set, @@ -1600,7 +1584,7 @@ user_roster_item_parse_query(User, Server, Items, attrs = [{<<"jid">>, - jlib:jid_to_string(JID)}, + jid:to_string(JID)}, {<<"subscription">>, <<"remove">>}], children @@ -1615,7 +1599,7 @@ user_roster_item_parse_query(User, Server, Items, nothing. us_to_list({User, Server}) -> - jlib:jid_to_string({User, Server, <<"">>}). + jid:to_string({User, Server, <<"">>}). webadmin_user(Acc, _User, _Server, Lang) -> Acc ++ @@ -1676,7 +1660,7 @@ is_item_of_domain(_MatchDomain, {xmlcdata, _}) -> false. is_jid_of_domain(MatchDomain, {<<"jid">>, JIDString}) -> - case jlib:string_to_jid(JIDString) of + case jid:from_string(JIDString) of JID when JID#jid.lserver == MatchDomain -> true; _ -> false end; @@ -1716,7 +1700,7 @@ export(_Server) -> fun(Host, #roster{usj = {LUser, LServer, LJID}} = R) when LServer == Host -> Username = ejabberd_odbc:escape(LUser), - SJID = ejabberd_odbc:escape(jlib:jid_to_string(LJID)), + SJID = ejabberd_odbc:escape(jid:to_string(LJID)), ItemVals = record_to_string(R), ItemGroups = groups_to_string(R), odbc_queries:update_roster_sql(Username, SJID, @@ -1767,3 +1751,17 @@ import(_LServer, riak, #roster_version{} = RV) -> ejabberd_riak:put(RV, roster_version_schema()); import(_, _, _) -> pass. + +mod_opt_type(access) -> + fun (A) when is_atom(A) -> A end; +mod_opt_type(db_type) -> fun gen_mod:v_db/1; +mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; +mod_opt_type(managers) -> + fun (B) when is_list(B) -> B end; +mod_opt_type(store_current_id) -> + fun (B) when is_boolean(B) -> B end; +mod_opt_type(versioning) -> + fun (B) when is_boolean(B) -> B end; +mod_opt_type(_) -> + [access, db_type, iqdisc, managers, store_current_id, + versioning]. diff --git a/src/mod_service_log.erl b/src/mod_service_log.erl index 7a23b401..3e1da3a4 100644 --- a/src/mod_service_log.erl +++ b/src/mod_service_log.erl @@ -5,7 +5,7 @@ %%% Created : 24 Aug 2003 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -29,10 +29,8 @@ -behaviour(gen_mod). --export([start/2, - stop/1, - log_user_send/3, - log_user_receive/4]). +-export([start/2, stop/1, log_user_send/4, + log_user_receive/5, mod_opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -53,11 +51,13 @@ stop(Host) -> ?MODULE, log_user_receive, 50), ok. -log_user_send(From, To, Packet) -> - log_packet(From, To, Packet, From#jid.lserver). +log_user_send(Packet, _C2SState, From, To) -> + log_packet(From, To, Packet, From#jid.lserver), + Packet. -log_user_receive(_JID, From, To, Packet) -> - log_packet(From, To, Packet, To#jid.lserver). +log_user_receive(Packet, _C2SState, _JID, From, To) -> + log_packet(From, To, Packet, To#jid.lserver), + Packet. log_packet(From, To, #xmlel{name = Name, attrs = Attrs, children = Els}, @@ -67,7 +67,7 @@ log_packet(From, To, lists:map( fun(S) -> B = iolist_to_binary(S), - N = jlib:nameprep(B), + N = jid:nameprep(B), if N /= error -> N end @@ -77,8 +77,8 @@ log_packet(From, To, resource = <<"">>, luser = <<"">>, lserver = Host, lresource = <<"">>}, NewAttrs = - jlib:replace_from_to_attrs(jlib:jid_to_string(From), - jlib:jid_to_string(To), Attrs), + jlib:replace_from_to_attrs(jid:to_string(From), + jid:to_string(To), Attrs), FixedPacket = #xmlel{name = Name, attrs = NewAttrs, children = Els}, lists:foreach(fun (Logger) -> @@ -95,3 +95,14 @@ log_packet(From, To, [FixedPacket]}) end, Loggers). + +mod_opt_type(loggers) -> + fun (L) -> + lists:map(fun (S) -> + B = iolist_to_binary(S), + N = jid:nameprep(B), + if N /= error -> N end + end, + L) + end; +mod_opt_type(_) -> [loggers]. diff --git a/src/mod_shared_roster.erl b/src/mod_shared_roster.erl index 4c3f177e..212a7d47 100644 --- a/src/mod_shared_roster.erl +++ b/src/mod_shared_roster.erl @@ -5,7 +5,7 @@ %%% Created : 5 Mar 2005 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -29,16 +29,17 @@ -behaviour(gen_mod). --export([start/2, stop/1, item_to_xml/1, export/1, import/1, - webadmin_menu/3, webadmin_page/3, get_user_roster/2, - get_subscription_lists/3, get_jid_info/4, import/3, - process_item/2, in_subscription/6, out_subscription/4, - user_available/1, unset_presence/4, register_user/2, - remove_user/2, list_groups/1, create_group/2, - create_group/3, delete_group/2, get_group_opts/2, - set_group_opts/3, get_group_users/2, - get_group_explicit_users/2, is_user_in_group/3, - add_user_to_group/3, remove_user_from_group/3]). +-export([start/2, stop/1, item_to_xml/1, export/1, + import/1, webadmin_menu/3, webadmin_page/3, + get_user_roster/2, get_subscription_lists/3, + get_jid_info/4, import/3, process_item/2, + in_subscription/6, out_subscription/4, user_available/1, + unset_presence/4, register_user/2, remove_user/2, + list_groups/1, create_group/2, create_group/3, + delete_group/2, get_group_opts/2, set_group_opts/3, + get_group_users/2, get_group_explicit_users/2, + is_user_in_group/3, add_user_to_group/3, + remove_user_from_group/3, mod_opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -97,9 +98,6 @@ start(Host, Opts) -> ejabberd_hooks:add(remove_user, Host, ?MODULE, remove_user, 50). -%%ejabberd_hooks:add(remove_user, Host, -%% ?MODULE, remove_user, 50), - stop(Host) -> ejabberd_hooks:delete(webadmin_menu_host, Host, ?MODULE, webadmin_menu, 70), @@ -125,12 +123,11 @@ stop(Host) -> register_user, 50), ejabberd_hooks:delete(anonymous_purge_hook, Host, ?MODULE, remove_user, 50), -%%ejabberd_hooks:delete(remove_user, Host, -%% ?MODULE, remove_user, 50), ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, - 50).%%ejabberd_hooks:delete(remove_user, Host, - %% ?MODULE, remove_user, 50), + 50). + %%ejabberd_hooks:delete(remove_user, Host, + %% ?MODULE, remove_user, 50), get_user_roster(Items, US) -> {U, S} = US, @@ -185,8 +182,8 @@ get_vcard_module(Server) -> get_rosteritem_name([], _, _) -> <<"">>; get_rosteritem_name([ModVcard], U, S) -> - From = jlib:make_jid(<<"">>, S, jlib:atom_to_binary(?MODULE)), - To = jlib:make_jid(U, S, <<"">>), + From = jid:make(<<"">>, S, jlib:atom_to_binary(?MODULE)), + To = jid:make(U, S, <<"">>), case lists:member(To#jid.lserver, ?MYHOSTS) of true -> IQ = {iq, <<"">>, get, <<"vcard-temp">>, <<"">>, @@ -209,11 +206,11 @@ get_rosteritem_name([ModVcard], U, S) -> get_rosteritem_name_vcard([]) -> <<"">>; get_rosteritem_name_vcard([Vcard]) -> - case xml:get_path_s(Vcard, + case fxml:get_path_s(Vcard, [{elem, <<"NICKNAME">>}, cdata]) of <<"">> -> - xml:get_path_s(Vcard, [{elem, <<"FN">>}, cdata]); + fxml:get_path_s(Vcard, [{elem, <<"FN">>}, cdata]); Nickname -> Nickname end. @@ -249,11 +246,11 @@ process_item(RosterItem, Host) -> %% existing roster groups. [] -> mod_roster:out_subscription(UserTo, ServerTo, - jlib:make_jid(UserFrom, ServerFrom, + jid:make(UserFrom, ServerFrom, <<"">>), unsubscribe), mod_roster:in_subscription(aaaa, UserFrom, ServerFrom, - jlib:make_jid(UserTo, ServerTo, + jid:make(UserTo, ServerTo, <<"">>), unsubscribe, <<"">>), RosterItem#roster{subscription = both, ask = none}; @@ -278,8 +275,8 @@ set_new_rosteritems(UserFrom, ServerFrom, UserTo, RIFrom = build_roster_record(UserFrom, ServerFrom, UserTo, ServerTo, NameTo, GroupsFrom), set_item(UserFrom, ServerFrom, ResourceTo, RIFrom), - JIDTo = jlib:make_jid(UserTo, ServerTo, <<"">>), - JIDFrom = jlib:make_jid(UserFrom, ServerFrom, <<"">>), + JIDTo = jid:make(UserTo, ServerTo, <<"">>), + JIDFrom = jid:make(UserFrom, ServerFrom, <<"">>), RITo = build_roster_record(UserTo, ServerTo, UserFrom, ServerFrom, UserFrom, []), set_item(UserTo, ServerTo, <<"">>, RITo), @@ -308,14 +305,14 @@ set_item(User, Server, Resource, Item) -> [#xmlel{name = <<"query">>, attrs = [{<<"xmlns">>, ?NS_ROSTER}], children = [mod_roster:item_to_xml(Item)]}]}, - ejabberd_router:route(jlib:make_jid(User, Server, + ejabberd_router:route(jid:make(User, Server, Resource), - jlib:make_jid(<<"">>, Server, <<"">>), + jid:make(<<"">>, Server, <<"">>), jlib:iq_to_xml(ResIQ)). get_subscription_lists({F, T}, User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), US = {LUser, LServer}, DisplayedGroups = get_user_displayed_groups(US), SRUsers = lists:usort(lists:flatmap(fun (Group) -> @@ -327,10 +324,10 @@ get_subscription_lists({F, T}, User, Server) -> get_jid_info({Subscription, Groups}, User, Server, JID) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), US = {LUser, LServer}, - {U1, S1, _} = jlib:jid_tolower(JID), + {U1, S1, _} = jid:tolower(JID), US1 = {U1, S1}, DisplayedGroups = get_user_displayed_groups(US), SRUsers = lists:foldl(fun (Group, Acc1) -> @@ -360,7 +357,7 @@ in_subscription(Acc, User, Server, JID, Type, out_subscription(UserFrom, ServerFrom, JIDTo, unsubscribed) -> #jid{luser = UserTo, lserver = ServerTo} = JIDTo, - JIDFrom = jlib:make_jid(UserFrom, ServerFrom, <<"">>), + JIDFrom = jid:make(UserFrom, ServerFrom, <<"">>), mod_roster:out_subscription(UserTo, ServerTo, JIDFrom, unsubscribe), mod_roster:in_subscription(aaaa, UserFrom, ServerFrom, @@ -373,11 +370,11 @@ out_subscription(User, Server, JID, Type) -> process_subscription(Direction, User, Server, JID, _Type, Acc) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), US = {LUser, LServer}, {U1, S1, _} = - jlib:jid_tolower(jlib:jid_remove_resource(JID)), + jid:tolower(jid:remove_resource(JID)), US1 = {U1, S1}, DisplayedGroups = get_user_displayed_groups(US), SRUsers = lists:usort(lists:flatmap(fun (Group) -> @@ -621,7 +618,6 @@ get_group_users(Host1, Group1) -> get_group_users(Host, Group, GroupOpts) -> case proplists:get_value(all_users, GroupOpts, false) of -%% @spec (Host::string(), Group::string()) -> [{User::string(), Server::string()}] true -> ejabberd_auth:get_vh_registered_users(Host); false -> [] end @@ -661,7 +657,7 @@ get_group_explicit_users(Host, Group, odbc) -> {selected, [<<"jid">>], Rs} -> lists:map(fun ([JID]) -> {U, S, _} = - jlib:jid_tolower(jlib:string_to_jid(JID)), + jid:tolower(jid:from_string(JID)), {U, S} end, Rs); @@ -674,25 +670,22 @@ get_group_name(Host1, Group1) -> %% Get list of names of groups that have @all@/@online@/etc in the memberlist get_special_users_groups(Host) -> -%% Get list of names of groups that have @online@ in the memberlist lists:filter(fun (Group) -> get_group_opt(Host, Group, all_users, false) orelse get_group_opt(Host, Group, online_users, false) end, list_groups(Host)). +%% Get list of names of groups that have @online@ in the memberlist get_special_users_groups_online(Host) -> -%% Given two lists of groupnames and their options, -%% return the list of displayed groups to the second list lists:filter(fun (Group) -> get_group_opt(Host, Group, online_users, false) end, list_groups(Host)). +%% Given two lists of groupnames and their options, +%% return the list of displayed groups to the second list displayed_groups(GroupsOpts, SelectedGroupsOpts) -> -%% Given a list of group names with options, -%% for those that have @all@ in memberlist, -%% get the list of groups displayed DisplayedGroups = lists:usort(lists:flatmap(fun ({_Group, Opts}) -> [G @@ -711,6 +704,9 @@ displayed_groups(GroupsOpts, SelectedGroupsOpts) -> lists:member(disabled, proplists:get_value(G, GroupsOpts, []))]. +%% Given a list of group names with options, +%% for those that have @all@ in memberlist, +%% get the list of groups displayed get_special_displayed_groups(GroupsOpts) -> Groups = lists:filter(fun ({_Group, Opts}) -> proplists:get_value(all_users, Opts, false) @@ -824,7 +820,7 @@ is_user_in_group(US, Group, Host, odbc) -> %% @spec (Host::string(), {User::string(), Server::string()}, Group::string()) -> {atomic, ok} add_user_to_group(Host, US, Group) -> {LUser, LServer} = US, - case ejabberd_regexp:run(LUser, <<"^@.+@$">>) of + case ejabberd_regexp:run(LUser, <<"^@.+@\$">>) of match -> GroupOpts = (?MODULE):get_group_opts(Host, Group), MoreGroupOpts = case LUser of @@ -883,7 +879,7 @@ push_displayed_to_user(LUser, LServer, Host, Subscription, DisplayedGroups) -> remove_user_from_group(Host, US, Group) -> {LUser, LServer} = US, - case ejabberd_regexp:run(LUser, <<"^@.+@$">>) of + case ejabberd_regexp:run(LUser, <<"^@.+@\$">>) of match -> GroupOpts = (?MODULE):get_group_opts(Host, Group), NewGroupOpts = case LUser of @@ -953,8 +949,8 @@ remove_user(User, Server) -> push_user_to_members(User, Server, remove). push_user_to_members(User, Server, Subscription) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), GroupsOpts = groups_with_opts(LServer), SpecialGroups = get_special_displayed_groups(GroupsOpts), @@ -984,11 +980,11 @@ push_user_to_displayed(LUser, LServer, Group, Host, Subscription, DisplayedToGro GroupName = proplists:get_value(name, GroupOpts, Group), [push_user_to_group(LUser, LServer, GroupD, Host, GroupName, Subscription) - || {GroupD, _Opts} <- DisplayedToGroupsOpts]. + || GroupD <- DisplayedToGroupsOpts]. broadcast_user_to_displayed(LUser, LServer, Host, Subscription, DisplayedToGroupsOpts) -> [broadcast_user_to_group(LUser, LServer, GroupD, Host, Subscription) - || {GroupD, _Opts} <- DisplayedToGroupsOpts]. + || GroupD <- DisplayedToGroupsOpts]. push_user_to_group(LUser, LServer, Group, Host, GroupName, Subscription) -> @@ -1011,12 +1007,13 @@ broadcast_user_to_group(LUser, LServer, Group, Host, Subscription) -> %% Get list of groups to which this group is displayed displayed_to_groups(GroupName, LServer) -> GroupsOpts = groups_with_opts(LServer), - lists:filter(fun ({_Group, Opts}) -> + Gs = lists:filter(fun ({_Group, Opts}) -> lists:member(GroupName, proplists:get_value(displayed_groups, Opts, [])) end, - GroupsOpts). + GroupsOpts), + [Name || {Name, _} <- Gs]. push_item(User, Server, Item) -> Stanza = jlib:iq_to_xml(#iq{type = set, @@ -1027,8 +1024,8 @@ push_item(User, Server, Item) -> attrs = [{<<"xmlns">>, ?NS_ROSTER}], children = [item_to_xml(Item)]}]}), lists:foreach(fun (Resource) -> - JID = jlib:make_jid(User, Server, Resource), - ejabberd_router:route(JID, JID, Stanza) + JID = jid:make(User, Server, Resource), + ejabberd_router:route(jid:remove_resource(JID), JID, Stanza) end, ejabberd_sm:get_user_resources(User, Server)). @@ -1043,7 +1040,7 @@ push_roster_item(User, Server, ContactU, ContactS, item_to_xml(Item) -> Attrs1 = [{<<"jid">>, - jlib:jid_to_string(Item#roster.jid)}], + jid:to_string(Item#roster.jid)}], Attrs2 = case Item#roster.name of <<"">> -> Attrs1; Name -> [{<<"name">>, Name} | Attrs1] @@ -1163,7 +1160,7 @@ list_shared_roster_groups(Host, Query, Lang) -> [?INPUTT(<<"submit">>, <<"addnew">>, <<"Add New">>)])])]))])), (?H1GL((?T(<<"Shared Roster Groups">>)), - <<"modsharedroster">>, <<"mod_shared_roster">>)) + <<"mod_shared_roster">>, <<"mod_shared_roster">>)) ++ case Res of ok -> [?XREST(<<"Submitted">>)]; @@ -1257,7 +1254,7 @@ shared_roster_group(Host, Group, Query, Lang) -> <<"20">>, list_to_binary(FDisplayedGroups))])])])])), (?H1GL((?T(<<"Shared Roster Groups">>)), - <<"modsharedroster">>, <<"mod_shared_roster">>)) + <<"mod_shared_roster">>, <<"mod_shared_roster">>)) ++ [?XC(<<"h2">>, <<(?T(<<"Group ">>))/binary, Group/binary>>)] ++ case Res of @@ -1301,7 +1298,7 @@ shared_roster_group_parse_query(Host, Group, Query) -> <<"@all@">> -> USs; <<"@online@">> -> USs; _ -> - case jlib:string_to_jid(SJID) + case jid:from_string(SJID) of JID when is_record(JID, @@ -1361,7 +1358,7 @@ get_opt(Opts, Opt, Default) -> end. us_to_list({User, Server}) -> - jlib:jid_to_string({User, Server, <<"">>}). + jid:to_string({User, Server, <<"">>}). split_grouphost(Host, Group) -> case str:tokens(Group, <<"@">>) of @@ -1371,8 +1368,8 @@ split_grouphost(Host, Group) -> broadcast_subscription(User, Server, ContactJid, Subscription) -> ejabberd_sm:route( - jlib:make_jid(<<"">>, Server, <<"">>), - jlib:make_jid(User, Server, <<"">>), + jid:make(<<"">>, Server, <<"">>), + jid:make(User, Server, <<"">>), {broadcast, {item, ContactJid, Subscription}}). @@ -1389,7 +1386,7 @@ displayed_groups_update(Members, DisplayedGroups, Subscription) -> end, Members). make_jid_s(U, S) -> - ejabberd_odbc:escape(jlib:jid_to_string(jlib:jid_tolower(jlib:make_jid(U, + ejabberd_odbc:escape(jid:to_string(jid:tolower(jid:make(U, S, <<"">>)))). @@ -1469,9 +1466,9 @@ export(_Server) -> when LServer == Host -> SGroup = ejabberd_odbc:escape(Group), SJID = ejabberd_odbc:escape( - jlib:jid_to_string( - jlib:jid_tolower( - jlib:make_jid(U, S, <<"">>)))), + jid:to_string( + jid:tolower( + jid:make(U, S, <<"">>)))), [[<<"delete from sr_user where jid='">>, SJID, <<"'and grp='">>, Group, <<"';">>], [<<"insert into sr_user(jid, grp) values ('">>, @@ -1488,7 +1485,7 @@ import(LServer) -> end}, {<<"select jid, grp from sr_user;">>, fun([SJID, Group]) -> - #jid{luser = U, lserver = S} = jlib:string_to_jid(SJID), + #jid{luser = U, lserver = S} = jid:from_string(SJID), #sr_user{us = {U, S}, group_host = {Group, LServer}} end}]. @@ -1506,3 +1503,6 @@ import(_LServer, riak, #sr_user{us = US, group_host = {Group, Host}} = User) -> {<<"group_host">>, {Group, Host}}]}]); import(_, _, _) -> pass. + +mod_opt_type(db_type) -> fun gen_mod:v_db/1; +mod_opt_type(_) -> [db_type]. diff --git a/src/mod_shared_roster_ldap.erl b/src/mod_shared_roster_ldap.erl index af85e4d4..dd6a7f6d 100644 --- a/src/mod_shared_roster_ldap.erl +++ b/src/mod_shared_roster_ldap.erl @@ -3,11 +3,12 @@ %%% Author : Realloc <realloc@realloc.spb.ru> %%% Marcin Owsiany <marcin@owsiany.pl> %%% Evgeniy Khramtsov <ekhramtsov@process-one.net> +%%% Contributor : Mike Kaganski <mikekaganski@hotmail.com> %%% Description : LDAP shared roster management %%% Created : 5 Mar 2005 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -39,22 +40,18 @@ -export([get_user_roster/2, get_subscription_lists/3, get_jid_info/4, process_item/2, in_subscription/6, - out_subscription/4]). + out_subscription/4, mod_opt_type/1, opt_type/1]). -include("ejabberd.hrl"). --include("logger.hrl"). -include("jlib.hrl"). -include("mod_roster.hrl"). - -include("eldap.hrl"). +-define(ERROR_MSG(Fmt, Args), error_logger:error_msg(Fmt, Args)). --define(CACHE_SIZE, 1000). - --define(USER_CACHE_VALIDITY, 300). - --define(GROUP_CACHE_VALIDITY, 300). - --define(LDAP_SEARCH_TIMEOUT, 5). +-define(CACHE_SIZE, 1). +-define(CACHE_VALIDITY, 300). %% in seconds +-define(LDAP_SEARCH_TIMEOUT, 5). %% Timeout for LDAP search queries in seconds +-define(INVALID_SETTING_MSG, "~s is not properly set! ~s will not function."). -record(state, {host = <<"">> :: binary(), @@ -72,7 +69,6 @@ group_attr = <<"">> :: binary(), group_desc = <<"">> :: binary(), user_desc = <<"">> :: binary(), - user_uid = <<"">> :: binary(), uid_format = <<"">> :: binary(), uid_format_re = <<"">> :: binary(), filter = <<"">> :: binary(), @@ -80,25 +76,51 @@ rfilter = <<"">> :: binary(), gfilter = <<"">> :: binary(), auth_check = true :: boolean(), - user_cache_size = ?CACHE_SIZE :: non_neg_integer(), - group_cache_size = ?CACHE_SIZE :: non_neg_integer(), - user_cache_validity = ?USER_CACHE_VALIDITY :: non_neg_integer(), - group_cache_validity = ?GROUP_CACHE_VALIDITY :: non_neg_integer()}). - + %% Group data parameters + group_base = <<"">> :: binary(), + %% - Subgroup of roster filter + %% This filter defines which groups are displayed in the shared roster + %% Valid values are 'all' or "LDAP filter string" or "LDAP filter string containing %g" + shgfilter = <<"">> :: binary(), + shg_attr = <<"">> :: binary(), + %% - Subgroup of group filter + group_is_dn = true :: boolean(), + member_attr = <<"">> :: binary(), + %% User data parameters + member_selection_mode = memberattr_dn :: memberattr_normal | memberattr_dn | + group_children, + %% Algorithm control parameters + subscribe_all = false :: binary(), + roster_cache_size = ?CACHE_SIZE :: non_neg_integer(), + roster_cache_validity = ?CACHE_VALIDITY :: non_neg_integer()}). + +%% If #state.member_selection_mode is memberattr_normal or memberattr_dn, +%% then members is list of member_attr values; +%% if #state.member_selection_mode is group_children, +%% then members is dn of the group (to make it possible to search for its subtree) -record(group_info, {desc, members}). +-record(user_info, {us, name}). + +-record(shared_roster_item, {us, name, groups}). + +% Groups visible to this group +% grp may be atom 'all' or a group name string. +% shgrps is a list containing one or more grp +-record(shg_data, {grp, shgrps}). + %%==================================================================== %% API %%==================================================================== start_link(Host, Opts) -> Proc = gen_mod:get_module_proc(Host, ?MODULE), gen_server:start_link({local, Proc}, ?MODULE, - [Host, Opts], []). + [Host, Opts], []). start(Host, Opts) -> Proc = gen_mod:get_module_proc(Host, ?MODULE), ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]}, - permanent, 1000, worker, [?MODULE]}, + permanent, 1000, worker, [?MODULE]}, supervisor:start_child(ejabberd_sup, ChildSpec). stop(Host) -> @@ -110,109 +132,92 @@ stop(Host) -> %% Hooks %%-------------------------------------------------------------------- get_user_roster(Items, {U, S} = US) -> - SRUsers = get_user_to_groups_map(US, true), - {NewItems1, SRUsersRest} = lists:mapfoldl(fun (Item, - SRUsers1) -> - {_, _, {U1, S1, _}} = - Item#roster.usj, - US1 = {U1, S1}, - case dict:find(US1, - SRUsers1) - of - {ok, _GroupNames} -> - {Item#roster{subscription - = - both, - ask = - none}, - dict:erase(US1, - SRUsers1)}; - error -> - {Item, SRUsers1} - end - end, - SRUsers, Items), + {ok, State} = eldap_utils:get_state(S, ?MODULE), + SRUsers = get_shared_roster(State, US), + %%?ERROR_MSG("XXXXXX get_user_roster: SRUsers=~p", [SRUsers]), + %% If partially subscribed users are also in shared roster, + %% show them as totally subscribed: + {NewItems1, SRUsersRest} = lists:mapfoldl( + fun(Item, SRUsers1) -> + {_, _, {U1, S1, _}} = Item#roster.usj, + US1 = {U1, S1}, + case lists:keytake(US1, #shared_roster_item.us, SRUsers1) of + %%case dict:find(US1, SRUsers1) of + {value, _, SRUsers2} -> {Item#roster{subscription = both, ask = none}, SRUsers2}; + %%{ok, _GroupNames} -> {Item#roster{subscription = both, ask = none}, dict:erase(US1, SRUsers1)}; + false -> {Item, SRUsers1} + end + end, + SRUsers, Items), + %% Export items in roster format: SRItems = [#roster{usj = {U, S, {U1, S1, <<"">>}}, - us = US, jid = {U1, S1, <<"">>}, - name = get_user_name(U1, S1), subscription = both, - ask = none, groups = GroupNames} - || {{U1, S1}, GroupNames} <- dict:to_list(SRUsersRest)], + us = US, + jid = {U1, S1, <<"">>}, + name = Name, + subscription = both, + ask = none, + groups = Groups} || + #shared_roster_item{us = {U1, S1}, name = Name, groups = Groups} <- SRUsersRest], + %% SRItems = [#roster{usj = {U, S, {U1, S1, <<"">>}}, + %% us = US, jid = {U1, S1, <<"">>}, + %% name = get_user_name(U1, S1), subscription = both, + %% ask = none, groups = GroupNames} + %% || {{U1, S1}, GroupNames} <- dict:to_list(SRUsersRest)], SRItems ++ NewItems1. %% This function in use to rewrite the roster entries when moving or renaming %% them in the user contact list. process_item(RosterItem, _Host) -> - USFrom = RosterItem#roster.us, - {User, Server, _Resource} = RosterItem#roster.jid, - USTo = {User, Server}, - Map = get_user_to_groups_map(USFrom, false), - case dict:find(USTo, Map) of - error -> RosterItem; - {ok, []} -> RosterItem; - {ok, GroupNames} - when RosterItem#roster.subscription == remove -> - RosterItem#roster{subscription = both, ask = none, - groups = GroupNames}; - _ -> RosterItem#roster{subscription = both, ask = none} + {ok, State} = eldap_utils:get_state(_Host, ?MODULE), + {User,Server,_Resource} = RosterItem#roster.jid, + USTo = {User,Server}, + SR = get_shared_roster(State, RosterItem#roster.us), + case lists:keysearch(USTo, #shared_roster_item.us, SR) of + false -> + RosterItem; + {value, #shared_roster_item{groups = Groups}} when RosterItem#roster.subscription == remove -> + %% Roster item cannot be removed: + %% We simply reset the original groups: + RosterItem#roster{subscription = both, ask = none, + groups=Groups}; + _ -> + RosterItem#roster{subscription = both, ask = none} end. get_subscription_lists({F, T}, User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - US = {LUser, LServer}, - DisplayedGroups = get_user_displayed_groups(US), - SRUsers = lists:usort(lists:flatmap(fun (Group) -> - get_group_users(LServer, Group) - end, - DisplayedGroups)), - SRJIDs = [{U1, S1, <<"">>} || {U1, S1} <- SRUsers], + U = jid:nodeprep(User), + S = jid:nameprep(Server), + {ok, State} = eldap_utils:get_state(S, ?MODULE), + SRJIDs = get_presense_subscribers(State, {U, S}), +%?INFO_MSG("SRJIDs: ~p", [SRJIDs]), {lists:usort(SRJIDs ++ F), lists:usort(SRJIDs ++ T)}. -get_jid_info({Subscription, Groups}, User, Server, - JID) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - US = {LUser, LServer}, - {U1, S1, _} = jlib:jid_tolower(JID), +get_jid_info({Subscription, Groups}, User, Server, JID) -> + {ok, State} = eldap_utils:get_state(Server, ?MODULE), + {U1, S1, _} = jid:tolower(JID), US1 = {U1, S1}, - SRUsers = get_user_to_groups_map(US, false), - case dict:find(US1, SRUsers) of - {ok, GroupNames} -> - NewGroups = if Groups == [] -> GroupNames; - true -> Groups - end, - {both, NewGroups}; - error -> {Subscription, Groups} + SR = get_shared_roster(State, {User, Server}), + case lists:keysearch(US1, #shared_roster_item.us, SR) of + false -> {Subscription, Groups}; + {value, #shared_roster_item{groups = GroupNames}} when Groups == [] -> {both, GroupNames}; + _ -> {both, Groups} end. -in_subscription(Acc, User, Server, JID, Type, - _Reason) -> +in_subscription(Acc, User, Server, JID, Type, _Reason) -> process_subscription(in, User, Server, JID, Type, Acc). out_subscription(User, Server, JID, Type) -> - process_subscription(out, User, Server, JID, Type, - false). - -process_subscription(Direction, User, Server, JID, - _Type, Acc) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), - US = {LUser, LServer}, - {U1, S1, _} = - jlib:jid_tolower(jlib:jid_remove_resource(JID)), + process_subscription(out, User, Server, JID, Type, false). + +process_subscription(Direction, User, Server, JID, _Type, Acc) -> + {ok, State} = eldap_utils:get_state(Server, ?MODULE), + {U1, S1, _} = jid:tolower(JID), US1 = {U1, S1}, - DisplayedGroups = get_user_displayed_groups(US), - SRUsers = lists:usort(lists:flatmap(fun (Group) -> - get_group_users(LServer, Group) - end, - DisplayedGroups)), - case lists:member(US1, SRUsers) of - true -> - case Direction of - in -> {stop, false}; - out -> stop - end; - false -> Acc + SR = get_shared_roster(State, {User, Server}), + case lists:keysearch(US1, #shared_roster_item.us, SR) of + false -> Acc; + _ when Direction == in -> {stop, false}; + _ -> stop end. %%==================================================================== @@ -220,274 +225,509 @@ process_subscription(Direction, User, Server, JID, %%==================================================================== init([Host, Opts]) -> State = parse_options(Host, Opts), - cache_tab:new(shared_roster_ldap_user, - [{max_size, State#state.user_cache_size}, {lru, false}, - {life_time, State#state.user_cache_validity}]), - cache_tab:new(shared_roster_ldap_group, - [{max_size, State#state.group_cache_size}, {lru, false}, - {life_time, State#state.group_cache_validity}]), - ejabberd_hooks:add(roster_get, Host, ?MODULE, - get_user_roster, 70), + if + State#state.roster_cache_size > 0 -> + cache_tab:new(shared_roster_ldap_sr, + [{max_size, State#state.roster_cache_size}, + {lru, false}, % We don't need LRU algorithm + {life_time, State#state.roster_cache_validity}]); + true -> + false + end, + ejabberd_hooks:add(roster_get, Host, + ?MODULE, get_user_roster, 70), ejabberd_hooks:add(roster_in_subscription, Host, - ?MODULE, in_subscription, 30), + ?MODULE, in_subscription, 30), ejabberd_hooks:add(roster_out_subscription, Host, - ?MODULE, out_subscription, 30), + ?MODULE, out_subscription, 30), ejabberd_hooks:add(roster_get_subscription_lists, Host, - ?MODULE, get_subscription_lists, 70), - ejabberd_hooks:add(roster_get_jid_info, Host, ?MODULE, - get_jid_info, 70), - ejabberd_hooks:add(roster_process_item, Host, ?MODULE, - process_item, 50), + ?MODULE, get_subscription_lists, 70), + ejabberd_hooks:add(roster_get_jid_info, Host, + ?MODULE, get_jid_info, 70), + ejabberd_hooks:add(roster_process_item, Host, + ?MODULE, process_item, 50), eldap_pool:start_link(State#state.eldap_id, - State#state.servers, State#state.backups, - State#state.port, State#state.dn, - State#state.password, State#state.tls_options), + State#state.servers, + State#state.backups, + State#state.port, + State#state.dn, + State#state.password, + State#state.tls_options), {ok, State}. handle_call(get_state, _From, State) -> {reply, {ok, State}, State}; + handle_call(_Request, _From, State) -> {reply, {error, badarg}, State}. -handle_cast(_Msg, State) -> {noreply, State}. +handle_cast(_Msg, State) -> + {noreply, State}. -handle_info(_Info, State) -> {noreply, State}. +handle_info(_Info, State) -> + {noreply, State}. terminate(_Reason, State) -> Host = State#state.host, - ejabberd_hooks:delete(roster_get, Host, ?MODULE, - get_user_roster, 70), + ejabberd_hooks:delete(roster_get, Host, + ?MODULE, get_user_roster, 70), ejabberd_hooks:delete(roster_in_subscription, Host, - ?MODULE, in_subscription, 30), + ?MODULE, in_subscription, 30), ejabberd_hooks:delete(roster_out_subscription, Host, - ?MODULE, out_subscription, 30), - ejabberd_hooks:delete(roster_get_subscription_lists, - Host, ?MODULE, get_subscription_lists, 70), + ?MODULE, out_subscription, 30), + ejabberd_hooks:delete(roster_get_subscription_lists, Host, + ?MODULE, get_subscription_lists, 70), ejabberd_hooks:delete(roster_get_jid_info, Host, - ?MODULE, get_jid_info, 70), + ?MODULE, get_jid_info, 70), ejabberd_hooks:delete(roster_process_item, Host, - ?MODULE, process_item, 50). + ?MODULE, process_item, 50). -code_change(_OldVsn, State, _Extra) -> {ok, State}. +code_change(_OldVsn, State, _Extra) -> + {ok, State}. %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- -%% For a given user, map all his shared roster contacts to groups they are -%% members of. Skip the user himself iff SkipUS is true. -get_user_to_groups_map({_, Server} = US, SkipUS) -> - DisplayedGroups = get_user_displayed_groups(US), -%% Pass given FilterParseArgs to eldap_filter:parse, and if successful, run and -%% return the resulting filter, retrieving given AttributesList. Return the -%% result entries. On any error silently return an empty list of results. -%% -%% Eldap server ID and base DN for the query are both retrieved from the State -%% record. - lists:foldl(fun (Group, Dict1) -> - GroupName = get_group_name(Server, Group), - lists:foldl(fun (Contact, Dict) -> - if SkipUS, Contact == US -> Dict; - true -> - dict:append(Contact, - GroupName, Dict) - end - end, - Dict1, get_group_users(Server, Group)) - end, - dict:new(), DisplayedGroups). - -eldap_search(State, FilterParseArgs, AttributesList) -> - case apply(eldap_filter, parse, FilterParseArgs) of - {ok, EldapFilter} -> - case eldap_pool:search(State#state.eldap_id, - [{base, State#state.base}, - {filter, EldapFilter}, - {timeout, ?LDAP_SEARCH_TIMEOUT}, - {deref_aliases, State#state.deref_aliases}, - {attributes, AttributesList}]) - of - #eldap_search_result{entries = Es} -> - %% A result with entries. Return their list. - Es; - _ -> - %% Something else. Pretend we got no results. - [] - end; - _ -> - %% Filter parsing failed. Pretend we got no results. - [] + +do_eldap_search(PoolName, Opts) -> + case eldap_pool:search(PoolName, Opts) of + #eldap_search_result{entries = Es} -> + %% A result with entries. Return their list. + Es; + Err -> + %% Something else. Pretend we got no results. + ?ERROR_MSG("Error searching: ~p ~p", [Err, Opts]), + [] + end. + +%% Pass given Filter or FilterTemplate and SubstList to eldap_filter:parse, +%% and if successful, run LDAP search on the whole subtree of Base, using +%% resulting filter, retrieving given AttributesList. Return the result entries. +%% On any error, print an error message and return an empty list of results. +eldap_search(State, Base, EldapFilter, AttributesList) when is_tuple(EldapFilter) -> + do_eldap_search(State#state.eldap_id, + [{base, Base}, +%% {scope, wholeSubtree} %% This is the default + {filter, EldapFilter}, + {timeout, ?LDAP_SEARCH_TIMEOUT}, + {deref_aliases, State#state.deref_aliases}, + {attributes, AttributesList}]); +%% Filter is string +eldap_search(State, Base, Filter, AttributesList) -> + eldap_search(State, Base, Filter, [], AttributesList). + +eldap_search(State, Base, FilterTemplate, SubstList, AttributesList) -> + case apply(eldap_filter, parse, [eldap_filter:do_sub(FilterTemplate, SubstList)]) of + {ok, EldapFilter} -> + %% Filter parsing succeeded + eldap_search(State, Base, EldapFilter, AttributesList); + Err -> + %% Filter parsing failed. Pretend we got no results. + ?ERROR_MSG("Error parsing filter: ~p", [Err]), + [] + end. + +%% The same as above, but gets the Attributes for the specified DN. +%% Note that this function doesn't honor the State's base DN; +%% TODO: fix this (create a custom check?) +eldap_search_dn(State, DN, EldapFilter, AttributesList) when is_tuple(EldapFilter) -> + do_eldap_search(State#state.eldap_id, + [{scope, baseObject}, + {base, DN}, + {filter, EldapFilter}, + {timeout, ?LDAP_SEARCH_TIMEOUT}, + {deref_aliases, State#state.deref_aliases}, + {attributes, AttributesList}]); +%% Filter is string. +eldap_search_dn(State, DN, Filter, AttributesList) -> + case eldap_filter:parse(Filter) of + {ok, EldapFilter} -> + %% Filter parsing succeeded + eldap_search_dn(State, DN, EldapFilter, AttributesList); + Err -> + %% Filter parsing failed. Pretend we got no results. + ?ERROR_MSG("Error parsing filter: ~p", [Err]), + [] + end. + +intersection(L1,L2) -> lists:filter(fun(X) -> lists:member(X,L1) end, L2). + +filter_roster(Roster, all) -> Roster; +filter_roster(_, []) -> []; +filter_roster(Roster, IncludeGroups) when is_list(IncludeGroups) -> + lists:foldl( + fun(RosterItem, Acc) -> + case intersection(IncludeGroups, RosterItem#shared_roster_item.groups) of + [] -> Acc; + CommonGroups -> [RosterItem#shared_roster_item{groups=CommonGroups} | Acc] + end + end, + [], Roster). + +get_user_visible_groups(UserGroups, VisibilityMap) -> + lists:foldl( + fun(Group, Acc) -> + case (lists:keysearch(Group, #shg_data.grp, VisibilityMap)) of + {value, #shg_data{shgrps=Gs}} when is_list(Gs) -> Gs ++ Acc; + _ -> Acc + end + end, + UserGroups, UserGroups). + +%% Returns [#shared_roster_item]; +%% Removes the US from returned data +%% If State#state.user_groups_only is 'true', then it removes all users that are not in US's groups, +%% and also removes the groups from the users that the US is not member of. +get_shared_roster(State, {_, Server} = US) -> + case (catch get_full_roster(State, Server)) of + {ok, {VisibilityMap, FullRoster}} -> + %%?ERROR_MSG("XXXXXX get_shared_roster: VMap=~p FullRoster=~p", [VisibilityMap, FullRoster]), + CommonRosterGroups = lists:foldl( + fun(_, all) -> all; + (#shg_data{grp=all, shgrps=all}, _) -> all; + (#shg_data{grp=all, shgrps=Gs}, Acc) when is_list(Gs) -> Gs ++ Acc; + (_, Acc) -> Acc + end, + [], VisibilityMap), + case lists:keytake(US, #shared_roster_item.us, FullRoster) of + false -> filter_roster(FullRoster, CommonRosterGroups); + {value, #shared_roster_item{groups=UserGroups}, Roster2} -> + VisibleGroups = case (CommonRosterGroups) of + all -> all; + CRG -> get_user_visible_groups(UserGroups, VisibilityMap) ++ CRG + end, + filter_roster(Roster2, VisibleGroups) + end; + {'EXIT', CatchData} -> ?ERROR_MSG("Error getting shared roster for user ~p: ~p", [US, CatchData]), []; + _Unexpected -> [] end. -get_user_displayed_groups({User, Host}) -> - {ok, State} = eldap_utils:get_state(Host, ?MODULE), - GroupAttr = State#state.group_attr, - Entries = eldap_search(State, - [eldap_filter:do_sub(State#state.rfilter, - [{<<"%u">>, User}])], - [GroupAttr]), - Reply = lists:flatmap(fun (#eldap_entry{attributes = - Attrs}) -> - case Attrs of - [{GroupAttr, ValuesList}] -> ValuesList; - _ -> [] - end - end, - Entries), - lists:usort(Reply). - -get_group_users(Host, Group) -> - {ok, State} = eldap_utils:get_state(Host, ?MODULE), - case cache_tab:dirty_lookup(shared_roster_ldap_group, - {Group, Host}, - fun () -> search_group_info(State, Group) end) - of - {ok, #group_info{members = Members}} - when Members /= undefined -> - Members; - _ -> [] +%% 1. If user is not a member of shared roster -> no additional subscriptions +%% 2. Else if ldap_subscribe_all is set AND this user is member of a group published to all -> +%% add all registered users of this vhost +%% 3. Else add only those groups this user' groups are published to +get_presense_subscribers(State, {_, Server} = US) -> + case (catch get_full_roster(State, Server)) of + {ok, {VisibilityMap, FullRoster}} -> + case lists:keytake(US, #shared_roster_item.us, FullRoster) of + false -> []; % Case #1 + {value, #shared_roster_item{groups=UserGroups}, Roster2} -> + AllGroups = lists:usort(lists:foldl( + fun(#shared_roster_item{groups=Gs}, Acc) -> Gs ++ Acc end, + [], FullRoster)), + Fun = case (State#state.subscribe_all) of + true -> % Possible case 2 + fun(_, all) -> all; + (#shg_data{grp=all, shgrps=all}, _) -> all; + (#shg_data{grp=all, shgrps=Gs}, Acc) when is_list(Gs) -> + case intersection(Gs, UserGroups) of + [] -> Acc; + _SomeCommon -> all + end; + (#shg_data{grp=G, shgrps=Gs}, Acc) when is_list(Gs) -> + case intersection(Gs, UserGroups) of + [] -> Acc; + _SomeCommon -> [G | Acc] + end; + (_, Acc) -> Acc + end; + _False -> % Case 3 + fun(#shg_data{grp=all}, Acc) -> AllGroups ++ Acc; + (#shg_data{grp=G, shgrps=Gs}, Acc) when is_list(Gs) -> + case intersection(Gs, UserGroups) of + [] -> Acc; + _SomeCommon -> [G | Acc] + end; + (_, Acc) -> Acc + end + end, + PublishTo = lists:foldl(Fun, [], VisibilityMap), + case (PublishTo) of + all -> + [{U1, S1, <<"">>} || {U1, S1} <- ejabberd_auth:get_vh_registered_users(Server)]; + Groups -> + [{U1, S1, <<"">>} || #shared_roster_item{us = {U1, S1}} <- filter_roster(Roster2, UserGroups ++ Groups)] + end + end; + {'EXIT', CatchData} -> ?ERROR_MSG("Error getting shared roster for user ~p: ~p", [US, CatchData]), []; + _Unexpected -> [] end. -get_group_name(Host, Group) -> - {ok, State} = eldap_utils:get_state(Host, ?MODULE), - case cache_tab:dirty_lookup(shared_roster_ldap_group, - {Group, Host}, - fun () -> search_group_info(State, Group) end) - of - {ok, #group_info{desc = GroupName}} - when GroupName /= undefined -> - GroupName; - _ -> Group +get_full_roster(State, Server) when State#state.roster_cache_size > 0 -> + cache_tab:dirty_lookup(shared_roster_ldap_sr, + {Server}, + fun() -> search_roster_info(State, Server) end); +get_full_roster(State, Server) -> + search_roster_info(State, Server). + +search_visible_groups(State, _) when State#state.shgfilter == all -> + [{all, all}]; +search_visible_groups(State, _) when State#state.shgfilter == none -> + [{all, none}]; +search_visible_groups(State, Groups) -> + case (string:str(State#state.shgfilter, "%g")) of + 0 -> [{all, search_group_visible_groups(State, "")}]; + _ -> lists:map( + fun(Group) -> {Group, search_group_visible_groups(State, Group)} end, + Groups) end. -get_user_name(User, Host) -> - {ok, State} = eldap_utils:get_state(Host, ?MODULE), - case cache_tab:dirty_lookup(shared_roster_ldap_user, - {User, Host}, - fun () -> search_user_name(State, User) end) - of - {ok, UserName} -> UserName; - error -> User +search_group_visible_groups(State, Group) -> + Entries = eldap_search(State, State#state.group_base, State#state.shgfilter, [{<<"%g">>, Group}], [State#state.shg_attr]), + lists:usort(lists:flatmap( + fun(#eldap_entry{attributes = Attrs}) -> + case Attrs of + [{_GroupAttr, ValuesList}] -> + ValuesList; + _ -> + [] + end + end, Entries)). + +group2name(all, _) -> all; +group2name(none, _) -> none; +group2name(Group, GroupNames) -> + case (lists:keysearch(Group, 1, GroupNames)) of + {value, {_, Name}} -> Name; + _ -> false end. +groups2names(all, _) -> all; +groups2names(none, _) -> none; +groups2names(GroupList, GroupNames) -> + lists:foldl( + fun(G, Acc) -> + case (group2name(G, GroupNames)) of + false -> Acc; + Name -> [Name | Acc] + end + end, + [], GroupList). + +prep_vis_map(VisGroups, GroupNames) -> + lists:foldl( + fun({G, Gs}, Acc) -> + case (group2name(G, GroupNames)) of + false -> Acc; + Name -> [#shg_data{grp=Name, shgrps=groups2names(Gs, GroupNames)} | Acc] + end + end, + [], VisGroups). + +search_roster_info(State, _Host) -> + Entries = eldap_search(State, State#state.group_base, State#state.rfilter, [State#state.group_attr]), + AllGroupIds = lists:usort(lists:flatmap( + fun(#eldap_entry{attributes = Attrs}) -> + case Attrs of + [{_GroupAttr, ValuesList}] -> + ValuesList; + _ -> + [] + end + end, Entries)), + VisGroups = search_visible_groups(State, AllGroupIds), + %%?ERROR_MSG("XXXXXX search_roster_info: VisGroups=~p", [VisGroups]), + + {GroupNames, RosterItems} = case State#state.member_selection_mode of + group_children -> + {GroupNames0, UsersDict0} = lists:foldl( + fun(Group, {GrNAcc, Dict1} = Acc) -> + case search_group_info(State, Group) of + {ok, #group_info{desc = GroupName, members = GroupDN}} -> + {[{Group, GroupName} | GrNAcc], search_users_info(State, GroupDN, GroupName, Dict1)}; + _ -> Acc %% Error getting group data -> No users! + end + end, + {[], dict:new()}, AllGroupIds), + + {GroupNames0, dict:fold( + fun(#user_info{us=US, name=UserName}, Groups, AccIn) -> + [#shared_roster_item{us = US, name = UserName, groups = Groups} | AccIn] + end, + [], UsersDict0)}; + _ -> + {GroupNames1, UsersDict1} = lists:foldl( + fun(Group, {GrNAcc, Dict1} = Acc) -> + case search_group_info(State, Group) of + {ok, #group_info{desc = GroupName, members = Members}} -> + {[{Group, GroupName} | GrNAcc], lists:foldl( + fun(Member, Dict) -> dict:append(Member, GroupName, Dict) end, + Dict1, Members)}; + _ -> Acc %% Error getting group data -> No users! + end + end, + {[], dict:new()}, AllGroupIds), + + %%?ERROR_MSG("UsersDict1: ~p", [UsersDict1]), + %%?ERROR_MSG("GroupNames1: ~p", [GroupNames1]), + + {GroupNames1, dict:fold( + fun(Member, Groups, AccIn) -> + case search_user_info(State, Member) of + {ok, #user_info{us=US, name=UserName}} -> + %%?ERROR_MSG("XXXX found user: ~p ~p ~p", [UserName, Groups, US]), + [#shared_roster_item{us = US, name = UserName, groups = Groups} | AccIn]; + _ -> AccIn + end + end, + [], UsersDict1)} + end, + + VisibilityMap = prep_vis_map(VisGroups, GroupNames), + {ok, {VisibilityMap, RosterItems}}. + search_group_info(State, Group) -> + AttList = case State#state.member_selection_mode of + group_children -> [State#state.group_desc]; + _ -> [State#state.group_desc, State#state.member_attr] + end, + SearchResult = case State#state.group_is_dn of + true -> eldap_search_dn(State, + Group, + State#state.gfilter, + AttList); + _ -> eldap_search(State, + State#state.group_base, + State#state.gfilter, + [{<<"%g">>, Group}], + AttList) + end, + case SearchResult of + [] -> + error; + LDAPEntries -> + case State#state.member_selection_mode of + group_children -> + [#eldap_entry{object_name=Name, attributes=Attrs} | _] = LDAPEntries, + {ok, #group_info{desc = eldap_utils:get_ldap_attr(State#state.group_desc, Attrs), + members = Name}}; + _ -> + {GroupDesc, MembersLists} = lists:foldl( + fun(#eldap_entry{attributes=Attrs}, {DescAcc, MembersAcc}) -> + case {eldap_utils:get_ldap_attr(State#state.group_desc, Attrs), + lists:keysearch(State#state.member_attr, 1, Attrs)} of + {Desc, {value, {GroupMemberAttr, Members}}} + when GroupMemberAttr == State#state.member_attr -> + {Desc, lists:usort(Members ++ MembersAcc)}; + _ -> + {DescAcc, MembersAcc} + end + end, + {Group, []}, LDAPEntries), + {ok, #group_info{desc = GroupDesc, + members = lists:usort(MembersLists)}} + end + end. + +%% Takes the attributes from LDAP user search; +%% returns error or {ok, #user_info} +construct_user(State, Attrs) -> Extractor = case State#state.uid_format_re of - <<"">> -> - fun (UID) -> - catch eldap_utils:get_user_part(UID, - State#state.uid_format) - end; - _ -> - fun (UID) -> - catch get_user_part_re(UID, - State#state.uid_format_re) - end - end, + <<"">> -> fun(UID) -> + catch eldap_utils:get_user_part(UID, State#state.uid_format) + end; + _ -> fun(UID) -> + catch get_user_part_re(UID, State#state.uid_format_re) + end + end, AuthChecker = case State#state.auth_check of - true -> fun ejabberd_auth:is_user_exists/2; - _ -> fun (_U, _S) -> true end - end, + true -> fun ejabberd_auth:is_user_exists/2; + _ -> fun(_U, _S) -> true end + end, Host = State#state.host, - case eldap_search(State, - [eldap_filter:do_sub(State#state.gfilter, - [{<<"%g">>, Group}])], - [State#state.group_attr, State#state.group_desc, - State#state.uid]) - of - [] -> error; - LDAPEntries -> - {GroupDesc, MembersLists} = lists:foldl(fun - (#eldap_entry{attributes = - Attrs}, - {DescAcc, JIDsAcc}) -> - case - {eldap_utils:get_ldap_attr(State#state.group_attr, - Attrs), - eldap_utils:get_ldap_attr(State#state.group_desc, - Attrs), - lists:keysearch(State#state.uid, - 1, - Attrs)} - of - {ID, Desc, - {value, - {GroupMemberAttr, - Members}}} - when ID /= <<"">>, - GroupMemberAttr - == - State#state.uid -> - JIDs = - lists:foldl(fun - ({ok, - UID}, - L) -> - PUID = - jlib:nodeprep(UID), - case - PUID - of - error -> - L; - _ -> - case - AuthChecker(PUID, - Host) - of - true -> - [{PUID, - Host} - | L]; - _ -> - L - end - end; - (_, - L) -> - L - end, - [], - lists:map(Extractor, - Members)), - {Desc, - [JIDs - | JIDsAcc]}; - _ -> - {DescAcc, JIDsAcc} - end - end, - {Group, []}, LDAPEntries), - {ok, - #group_info{desc = GroupDesc, - members = lists:usort(lists:flatten(MembersLists))}} + + case {eldap_utils:get_ldap_attr(State#state.uid, Attrs), + eldap_utils:get_ldap_attr(State#state.user_desc, Attrs)} of + {UID, Desc} when UID /= "" -> + %% By returning "" get_ldap_attr means "not found" + case Extractor(UID) of + {ok, UID1} -> + UID2 = jid:nodeprep(UID1), + case UID2 of + error -> error; + _ -> + case AuthChecker(UID2, Host) of + true -> {ok, #user_info{us={UID2, Host}, name=Desc}}; + _ -> error + end + end; + _ -> error + end; + _ -> + error end. -search_user_name(State, User) -> - case eldap_search(State, - [eldap_filter:do_sub(State#state.ufilter, - [{<<"%u">>, User}])], - [State#state.user_desc, State#state.user_uid]) - of - [#eldap_entry{attributes = Attrs} | _] -> - case {eldap_utils:get_ldap_attr(State#state.user_uid, - Attrs), - eldap_utils:get_ldap_attr(State#state.user_desc, Attrs)} - of - {UID, Desc} when UID /= <<"">> -> {ok, Desc}; - _ -> error - end; - [] -> error +%% This function is used when State#state.member_selection_mode is group_children +%% Returns UsersDict to which the users (#user_info) of this group are added +%%search_users_info(State, GroupInfo) -> +search_users_info(State, GroupDN, GroupName, UsersDict) -> + SearchResult = eldap_search(State, + GroupDN, + State#state.ufilter, + [State#state.user_desc, State#state.uid]), + lists:foldl( + fun(#eldap_entry{attributes=Attrs}, Dict1) -> + case construct_user(State, Attrs) of + {ok, UserInfo} -> + dict:append(UserInfo, GroupName, Dict1); + _ -> Dict1 + end + end, UsersDict, SearchResult). + +%% This function is used when State#state.member_selection_mode is either memberattr_normal or memberattr_dn +search_user_info(State, User) -> + %%?ERROR_MSG("XXX search_user_info: searching for ~p", [User]), + SearchResult = case State#state.member_selection_mode of + memberattr_dn -> eldap_search_dn(State, + User, + State#state.ufilter, + [State#state.user_desc, State#state.uid]); + memberattr_normal -> eldap_search(State, + State#state.base, + State#state.ufilter, + [{<<"%u">>, User}], + [State#state.user_desc, State#state.uid]) + end, + case SearchResult of + [#eldap_entry{attributes=Attrs}|_] -> + construct_user(State, Attrs); + [] -> + %%?ERROR_MSG("XX not found", []), + error end. %% Getting User ID part by regex pattern get_user_part_re(String, Pattern) -> case catch re:run(String, Pattern) of - {match, Captured} -> - {First, Len} = lists:nth(2, Captured), - Result = str:sub_string(String, First + 1, First + Len), - {ok, Result}; - _ -> {error, badmatch} + {match, Captured} -> + {First, Len} = lists:nth(2,Captured), + Result = string:sub_string(String, First+1, First+Len), + {ok,Result}; + _ -> {error, badmatch} end. +% select(SelectFirst, First, Second) -> +% case SelectFirst of +% true -> First; +% _ -> Second +% end. + +% prepare_filter(Opts, Name, Default, ReturnParsed) -> +% F = gen_mod:get_opt(Name, Opts, Default), +% prepare_filter(F, Name, ReturnParsed). + +% prepare_filter(F, Name, ReturnParsed) -> +% case eldap_filter:parse(F) of +% {ok, EldapFilter} -> +% case ReturnParsed of +% true -> EldapFilter; +% _ -> F +% end; +% _ -> +% ?ERROR_MSG(?INVALID_SETTING_MSG, [atom_to_list(Name), ?MODULE]), +% [] +% end. + parse_options(Host, Opts) -> Eldap_ID = jlib:atom_to_binary(gen_mod:get_module_proc(Host, ?MODULE)), Cfg = eldap_utils:get_config(Host, Opts), @@ -521,81 +761,223 @@ parse_options(Host, Opts) -> (false) -> false; (true) -> true end, true), - UserCacheValidity = eldap_utils:get_opt( - {ldap_user_cache_validity, Host}, Opts, - fun(I) when is_integer(I), I>0 -> I end, - ?USER_CACHE_VALIDITY), - GroupCacheValidity = eldap_utils:get_opt( + RosterCacheValidity = gen_mod:get_opt( {ldap_group_cache_validity, Host}, Opts, fun(I) when is_integer(I), I>0 -> I end, - ?GROUP_CACHE_VALIDITY), - UserCacheSize = eldap_utils:get_opt( - {ldap_user_cache_size, Host}, Opts, - fun(I) when is_integer(I), I>0 -> I end, - ?CACHE_SIZE), - GroupCacheSize = eldap_utils:get_opt( - {ldap_group_cache_size, Host}, Opts, + ?CACHE_VALIDITY), + RosterCacheSize = gen_mod:get_opt( + {ldap_roster_cache_size, Host}, Opts, fun(I) when is_integer(I), I>0 -> I end, ?CACHE_SIZE), - ConfigFilter = eldap_utils:get_opt({ldap_filter, Host}, Opts, - fun check_filter/1, <<"">>), - ConfigUserFilter = eldap_utils:get_opt({ldap_ufilter, Host}, Opts, - fun check_filter/1, <<"">>), - ConfigGroupFilter = eldap_utils:get_opt({ldap_gfilter, Host}, Opts, - fun check_filter/1, <<"">>), - RosterFilter = eldap_utils:get_opt({ldap_rfilter, Host}, Opts, - fun check_filter/1, <<"">>), + ConfigFilter = gen_mod:get_opt({ldap_filter, Host}, Opts, + fun check_filter/1, <<"">>), + ConfigUserFilter = gen_mod:get_opt({ldap_ufilter, Host}, Opts, + fun check_filter/1, <<"">>), + ConfigGroupFilter = gen_mod:get_opt({ldap_gfilter, Host}, Opts, + fun check_filter/1, <<"">>), + RosterFilter = gen_mod:get_opt({ldap_rfilter, Host}, Opts, + fun check_filter/1, <<"">>), SubFilter = <<"(&(", UIDAttr/binary, "=", - UIDAttrFormat/binary, ")(", GroupAttr/binary, "=%g))">>, + UIDAttrFormat/binary, ")(", GroupAttr/binary, "=%g))">>, UserSubFilter = case ConfigUserFilter of - <<"">> -> - eldap_filter:do_sub(SubFilter, [{<<"%g">>, <<"*">>}]); - UString -> UString - end, + <<"">> -> + eldap_filter:do_sub(SubFilter, [{<<"%g">>, <<"*">>}]); + UString -> UString + end, GroupSubFilter = case ConfigGroupFilter of - <<"">> -> - eldap_filter:do_sub(SubFilter, - [{<<"%u">>, <<"*">>}]); - GString -> GString + <<"">> -> + eldap_filter:do_sub(SubFilter, + [{<<"%u">>, <<"*">>}]); + GString -> GString end, Filter = case ConfigFilter of - <<"">> -> SubFilter; - _ -> - <<"(&", SubFilter/binary, ConfigFilter/binary, ")">> - end, + <<"">> -> SubFilter; + _ -> + <<"(&", SubFilter/binary, ConfigFilter/binary, ")">> + end, UserFilter = case ConfigFilter of - <<"">> -> UserSubFilter; - _ -> - <<"(&", UserSubFilter/binary, ConfigFilter/binary, ")">> - end, + <<"">> -> UserSubFilter; + _ -> + <<"(&", UserSubFilter/binary, ConfigFilter/binary, ")">> + end, GroupFilter = case ConfigFilter of - <<"">> -> GroupSubFilter; - _ -> - <<"(&", GroupSubFilter/binary, ConfigFilter/binary, - ")">> - end, + <<"">> -> GroupSubFilter; + _ -> + <<"(&", GroupSubFilter/binary, ConfigFilter/binary, + ")">> + end, +%%%%%%%%%%%%% + GroupBase = gen_mod:get_opt(ldap_group_base, Opts, fun iolist_to_binary/1, + Cfg#eldap_config.base), + GroupIsDN = gen_mod:get_opt(ldap_group_is_dn, Opts, + fun(on) -> true; + (off) -> false; + (false) -> false; + (true) -> true + end, true), + MemberSelMode = gen_mod:get_opt(ldap_member_selection_mode, Opts, + fun(memberattr_normal) -> memberattr_normal; + (memberattr_dn) -> memberattr_dn; + (group_children) -> group_children; + (Invalid) -> + ?ERROR_MSG("Invalid ldap_member_selection_mode '~p'. " + "Value 'memberattr_normal' will be used instead.", + [Invalid]) + end, memberattr_normal), + SubscribeAll = gen_mod:get_opt(ldap_subscribe_all, Opts, + fun(on) -> true; + (off) -> false; + (false) -> false; + (true) -> true + end, false), + % MemberIsDN = (MemberSelMode == member_attr_dn) or (MemberSelMode == group_children), + ShGFilter = gen_mod:get_opt(ldap_shgfilter, Opts, + fun(all) -> all; + (none) -> none; + (S) -> check_filter(S) + end, all), + ShGAttr = gen_mod:get_opt(ldap_shgattr, Opts, + fun iolist_to_binary/1, + << GroupAttr/binary >>), +%%%%%% #state{host = Host, eldap_id = Eldap_ID, - servers = Cfg#eldap_config.servers, - backups = Cfg#eldap_config.backups, + servers = Cfg#eldap_config.servers, + backups = Cfg#eldap_config.backups, port = Cfg#eldap_config.port, - tls_options = Cfg#eldap_config.tls_options, - dn = Cfg#eldap_config.dn, + tls_options = Cfg#eldap_config.tls_options, + dn = Cfg#eldap_config.dn, password = Cfg#eldap_config.password, base = Cfg#eldap_config.base, deref_aliases = Cfg#eldap_config.deref_aliases, - uid = UIDAttr, - group_attr = GroupAttr, group_desc = GroupDesc, - user_desc = UserDesc, user_uid = UserUID, - uid_format = UIDAttrFormat, - uid_format_re = UIDAttrFormatRe, filter = Filter, - ufilter = UserFilter, rfilter = RosterFilter, - gfilter = GroupFilter, auth_check = AuthCheck, - user_cache_size = UserCacheSize, - user_cache_validity = UserCacheValidity, - group_cache_size = GroupCacheSize, - group_cache_validity = GroupCacheValidity}. + group_attr = GroupAttr, group_desc = GroupDesc, + user_desc = UserDesc, uid = UserUID, + uid_format = UIDAttrFormat, + uid_format_re = UIDAttrFormatRe, filter = Filter, + ufilter = UserFilter, rfilter = RosterFilter, + gfilter = GroupFilter, auth_check = AuthCheck, + group_base = GroupBase, + member_attr = UIDAttr, + member_selection_mode = MemberSelMode, + group_is_dn = GroupIsDN, + shgfilter = ShGFilter, + shg_attr = ShGAttr, + subscribe_all = SubscribeAll, + roster_cache_size = RosterCacheSize, + roster_cache_validity = RosterCacheValidity}. check_filter(F) -> - NewF = iolist_to_binary(F), - {ok, _} = eldap_filter:parse(NewF), - NewF. + NewF = iolist_to_binary(F), + {ok, _} = eldap_filter:parse(NewF), + NewF. + +mod_opt_type(deref_aliases) -> + fun (never) -> never; + (searching) -> searching; + (finding) -> finding; + (always) -> always + end; +mod_opt_type(ldap_backups) -> + fun (L) -> [iolist_to_binary(H) || H <- L] end; +mod_opt_type(ldap_base) -> fun iolist_to_binary/1; +mod_opt_type(ldap_deref_aliases) -> + fun (never) -> never; + (searching) -> searching; + (finding) -> finding; + (always) -> always + end; +mod_opt_type(ldap_encrypt) -> + fun (tls) -> tls; + (starttls) -> starttls; + (none) -> none + end; +mod_opt_type(ldap_password) -> fun iolist_to_binary/1; +mod_opt_type(ldap_port) -> + fun (I) when is_integer(I), I > 0 -> I end; +mod_opt_type(ldap_rootdn) -> fun iolist_to_binary/1; +mod_opt_type(ldap_servers) -> + fun (L) -> [iolist_to_binary(H) || H <- L] end; +mod_opt_type(ldap_tls_cacertfile) -> + fun iolist_to_binary/1; +mod_opt_type(ldap_tls_certfile) -> + fun iolist_to_binary/1; +mod_opt_type(ldap_tls_depth) -> + fun (I) when is_integer(I), I >= 0 -> I end; +mod_opt_type(ldap_tls_verify) -> + fun (hard) -> hard; + (soft) -> soft; + (false) -> false + end; +mod_opt_type(ldap_auth_check) -> + fun (on) -> true; + (off) -> false; + (false) -> false; + (true) -> true + end; +mod_opt_type(ldap_filter) -> fun check_filter/1; +mod_opt_type(ldap_gfilter) -> fun check_filter/1; +mod_opt_type(ldap_group_cache_size) -> + fun (I) when is_integer(I), I > 0 -> I end; +mod_opt_type(ldap_group_cache_validity) -> + fun (I) when is_integer(I), I > 0 -> I end; +mod_opt_type(ldap_groupattr) -> fun iolist_to_binary/1; +mod_opt_type(ldap_groupdesc) -> fun iolist_to_binary/1; +mod_opt_type(ldap_memberattr) -> fun iolist_to_binary/1; +mod_opt_type(ldap_memberattr_format) -> + fun iolist_to_binary/1; +mod_opt_type(ldap_memberattr_format_re) -> + fun (S) -> + Re = iolist_to_binary(S), {ok, MP} = re:compile(Re), MP + end; +mod_opt_type(ldap_rfilter) -> fun check_filter/1; +mod_opt_type(ldap_ufilter) -> fun check_filter/1; +mod_opt_type(ldap_user_cache_size) -> + fun (I) when is_integer(I), I > 0 -> I end; +mod_opt_type(ldap_user_cache_validity) -> + fun (I) when is_integer(I), I > 0 -> I end; +mod_opt_type(ldap_userdesc) -> fun iolist_to_binary/1; +mod_opt_type(ldap_useruid) -> fun iolist_to_binary/1; +mod_opt_type(ldap_group_base) -> fun iolist_to_binary/1; +mod_opt_type(ldap_group_is_dn) -> fun(B) when is_boolean(B) -> B end; +mod_opt_type(ldap_member_selection_mode) -> + fun(memberattr_normal) -> memberattr_normal; + (memberattr_dn) -> memberattr_dn; + (group_children) -> group_children + end; +mod_opt_type(ldap_subscribe_all) -> fun(B) when is_boolean(B) -> B end; +mod_opt_type(ldap_shgfilter) -> + fun(all) -> all; + (none) -> none; + (S) -> check_filter(S) + end; +mod_opt_type(ldap_shgattr) -> fun iolist_to_binary/1; +mod_opt_type(_) -> + [ldap_auth_check, ldap_filter, ldap_gfilter, + ldap_group_cache_size, ldap_group_cache_validity, + ldap_groupattr, ldap_groupdesc, ldap_memberattr, + ldap_memberattr_format, ldap_memberattr_format_re, + ldap_rfilter, ldap_ufilter, ldap_user_cache_size, + ldap_user_cache_validity, ldap_userdesc, ldap_useruid, + deref_aliases, ldap_backups, ldap_base, + ldap_deref_aliases, ldap_encrypt, ldap_password, + ldap_port, ldap_rootdn, ldap_servers, + ldap_tls_cacertfile, ldap_tls_certfile, ldap_tls_depth, + ldap_tls_verify, ldap_group_base, ldap_group_is_dn, + ldap_member_selection_mode, ldap_subscribe_all, + ldap_shgfilter, ldap_shgattr]. + +opt_type(ldap_filter) -> fun check_filter/1; +opt_type(ldap_gfilter) -> fun check_filter/1; +opt_type(ldap_group_cache_size) -> + fun (I) when is_integer(I), I > 0 -> I end; +opt_type(ldap_group_cache_validity) -> + fun (I) when is_integer(I), I > 0 -> I end; +opt_type(ldap_rfilter) -> fun check_filter/1; +opt_type(ldap_ufilter) -> fun check_filter/1; +opt_type(ldap_user_cache_size) -> + fun (I) when is_integer(I), I > 0 -> I end; +opt_type(ldap_user_cache_validity) -> + fun (I) when is_integer(I), I > 0 -> I end; +opt_type(_) -> + [ldap_filter, ldap_gfilter, ldap_group_cache_size, + ldap_group_cache_validity, ldap_rfilter, ldap_ufilter, + ldap_user_cache_size, ldap_user_cache_validity]. diff --git a/src/mod_sic.erl b/src/mod_sic.erl index ed44f850..3ffd9c91 100644 --- a/src/mod_sic.erl +++ b/src/mod_sic.erl @@ -5,7 +5,7 @@ %%% Created : 6 Mar 2010 by Karim Gemayel <karim.gemayel@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,12 +25,14 @@ -module(mod_sic). +-protocol({xep, 279, '0.2'}). + -author('karim.gemayel@process-one.net'). -behaviour(gen_mod). -export([start/2, stop/1, process_local_iq/3, - process_sm_iq/3]). + process_sm_iq/3, mod_opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -89,3 +91,6 @@ get_ip({User, Server, Resource}, IQ#iq{type = error, sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]} end. + +mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; +mod_opt_type(_) -> [iqdisc]. diff --git a/src/mod_sip.erl b/src/mod_sip.erl index f7f2b8ed..6ffe5633 100644 --- a/src/mod_sip.erl +++ b/src/mod_sip.erl @@ -1,12 +1,12 @@ %%%------------------------------------------------------------------- %%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> -%%% @copyright (C) 2014, Evgeny Khramtsov %%% @doc %%% %%% @end %%% Created : 21 Apr 2014 by Evgeny Khramtsov <ekhramtsov@process-one.net> %%% -%%% ejabberd, Copyright (C) 2014-2015 ProcessOne +%%% +%%% ejabberd, Copyright (C) 2014-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -24,6 +24,7 @@ %%%------------------------------------------------------------------- -module(mod_sip). +-protocol({rfc, 3261}). -behaviour(gen_mod). -behaviour(esip). @@ -31,9 +32,9 @@ %% API -export([start/2, stop/1, make_response/2, is_my_host/1, at_my_host/1]). -%% esip_callbacks --export([data_in/2, data_out/2, message_in/2, message_out/2, - request/2, request/3, response/2, locate/1]). +-export([data_in/2, data_out/2, message_in/2, + message_out/2, request/2, request/3, response/2, + locate/1, mod_opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -159,8 +160,8 @@ locate(_SIPMsg) -> ok. find(#uri{user = User, host = Host}) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Host), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Host), if LUser == <<"">> -> to_me; true -> @@ -191,7 +192,7 @@ action(#sip{method = <<"REGISTER">>, type = request, hdrs = Hdrs, true -> register; false -> - {auth, jlib:nameprep(ToURI#uri.host)} + {auth, jid:nameprep(ToURI#uri.host)} end; false -> deny @@ -222,7 +223,7 @@ action(#sip{method = Method, hdrs = Hdrs, type = request} = Req, SIPSock) -> true -> find(ToURI); false -> - LServer = jlib:nameprep(FromURI#uri.host), + LServer = jid:nameprep(FromURI#uri.host), {relay, LServer} end; false -> @@ -249,8 +250,8 @@ check_auth(#sip{method = Method, hdrs = Hdrs, body = Body}, AuthHdr, _SIPSock) - from end, {_, #uri{user = User, host = Host}, _} = esip:get_hdr(Issuer, Hdrs), - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Host), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Host), case lists:filter( fun({_, Params}) -> Username = esip:get_param(<<"username">>, Params), @@ -262,8 +263,12 @@ check_auth(#sip{method = Method, hdrs = Hdrs, body = Body}, AuthHdr, _SIPSock) - case ejabberd_auth:get_password_s(LUser, LServer) of <<"">> -> false; - Password -> - esip:check_auth(Auth, Method, Body, Password) + Password when is_binary(Password) -> + esip:check_auth(Auth, Method, Body, Password); + _ScramedPassword -> + ?ERROR_MSG("unable to authenticate ~s@~s against SCRAM'ed " + "password", [LUser, LServer]), + false end; [] -> false @@ -294,7 +299,48 @@ make_response(Req, Resp) -> esip:make_response(Req, Resp, esip:make_tag()). at_my_host(#uri{host = Host}) -> - is_my_host(jlib:nameprep(Host)). + is_my_host(jid:nameprep(Host)). is_my_host(LServer) -> gen_mod:is_loaded(LServer, ?MODULE). + +mod_opt_type(always_record_route) -> + fun (true) -> true; + (false) -> false + end; +mod_opt_type(flow_timeout_tcp) -> + fun (I) when is_integer(I), I > 0 -> I end; +mod_opt_type(flow_timeout_udp) -> + fun (I) when is_integer(I), I > 0 -> I end; +mod_opt_type(record_route) -> + fun (IOList) -> + S = iolist_to_binary(IOList), + #uri{} = esip:decode_uri(S) + end; +mod_opt_type(routes) -> + fun (L) -> + lists:map(fun (IOList) -> + S = iolist_to_binary(IOList), + #uri{} = esip:decode_uri(S) + end, + L) + end; +mod_opt_type(via) -> + fun (L) -> + lists:map(fun (Opts) -> + Type = proplists:get_value(type, Opts), + Host = proplists:get_value(host, Opts), + Port = proplists:get_value(port, Opts), + true = (Type == tcp) or (Type == tls) or + (Type == udp), + true = is_binary(Host) and (Host /= <<"">>), + true = is_integer(Port) and (Port > 0) and + (Port < 65536) + or (Port == undefined), + {Type, {Host, Port}} + end, + L) + end; +mod_opt_type(_) -> + [always_record_route, flow_timeout_tcp, + flow_timeout_udp, record_route, routes, via]. diff --git a/src/mod_sip_proxy.erl b/src/mod_sip_proxy.erl index 6168d997..04ae55ae 100644 --- a/src/mod_sip_proxy.erl +++ b/src/mod_sip_proxy.erl @@ -1,12 +1,12 @@ %%%------------------------------------------------------------------- %%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> -%%% @copyright (C) 2014, Evgeny Khramtsov %%% @doc %%% %%% @end %%% Created : 21 Apr 2014 by Evgeny Khramtsov <ekhramtsov@process-one.net> %%% -%%% ejabberd, Copyright (C) 2014-2015 ProcessOne +%%% +%%% ejabberd, Copyright (C) 2014-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,16 +25,18 @@ %%%------------------------------------------------------------------- -module(mod_sip_proxy). +-behaviour(ejabberd_config). + -define(GEN_FSM, p1_fsm). -behaviour(?GEN_FSM). %% API -export([start/2, start_link/2, route/3, route/4]). -%% gen_fsm callbacks --export([init/1, wait_for_request/2, wait_for_response/2, - handle_event/3, handle_sync_event/4, - handle_info/3, terminate/3, code_change/4]). +-export([init/1, wait_for_request/2, + wait_for_response/2, handle_event/3, + handle_sync_event/4, handle_info/3, terminate/3, + code_change/4, opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -247,8 +249,8 @@ connect(#sip{hdrs = Hdrs} = Req, Opts) -> {_, ToURI, _} = esip:get_hdr('to', Hdrs), case mod_sip:at_my_host(ToURI) of true -> - LUser = jlib:nodeprep(ToURI#uri.user), - LServer = jlib:nameprep(ToURI#uri.host), + LUser = jid:nodeprep(ToURI#uri.user), + LServer = jid:nameprep(ToURI#uri.host), case mod_sip_registrar:find_sockets(LUser, LServer) of [_|_] = SIPSocks -> {ok, SIPSocks}; @@ -297,8 +299,7 @@ add_record_route_and_set_uri(URI, LServer, #sip{hdrs = Hdrs} = Req) -> case need_record_route(LServer) of true -> RR_URI = get_configured_record_route(LServer), - {MSecs, Secs, _} = now(), - TS = list_to_binary(integer_to_list(MSecs*1000000 + Secs)), + TS = list_to_binary(integer_to_list(p1_time_compat:system_time(seconds))), Sign = make_sign(TS, Hdrs), User = <<TS/binary, $-, Sign/binary>>, NewRR_URI = RR_URI#uri{user = User}, @@ -339,8 +340,7 @@ is_signed_by_me(TS_Sign, Hdrs) -> try [TSBin, Sign] = str:tokens(TS_Sign, <<"-">>), TS = list_to_integer(binary_to_list(TSBin)), - {MSecs, Secs, _} = now(), - NowTS = MSecs*1000000 + Secs, + NowTS = p1_time_compat:system_time(seconds), true = (NowTS - TS) =< ?SIGN_LIFETIME, Sign == make_sign(TSBin, Hdrs) catch _:_ -> @@ -410,7 +410,7 @@ choose_best_response(#state{responses = Responses} = State) -> %% Just compare host part only. cmp_uri(#uri{host = H1}, #uri{host = H2}) -> - jlib:nameprep(H1) == jlib:nameprep(H2). + jid:nameprep(H1) == jid:nameprep(H2). is_my_route(URI, URIs) -> lists:any(fun(U) -> cmp_uri(URI, U) end, URIs). @@ -439,20 +439,24 @@ prepare_request(LServer, #sip{hdrs = Hdrs} = Req) -> Hdrs3 = lists:filter( fun({'proxy-authorization', {_, Params}}) -> Realm = esip:unquote(esip:get_param(<<"realm">>, Params)), - not mod_sip:is_my_host(jlib:nameprep(Realm)); + not mod_sip:is_my_host(jid:nameprep(Realm)); (_) -> true end, Hdrs2), Req#sip{hdrs = Hdrs3}. safe_nodeprep(S) -> - case jlib:nodeprep(S) of + case jid:nodeprep(S) of error -> S; S1 -> S1 end. safe_nameprep(S) -> - case jlib:nameprep(S) of + case jid:nameprep(S) of error -> S; S1 -> S1 end. + +opt_type(domain_certfile) -> fun iolist_to_binary/1; +opt_type(shared_key) -> fun (V) -> V end; +opt_type(_) -> [domain_certfile, shared_key]. diff --git a/src/mod_sip_registrar.erl b/src/mod_sip_registrar.erl index a534c61c..fcaa4236 100644 --- a/src/mod_sip_registrar.erl +++ b/src/mod_sip_registrar.erl @@ -1,12 +1,12 @@ %%%------------------------------------------------------------------- %%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> -%%% @copyright (C) 2014, Evgeny Khramtsov %%% @doc %%% %%% @end %%% Created : 23 Apr 2014 by Evgeny Khramtsov <ekhramtsov@process-one.net> %%% -%%% ejabberd, Copyright (C) 2014-2015 ProcessOne +%%% +%%% ejabberd, Copyright (C) 2014-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -48,7 +48,7 @@ socket = #sip_socket{} :: #sip_socket{}, call_id = <<"">> :: binary(), cseq = 0 :: non_neg_integer(), - timestamp = now() :: erlang:timestamp(), + timestamp = p1_time_compat:timestamp() :: erlang:timestamp(), contact :: {binary(), #uri{}, [{binary(), binary()}]}, flow_tref :: reference(), reg_tref = make_ref() :: reference(), @@ -65,8 +65,8 @@ start_link() -> request(#sip{hdrs = Hdrs} = Req, SIPSock) -> {_, #uri{user = U, host = S}, _} = esip:get_hdr('to', Hdrs), - LUser = jlib:nodeprep(U), - LServer = jlib:nameprep(S), + LUser = jid:nodeprep(U), + LServer = jid:nameprep(S), {PeerIP, _} = SIPSock#sip_socket.peer, US = {LUser, LServer}, CallID = esip:get_hdr('call-id', Hdrs), @@ -242,7 +242,7 @@ register_session(US, SIPSocket, CallID, CSeq, IsOutboundSupported, socket = SIPSocket, call_id = CallID, cseq = CSeq, - timestamp = now(), + timestamp = p1_time_compat:timestamp(), contact = Contact, expires = Expires} end, ContactsWithExpires), @@ -490,15 +490,18 @@ need_ob_hdrs(Contacts, _IsOutboundSupported = true) -> end, Contacts). get_flow_timeout(LServer, #sip_socket{type = Type}) -> - {Option, Default} = - case Type of - udp -> {flow_timeout_udp, ?FLOW_TIMEOUT_UDP}; - _ -> {flow_timeout_tcp, ?FLOW_TIMEOUT_TCP} - end, - gen_mod:get_module_opt( - LServer, mod_sip, Option, - fun(I) when is_integer(I), I>0 -> I end, - Default). + case Type of + udp -> + gen_mod:get_module_opt( + LServer, mod_sip, flow_timeout_udp, + fun(I) when is_integer(I), I>0 -> I end, + ?FLOW_TIMEOUT_UDP); + _ -> + gen_mod:get_module_opt( + LServer, mod_sip, flow_timeout_tcp, + fun(I) when is_integer(I), I>0 -> I end, + ?FLOW_TIMEOUT_TCP) + end. update_table() -> Fields = record_info(fields, sip_session), diff --git a/src/mod_stats.erl b/src/mod_stats.erl index 4317e9e9..0328aec3 100644 --- a/src/mod_stats.erl +++ b/src/mod_stats.erl @@ -5,7 +5,7 @@ %%% Created : 11 Jan 2003 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -27,9 +27,12 @@ -author('alexey@process-one.net'). +-protocol({xep, 39, '0.6.0'}). + -behaviour(gen_mod). --export([start/2, stop/1, process_local_iq/3]). +-export([start/2, stop/1, process_local_iq/3, + mod_opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -54,7 +57,7 @@ process_local_iq(_From, To, IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; get -> #xmlel{children = Els} = SubEl, - Node = str:tokens(xml:get_tag_attr_s(<<"node">>, SubEl), + Node = str:tokens(fxml:get_tag_attr_s(<<"node">>, SubEl), <<"/">>), Names = get_names(Els, []), case get_local_stats(To#jid.server, Node, Names) of @@ -73,7 +76,7 @@ get_names([], Res) -> Res; get_names([#xmlel{name = <<"stat">>, attrs = Attrs} | Els], Res) -> - Name = xml:get_attr_s(<<"name">>, Attrs), + Name = fxml:get_attr_s(<<"name">>, Attrs), case Name of <<"">> -> get_names(Els, Res); _ -> get_names(Els, [Name | Res]) @@ -172,7 +175,7 @@ get_local_stat(_Server, _, Name) -> get_node_stat(Node, Name) when Name == <<"time/uptime">> -> - case catch rpc:call(Node, erlang, statistics, + case catch ejabberd_cluster:call(Node, erlang, statistics, [wall_clock]) of {badrpc, _Reason} -> @@ -185,7 +188,7 @@ get_node_stat(Node, Name) end; get_node_stat(Node, Name) when Name == <<"time/cputime">> -> - case catch rpc:call(Node, erlang, statistics, [runtime]) + case catch ejabberd_cluster:call(Node, erlang, statistics, [runtime]) of {badrpc, _Reason} -> ?STATERR(<<"500">>, <<"Internal Server Error">>); @@ -197,7 +200,7 @@ get_node_stat(Node, Name) end; get_node_stat(Node, Name) when Name == <<"users/online">> -> - case catch rpc:call(Node, ejabberd_sm, + case catch ejabberd_cluster:call(Node, ejabberd_sm, dirty_get_my_sessions_list, []) of {badrpc, _Reason} -> @@ -208,7 +211,7 @@ get_node_stat(Node, Name) end; get_node_stat(Node, Name) when Name == <<"transactions/committed">> -> - case catch rpc:call(Node, mnesia, system_info, + case catch ejabberd_cluster:call(Node, mnesia, system_info, [transaction_commits]) of {badrpc, _Reason} -> @@ -219,7 +222,7 @@ get_node_stat(Node, Name) end; get_node_stat(Node, Name) when Name == <<"transactions/aborted">> -> - case catch rpc:call(Node, mnesia, system_info, + case catch ejabberd_cluster:call(Node, mnesia, system_info, [transaction_failures]) of {badrpc, _Reason} -> @@ -230,7 +233,7 @@ get_node_stat(Node, Name) end; get_node_stat(Node, Name) when Name == <<"transactions/restarted">> -> - case catch rpc:call(Node, mnesia, system_info, + case catch ejabberd_cluster:call(Node, mnesia, system_info, [transaction_restarts]) of {badrpc, _Reason} -> @@ -241,7 +244,7 @@ get_node_stat(Node, Name) end; get_node_stat(Node, Name) when Name == <<"transactions/logged">> -> - case catch rpc:call(Node, mnesia, system_info, + case catch ejabberd_cluster:call(Node, mnesia, system_info, [transaction_log_writes]) of {badrpc, _Reason} -> @@ -263,3 +266,6 @@ search_running_node(SNode, [Node | Nodes]) -> SNode -> Node; _ -> search_running_node(SNode, Nodes) end. + +mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; +mod_opt_type(_) -> [iqdisc]. diff --git a/src/mod_time.erl b/src/mod_time.erl index c82fde41..8bfe9f9f 100644 --- a/src/mod_time.erl +++ b/src/mod_time.erl @@ -6,7 +6,7 @@ %%% Created : 18 Jan 2003 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -28,12 +28,12 @@ -author('alexey@process-one.net'). --behaviour(gen_mod). +-protocol({xep, 202, '2.0'}). --export([start/2, stop/1, process_local_iq90/3, - process_local_iq/3]). +-behaviour(gen_mod). - % TODO: Remove once XEP-0090 is Obsolete +-export([start/2, stop/1, process_local_iq/3, + mod_opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -44,43 +44,20 @@ start(Host, Opts) -> IQDisc = gen_mod:get_opt(iqdisc, Opts, fun gen_iq_handler:check_type/1, one_queue), gen_iq_handler:add_iq_handler(ejabberd_local, Host, - ?NS_TIME90, ?MODULE, process_local_iq90, - IQDisc), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_TIME, ?MODULE, process_local_iq, IQDisc). stop(Host) -> gen_iq_handler:remove_iq_handler(ejabberd_local, Host, - ?NS_TIME90), - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_TIME). -%% TODO: Remove this function once XEP-0090 is Obsolete -process_local_iq90(_From, _To, - #iq{type = Type, sub_el = SubEl} = IQ) -> - case Type of - set -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; - get -> - UTC = jlib:timestamp_to_iso(calendar:universal_time()), - IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = [{<<"xmlns">>, ?NS_TIME90}], - children = - [#xmlel{name = <<"utc">>, attrs = [], - children = [{xmlcdata, UTC}]}]}]} - end. - process_local_iq(_From, _To, #iq{type = Type, sub_el = SubEl} = IQ) -> case Type of set -> IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; get -> - Now = now(), - Now_universal = calendar:now_to_universal_time(Now), - Now_local = calendar:now_to_local_time(Now), + Now_universal = calendar:universal_time(), + Now_local = calendar:universal_time_to_local_time(Now_universal), {UTC, UTC_diff} = jlib:timestamp_to_iso(Now_universal, utc), Seconds_diff = @@ -107,3 +84,6 @@ process_local_iq(_From, _To, sign(N) when N < 0 -> <<"-">>; sign(_) -> <<"+">>. + +mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; +mod_opt_type(_) -> [iqdisc]. diff --git a/src/mod_vcard.erl b/src/mod_vcard.erl index ba23d068..256dc5de 100644 --- a/src/mod_vcard.erl +++ b/src/mod_vcard.erl @@ -5,7 +5,7 @@ %%% Created : 2 Jan 2003 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,16 +25,23 @@ -module(mod_vcard). +-compile([{parse_transform, ejabberd_sql_pt}]). + -author('alexey@process-one.net'). +-protocol({xep, 54, '1.2'}). +-protocol({xep, 55, '1.3'}). + -behaviour(gen_mod). -export([start/2, init/3, stop/1, get_sm_features/5, process_local_iq/3, process_sm_iq/3, reindex_vcards/0, - remove_user/2, export/1, import/1, import/3]). + remove_user/2, export/1, import/1, import/3, + mod_opt_type/1, set_vcard/3]). -include("ejabberd.hrl"). -include("logger.hrl"). +-include("ejabberd_sql_pt.hrl"). -include("jlib.hrl"). @@ -90,7 +97,7 @@ start(Host, Opts) -> <<"vjud.@HOST@">>), Search = gen_mod:get_opt(search, Opts, fun(B) when is_boolean(B) -> B end, - true), + false), register(gen_mod:get_module_proc(Host, ?PROCNAME), spawn(?MODULE, init, [MyHost, Host, Search])). @@ -98,7 +105,7 @@ init(Host, ServerHost, Search) -> case Search of false -> loop(Host, ServerHost); _ -> - ejabberd_router:register_route(Host), + ejabberd_router:register_route(Host, ServerHost), loop(Host, ServerHost) end. @@ -162,7 +169,7 @@ process_local_iq(_From, _To, [{xmlcdata, <<(translate:translate(Lang, <<"Erlang Jabber Server">>))/binary, - "\nCopyright (c) 2002-2015 ProcessOne">>}]}, + "\nCopyright (c) 2002-2016 ProcessOne">>}]}, #xmlel{name = <<"BDAY">>, attrs = [], children = [{xmlcdata, <<"2002-11-16">>}]}]}]} @@ -208,14 +215,13 @@ get_vcard(LUser, LServer, mnesia) -> {aborted, _Reason} -> error end; get_vcard(LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - case catch odbc_queries:get_vcard(LServer, Username) of - {selected, [<<"vcard">>], [[SVCARD]]} -> - case xml_stream:parse_element(SVCARD) of + case catch odbc_queries:get_vcard(LServer, LUser) of + {selected, [{SVCARD}]} -> + case fxml_stream:parse_element(SVCARD) of {error, _Reason} -> error; VCARD -> [VCARD] end; - {selected, [<<"vcard">>], []} -> []; + {selected, []} -> []; _ -> error end; get_vcard(LUser, LServer, riak) -> @@ -229,35 +235,35 @@ get_vcard(LUser, LServer, riak) -> end. set_vcard(User, LServer, VCARD) -> - FN = xml:get_path_s(VCARD, [{elem, <<"FN">>}, cdata]), - Family = xml:get_path_s(VCARD, + FN = fxml:get_path_s(VCARD, [{elem, <<"FN">>}, cdata]), + Family = fxml:get_path_s(VCARD, [{elem, <<"N">>}, {elem, <<"FAMILY">>}, cdata]), - Given = xml:get_path_s(VCARD, + Given = fxml:get_path_s(VCARD, [{elem, <<"N">>}, {elem, <<"GIVEN">>}, cdata]), - Middle = xml:get_path_s(VCARD, + Middle = fxml:get_path_s(VCARD, [{elem, <<"N">>}, {elem, <<"MIDDLE">>}, cdata]), - Nickname = xml:get_path_s(VCARD, + Nickname = fxml:get_path_s(VCARD, [{elem, <<"NICKNAME">>}, cdata]), - BDay = xml:get_path_s(VCARD, + BDay = fxml:get_path_s(VCARD, [{elem, <<"BDAY">>}, cdata]), - CTRY = xml:get_path_s(VCARD, + CTRY = fxml:get_path_s(VCARD, [{elem, <<"ADR">>}, {elem, <<"CTRY">>}, cdata]), - Locality = xml:get_path_s(VCARD, + Locality = fxml:get_path_s(VCARD, [{elem, <<"ADR">>}, {elem, <<"LOCALITY">>}, cdata]), - EMail1 = xml:get_path_s(VCARD, + EMail1 = fxml:get_path_s(VCARD, [{elem, <<"EMAIL">>}, {elem, <<"USERID">>}, cdata]), - EMail2 = xml:get_path_s(VCARD, + EMail2 = fxml:get_path_s(VCARD, [{elem, <<"EMAIL">>}, cdata]), - OrgName = xml:get_path_s(VCARD, + OrgName = fxml:get_path_s(VCARD, [{elem, <<"ORG">>}, {elem, <<"ORGNAME">>}, cdata]), - OrgUnit = xml:get_path_s(VCARD, + OrgUnit = fxml:get_path_s(VCARD, [{elem, <<"ORG">>}, {elem, <<"ORGUNIT">>}, cdata]), EMail = case EMail1 of <<"">> -> EMail2; _ -> EMail1 end, - LUser = jlib:nodeprep(User), + LUser = jid:nodeprep(User), LFN = string2lower(FN), LFamily = string2lower(Family), LGiven = string2lower(Given), @@ -332,39 +338,14 @@ set_vcard(User, LServer, VCARD) -> {<<"orgunit">>, OrgUnit}, {<<"lorgunit">>, LOrgUnit}]}]); odbc -> - Username = ejabberd_odbc:escape(User), - LUsername = ejabberd_odbc:escape(LUser), - SVCARD = - ejabberd_odbc:escape(xml:element_to_binary(VCARD)), - SFN = ejabberd_odbc:escape(FN), - SLFN = ejabberd_odbc:escape(LFN), - SFamily = ejabberd_odbc:escape(Family), - SLFamily = ejabberd_odbc:escape(LFamily), - SGiven = ejabberd_odbc:escape(Given), - SLGiven = ejabberd_odbc:escape(LGiven), - SMiddle = ejabberd_odbc:escape(Middle), - SLMiddle = ejabberd_odbc:escape(LMiddle), - SNickname = ejabberd_odbc:escape(Nickname), - SLNickname = ejabberd_odbc:escape(LNickname), - SBDay = ejabberd_odbc:escape(BDay), - SLBDay = ejabberd_odbc:escape(LBDay), - SCTRY = ejabberd_odbc:escape(CTRY), - SLCTRY = ejabberd_odbc:escape(LCTRY), - SLocality = ejabberd_odbc:escape(Locality), - SLLocality = ejabberd_odbc:escape(LLocality), - SEMail = ejabberd_odbc:escape(EMail), - SLEMail = ejabberd_odbc:escape(LEMail), - SOrgName = ejabberd_odbc:escape(OrgName), - SLOrgName = ejabberd_odbc:escape(LOrgName), - SOrgUnit = ejabberd_odbc:escape(OrgUnit), - SLOrgUnit = ejabberd_odbc:escape(LOrgUnit), - odbc_queries:set_vcard(LServer, LUsername, SBDay, SCTRY, - SEMail, SFN, SFamily, SGiven, SLBDay, - SLCTRY, SLEMail, SLFN, SLFamily, - SLGiven, SLLocality, SLMiddle, - SLNickname, SLOrgName, SLOrgUnit, - SLocality, SMiddle, SNickname, SOrgName, - SOrgUnit, SVCARD, Username) + SVCARD = fxml:element_to_binary(VCARD), + odbc_queries:set_vcard(LServer, LUser, BDay, CTRY, + EMail, FN, Family, Given, LBDay, + LCTRY, LEMail, LFN, LFamily, + LGiven, LLocality, LMiddle, + LNickname, LOrgName, LOrgUnit, + Locality, Middle, Nickname, OrgName, + OrgUnit, SVCARD, User) end, ejabberd_hooks:run(vcard_set, LServer, [LUser, LServer, VCARD]) @@ -400,7 +381,7 @@ string2lower(String) -> [{xmlcdata, <<(translate:translate(Lang, <<"Search users in ">>))/binary, - (jlib:jid_to_string(JID))/binary>>}]}, + (jid:to_string(JID))/binary>>}]}, #xmlel{name = <<"instructions">>, attrs = [], children = [{xmlcdata, @@ -574,7 +555,7 @@ iq_get_vcard(Lang) -> [{xmlcdata, <<(translate:translate(Lang, <<"ejabberd vCard module">>))/binary, - "\nCopyright (c) 2003-2015 ProcessOne">>}]}]. + "\nCopyright (c) 2003-2016 ProcessOne">>}]}]. find_xdata_el(#xmlel{children = SubEls}) -> find_xdata_el1(SubEls). @@ -583,7 +564,7 @@ find_xdata_el1([]) -> false; find_xdata_el1([#xmlel{name = Name, attrs = Attrs, children = SubEls} | Els]) -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of + case fxml:get_attr_s(<<"xmlns">>, Attrs) of ?NS_XDATA -> #xmlel{name = Name, attrs = Attrs, children = SubEls}; _ -> find_xdata_el1(Els) @@ -603,7 +584,7 @@ search_result(Lang, JID, ServerHost, Data) -> [{xmlcdata, <<(translate:translate(Lang, <<"Search Results for ">>))/binary, - (jlib:jid_to_string(JID))/binary>>}]}, + (jid:to_string(JID))/binary>>}]}, #xmlel{name = <<"reported">>, attrs = [], children = [?TLFIELD(<<"text-single">>, <<"Jabber ID">>, @@ -858,27 +839,27 @@ set_vcard_t(R, _) -> US = R#vcard.us, User = US, VCARD = R#vcard.vcard, - FN = xml:get_path_s(VCARD, [{elem, <<"FN">>}, cdata]), - Family = xml:get_path_s(VCARD, + FN = fxml:get_path_s(VCARD, [{elem, <<"FN">>}, cdata]), + Family = fxml:get_path_s(VCARD, [{elem, <<"N">>}, {elem, <<"FAMILY">>}, cdata]), - Given = xml:get_path_s(VCARD, + Given = fxml:get_path_s(VCARD, [{elem, <<"N">>}, {elem, <<"GIVEN">>}, cdata]), - Middle = xml:get_path_s(VCARD, + Middle = fxml:get_path_s(VCARD, [{elem, <<"N">>}, {elem, <<"MIDDLE">>}, cdata]), - Nickname = xml:get_path_s(VCARD, + Nickname = fxml:get_path_s(VCARD, [{elem, <<"NICKNAME">>}, cdata]), - BDay = xml:get_path_s(VCARD, + BDay = fxml:get_path_s(VCARD, [{elem, <<"BDAY">>}, cdata]), - CTRY = xml:get_path_s(VCARD, + CTRY = fxml:get_path_s(VCARD, [{elem, <<"ADR">>}, {elem, <<"CTRY">>}, cdata]), - Locality = xml:get_path_s(VCARD, + Locality = fxml:get_path_s(VCARD, [{elem, <<"ADR">>}, {elem, <<"LOCALITY">>}, cdata]), - EMail = xml:get_path_s(VCARD, + EMail = fxml:get_path_s(VCARD, [{elem, <<"EMAIL">>}, cdata]), - OrgName = xml:get_path_s(VCARD, + OrgName = fxml:get_path_s(VCARD, [{elem, <<"ORG">>}, {elem, <<"ORGNAME">>}, cdata]), - OrgUnit = xml:get_path_s(VCARD, + OrgUnit = fxml:get_path_s(VCARD, [{elem, <<"ORG">>}, {elem, <<"ORGUNIT">>}, cdata]), {LUser, _LServer} = US, LFN = string2lower(FN), @@ -912,8 +893,8 @@ reindex_vcards() -> mnesia:transaction(F). remove_user(User, Server) -> - LUser = jlib:nodeprep(User), - LServer = jlib:nameprep(Server), + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), remove_user(LUser, LServer, gen_mod:db_type(LServer, ?MODULE)). @@ -925,12 +906,14 @@ remove_user(LUser, LServer, mnesia) -> end, mnesia:transaction(F); remove_user(LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - ejabberd_odbc:sql_transaction(LServer, - [[<<"delete from vcard where username='">>, - Username, <<"';">>], - [<<"delete from vcard_search where lusername='">>, - Username, <<"';">>]]); + ejabberd_odbc:sql_transaction( + LServer, + fun() -> + ejabberd_odbc:sql_query_t( + ?SQL("delete from vcard where username=%(LUser)s")), + ejabberd_odbc:sql_query_t( + ?SQL("delete from vcard_search where lusername=%(LUser)s")) + end); remove_user(LUser, LServer, riak) -> {atomic, ejabberd_riak:delete(vcard, {LUser, LServer})}. @@ -948,7 +931,7 @@ update_vcard_table() -> fun(#vcard{us = {U, S}, vcard = El} = R) -> R#vcard{us = {iolist_to_binary(U), iolist_to_binary(S)}, - vcard = xml:to_xmlel(El)} + vcard = fxml:to_xmlel(El)} end); _ -> ?INFO_MSG("Recreating vcard table", []), @@ -987,7 +970,7 @@ export(_Server) -> when LServer == Host -> Username = ejabberd_odbc:escape(LUser), SVCARD = - ejabberd_odbc:escape(xml:element_to_binary(VCARD)), + ejabberd_odbc:escape(fxml:element_to_binary(VCARD)), [[<<"delete from vcard where username='">>, Username, <<"';">>], [<<"insert into vcard(username, vcard) values ('">>, Username, <<"', '">>, SVCARD, <<"');">>]]; @@ -1060,7 +1043,7 @@ export(_Server) -> import(LServer) -> [{<<"select username, vcard from vcard;">>, fun([LUser, SVCard]) -> - #xmlel{} = VCARD = xml_stream:parse_element(SVCard), + #xmlel{} = VCARD = fxml_stream:parse_element(SVCard), #vcard{us = {LUser, LServer}, vcard = VCARD} end}, {<<"select username, lusername, fn, lfn, family, lfamily, " @@ -1091,29 +1074,29 @@ import(_LServer, mnesia, #vcard{} = VCard) -> import(_LServer, mnesia, #vcard_search{} = S) -> mnesia:dirty_write(S); import(_LServer, riak, #vcard{us = {LUser, _}, vcard = El} = VCard) -> - FN = xml:get_path_s(El, [{elem, <<"FN">>}, cdata]), - Family = xml:get_path_s(El, + FN = fxml:get_path_s(El, [{elem, <<"FN">>}, cdata]), + Family = fxml:get_path_s(El, [{elem, <<"N">>}, {elem, <<"FAMILY">>}, cdata]), - Given = xml:get_path_s(El, + Given = fxml:get_path_s(El, [{elem, <<"N">>}, {elem, <<"GIVEN">>}, cdata]), - Middle = xml:get_path_s(El, + Middle = fxml:get_path_s(El, [{elem, <<"N">>}, {elem, <<"MIDDLE">>}, cdata]), - Nickname = xml:get_path_s(El, + Nickname = fxml:get_path_s(El, [{elem, <<"NICKNAME">>}, cdata]), - BDay = xml:get_path_s(El, + BDay = fxml:get_path_s(El, [{elem, <<"BDAY">>}, cdata]), - CTRY = xml:get_path_s(El, + CTRY = fxml:get_path_s(El, [{elem, <<"ADR">>}, {elem, <<"CTRY">>}, cdata]), - Locality = xml:get_path_s(El, + Locality = fxml:get_path_s(El, [{elem, <<"ADR">>}, {elem, <<"LOCALITY">>}, cdata]), - EMail1 = xml:get_path_s(El, + EMail1 = fxml:get_path_s(El, [{elem, <<"EMAIL">>}, {elem, <<"USERID">>}, cdata]), - EMail2 = xml:get_path_s(El, + EMail2 = fxml:get_path_s(El, [{elem, <<"EMAIL">>}, cdata]), - OrgName = xml:get_path_s(El, + OrgName = fxml:get_path_s(El, [{elem, <<"ORG">>}, {elem, <<"ORGNAME">>}, cdata]), - OrgUnit = xml:get_path_s(El, + OrgUnit = fxml:get_path_s(El, [{elem, <<"ORG">>}, {elem, <<"ORGUNIT">>}, cdata]), EMail = case EMail1 of <<"">> -> EMail2; @@ -1159,3 +1142,20 @@ import(_LServer, riak, #vcard_search{}) -> ok; import(_, _, _) -> pass. + +mod_opt_type(allow_return_all) -> + fun (B) when is_boolean(B) -> B end; +mod_opt_type(db_type) -> fun gen_mod:v_db/1; +mod_opt_type(host) -> fun iolist_to_binary/1; +mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; +mod_opt_type(matches) -> + fun (infinity) -> infinity; + (I) when is_integer(I), I > 0 -> I + end; +mod_opt_type(search) -> + fun (B) when is_boolean(B) -> B end; +mod_opt_type(search_all_hosts) -> + fun (B) when is_boolean(B) -> B end; +mod_opt_type(_) -> + [allow_return_all, db_type, host, iqdisc, matches, + search, search_all_hosts]. diff --git a/src/mod_vcard_ldap.erl b/src/mod_vcard_ldap.erl index 61db3897..98aaf936 100644 --- a/src/mod_vcard_ldap.erl +++ b/src/mod_vcard_ldap.erl @@ -5,7 +5,7 @@ %%% Created : 2 Jan 2003 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -21,10 +21,12 @@ %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% -%%%---------------------------------------------------------------------- +%%%---------------------u------------------------------------------------- -module(mod_vcard_ldap). +-behaviour(ejabberd_config). + -author('alexey@process-one.net'). -behaviour(gen_server). @@ -37,7 +39,8 @@ -export([start/2, start_link/2, stop/1, get_sm_features/5, process_local_iq/3, process_sm_iq/3, - remove_user/1, route/4, transform_module_options/1]). + remove_user/1, route/4, transform_module_options/1, + mod_opt_type/1, opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -52,7 +55,7 @@ {serverhost = <<"">> :: binary(), myhost = <<"">> :: binary(), eldap_id = <<"">> :: binary(), - search = true :: boolean(), + search = false :: boolean(), servers = [] :: [binary()], backups = [] :: [binary()], port = ?LDAP_PORT :: inet:port_number(), @@ -170,7 +173,7 @@ init([Host, Opts]) -> State#state.password, State#state.tls_options), case State#state.search of true -> - ejabberd_router:register_route(State#state.myhost); + ejabberd_router:register_route(State#state.myhost, Host); _ -> ok end, {ok, State}. @@ -220,7 +223,7 @@ process_local_iq(_From, _To, [{xmlcdata, <<(translate:translate(Lang, <<"Erlang Jabber Server">>))/binary, - "\nCopyright (c) 2002-2015 ProcessOne">>}]}, + "\nCopyright (c) 2002-2016 ProcessOne">>}]}, #xmlel{name = <<"BDAY">>, attrs = [], children = [{xmlcdata, <<"2002-11-16">>}]}]}]} @@ -419,7 +422,7 @@ ldap_attribute_to_vcard(_, _) -> none. [{xmlcdata, <<(translate:translate(Lang, <<"Search users in ">>))/binary, - (jlib:jid_to_string(JID))/binary>>}]}, + (jid:to_string(JID))/binary>>}]}, #xmlel{name = <<"instructions">>, attrs = [], children = [{xmlcdata, @@ -581,7 +584,7 @@ iq_get_vcard(Lang) -> [{xmlcdata, <<(translate:translate(Lang, <<"ejabberd vCard module">>))/binary, - "\nCopyright (c) 2003-2015 ProcessOne">>}]}]. + "\nCopyright (c) 2003-2016 ProcessOne">>}]}]. -define(LFIELD(Label, Var), #xmlel{name = <<"field">>, @@ -597,7 +600,7 @@ search_result(Lang, JID, State, Data) -> [{xmlcdata, <<(translate:translate(Lang, <<"Search Results for ">>))/binary, - (jlib:jid_to_string(JID))/binary>>}]}, + (jid:to_string(JID))/binary>>}]}, #xmlel{name = <<"reported">>, attrs = [], children = [?TLFIELD(<<"text-single">>, <<"Jabber ID">>, @@ -720,7 +723,7 @@ find_xdata_el1([]) -> false; find_xdata_el1([#xmlel{name = Name, attrs = Attrs, children = SubEls} | Els]) -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of + case fxml:get_attr_s(<<"xmlns">>, Attrs) of ?NS_XDATA -> #xmlel{name = Name, attrs = Attrs, children = SubEls}; _ -> find_xdata_el1(Els) @@ -732,14 +735,14 @@ parse_options(Host, Opts) -> <<"vjud.@HOST@">>), Search = gen_mod:get_opt(search, Opts, fun(B) when is_boolean(B) -> B end, - true), + false), Matches = gen_mod:get_opt(matches, Opts, fun(infinity) -> 0; (I) when is_integer(I), I>0 -> I end, 30), Eldap_ID = jlib:atom_to_binary(gen_mod:get_module_proc(Host, ?PROCNAME)), Cfg = eldap_utils:get_config(Host, Opts), - UIDsTemp = eldap_utils:get_opt( + UIDsTemp = gen_mod:get_opt( {ldap_uids, Host}, Opts, fun(Us) -> lists:map( @@ -752,7 +755,7 @@ parse_options(Host, Opts) -> end, [{<<"uid">>, <<"%u">>}]), UIDs = eldap_utils:uids_domain_subst(Host, UIDsTemp), SubFilter = eldap_utils:generate_subfilter(UIDs), - UserFilter = case eldap_utils:get_opt( + UserFilter = case gen_mod:get_opt( {ldap_filter, Host}, Opts, fun check_filter/1, <<"">>) of <<"">> -> @@ -840,3 +843,100 @@ check_filter(F) -> NewF = iolist_to_binary(F), {ok, _} = eldap_filter:parse(NewF), NewF. + +mod_opt_type(host) -> fun iolist_to_binary/1; +mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; +mod_opt_type(ldap_filter) -> fun check_filter/1; +mod_opt_type(ldap_search_fields) -> + fun (Ls) -> + [{iolist_to_binary(S), iolist_to_binary(P)} + || {S, P} <- Ls] + end; +mod_opt_type(ldap_search_reported) -> + fun (Ls) -> + [{iolist_to_binary(S), iolist_to_binary(P)} + || {S, P} <- Ls] + end; +mod_opt_type(ldap_uids) -> + fun (Us) -> + lists:map(fun ({U, P}) -> + {iolist_to_binary(U), iolist_to_binary(P)}; + ({U}) -> {iolist_to_binary(U)} + end, + Us) + end; +mod_opt_type(ldap_vcard_map) -> + fun (Ls) -> + lists:map(fun ({S, [{P, L}]}) -> + {iolist_to_binary(S), iolist_to_binary(P), + [iolist_to_binary(E) || E <- L]} + end, + Ls) + end; +mod_opt_type(matches) -> + fun (infinity) -> 0; + (I) when is_integer(I), I > 0 -> I + end; +mod_opt_type(search) -> + fun (B) when is_boolean(B) -> B end; +mod_opt_type(deref_aliases) -> + fun (never) -> never; + (searching) -> searching; + (finding) -> finding; + (always) -> always + end; +mod_opt_type(ldap_backups) -> + fun (L) -> [iolist_to_binary(H) || H <- L] end; +mod_opt_type(ldap_base) -> fun iolist_to_binary/1; +mod_opt_type(ldap_deref_aliases) -> + fun (never) -> never; + (searching) -> searching; + (finding) -> finding; + (always) -> always + end; +mod_opt_type(ldap_encrypt) -> + fun (tls) -> tls; + (starttls) -> starttls; + (none) -> none + end; +mod_opt_type(ldap_password) -> fun iolist_to_binary/1; +mod_opt_type(ldap_port) -> + fun (I) when is_integer(I), I > 0 -> I end; +mod_opt_type(ldap_rootdn) -> fun iolist_to_binary/1; +mod_opt_type(ldap_servers) -> + fun (L) -> [iolist_to_binary(H) || H <- L] end; +mod_opt_type(ldap_tls_cacertfile) -> + fun iolist_to_binary/1; +mod_opt_type(ldap_tls_certfile) -> + fun iolist_to_binary/1; +mod_opt_type(ldap_tls_depth) -> + fun (I) when is_integer(I), I >= 0 -> I end; +mod_opt_type(ldap_tls_verify) -> + fun (hard) -> hard; + (soft) -> soft; + (false) -> false + end; +mod_opt_type(_) -> + [host, iqdisc, ldap_filter, ldap_search_fields, + ldap_search_reported, ldap_uids, ldap_vcard_map, + matches, search, deref_aliases, ldap_backups, ldap_base, + ldap_deref_aliases, ldap_encrypt, ldap_password, + ldap_port, ldap_rootdn, ldap_servers, + ldap_tls_cacertfile, ldap_tls_certfile, ldap_tls_depth, + ldap_tls_verify]. + +opt_type(ldap_filter) -> fun check_filter/1; +opt_type(ldap_uids) -> + fun (Us) -> + lists:map(fun ({U, P}) -> + {iolist_to_binary(U), iolist_to_binary(P)}; + ({U}) -> {iolist_to_binary(U)} + end, + Us) + end; +opt_type(_) -> + [ldap_filter, ldap_uids, deref_aliases, ldap_backups, ldap_base, + ldap_deref_aliases, ldap_encrypt, ldap_password, + ldap_port, ldap_rootdn, ldap_servers, + ldap_tls_cacertfile, ldap_tls_certfile, ldap_tls_depth, + ldap_tls_verify]. diff --git a/src/mod_vcard_xupdate.erl b/src/mod_vcard_xupdate.erl index 41a07bbc..18fb09a5 100644 --- a/src/mod_vcard_xupdate.erl +++ b/src/mod_vcard_xupdate.erl @@ -12,8 +12,8 @@ %% gen_mod callbacks -export([start/2, stop/1]). -%% hooks --export([update_presence/3, vcard_set/3, export/1, import/1, import/3]). +-export([update_presence/3, vcard_set/3, export/1, + import/1, import/3, mod_opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -56,7 +56,7 @@ stop(Host) -> update_presence(#xmlel{name = <<"presence">>, attrs = Attrs} = Packet, User, Host) -> - case xml:get_attr_s(<<"type">>, Attrs) of + case fxml:get_attr_s(<<"type">>, Attrs) of <<>> -> presence_with_xupdate(Packet, User, Host); _ -> Packet end; @@ -64,7 +64,7 @@ update_presence(Packet, _User, _Host) -> Packet. vcard_set(LUser, LServer, VCARD) -> US = {LUser, LServer}, - case xml:get_path_s(VCARD, + case fxml:get_path_s(VCARD, [{elem, <<"PHOTO">>}, {elem, <<"BINVAL">>}, cdata]) of <<>> -> remove_xupdate(LUser, LServer); @@ -231,3 +231,6 @@ import(_LServer, riak, #vcard_xupdate{} = R) -> ejabberd_riak:put(R, vcard_xupdate_schema()); import(_, _, _) -> pass. + +mod_opt_type(db_type) -> fun gen_mod:v_db/1; +mod_opt_type(_) -> [db_type]. diff --git a/src/mod_version.erl b/src/mod_version.erl index e46262a2..0e3b96bd 100644 --- a/src/mod_version.erl +++ b/src/mod_version.erl @@ -5,7 +5,7 @@ %%% Created : 18 Jan 2003 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -27,9 +27,12 @@ -author('alexey@process-one.net'). +-protocol({xep, 92, '1.1'}). + -behaviour(gen_mod). --export([start/2, stop/1, process_local_iq/3]). +-export([start/2, stop/1, process_local_iq/3, + mod_opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -88,3 +91,8 @@ get_os() -> OS = <<OSType/binary, " ", OSVersion/binary>>, #xmlel{name = <<"os">>, attrs = [], children = [{xmlcdata, OS}]}. + +mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; +mod_opt_type(show_os) -> + fun (B) when is_boolean(B) -> B end; +mod_opt_type(_) -> [iqdisc, show_os]. diff --git a/src/node_buddy.erl b/src/node_buddy.erl index a6ff0d45..1ebef4e1 100644 --- a/src/node_buddy.erl +++ b/src/node_buddy.erl @@ -1,28 +1,27 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. +%%%---------------------------------------------------------------------- +%%% File : node_buddy.erl +%%% Author : Christophe Romain <christophe.romain@process-one.net> +%%% Purpose : +%%% Created : 1 Dec 2007 by Christophe Romain <christophe.romain@process-one.net> %%% %%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. +%%% ejabberd, Copyright (C) 2002-2016 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. %%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. +%%% 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. %%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Christophe Romain <christophe.romain@process-one.net> -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== +%%% 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(node_buddy). -behaviour(gen_pubsub_node). @@ -45,10 +44,10 @@ path_to_node/1]). init(Host, ServerHost, Opts) -> - node_hometree:init(Host, ServerHost, Opts). + node_flat:init(Host, ServerHost, Opts). terminate(Host, ServerHost) -> - node_hometree:terminate(Host, ServerHost). + node_flat:terminate(Host, ServerHost). options() -> [{deliver_payloads, true}, @@ -86,89 +85,89 @@ features() -> <<"subscription-notifications">>]. create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) -> - node_hometree:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). + node_flat:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). create_node(Nidx, Owner) -> - node_hometree:create_node(Nidx, Owner). + node_flat:create_node(Nidx, Owner). delete_node(Removed) -> - node_hometree:delete_node(Removed). + node_flat:delete_node(Removed). subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options) -> - node_hometree:subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, + node_flat:subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options). unsubscribe_node(Nidx, Sender, Subscriber, SubId) -> - node_hometree:unsubscribe_node(Nidx, Sender, Subscriber, SubId). + node_flat:unsubscribe_node(Nidx, Sender, Subscriber, SubId). publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload) -> - node_hometree:publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload). + node_flat:publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload). remove_extra_items(Nidx, MaxItems, ItemIds) -> - node_hometree:remove_extra_items(Nidx, MaxItems, ItemIds). + node_flat:remove_extra_items(Nidx, MaxItems, ItemIds). delete_item(Nidx, Publisher, PublishModel, ItemId) -> - node_hometree:delete_item(Nidx, Publisher, PublishModel, ItemId). + node_flat:delete_item(Nidx, Publisher, PublishModel, ItemId). purge_node(Nidx, Owner) -> - node_hometree:purge_node(Nidx, Owner). + node_flat:purge_node(Nidx, Owner). get_entity_affiliations(Host, Owner) -> - node_hometree:get_entity_affiliations(Host, Owner). + node_flat:get_entity_affiliations(Host, Owner). get_node_affiliations(Nidx) -> - node_hometree:get_node_affiliations(Nidx). + node_flat:get_node_affiliations(Nidx). get_affiliation(Nidx, Owner) -> - node_hometree:get_affiliation(Nidx, Owner). + node_flat:get_affiliation(Nidx, Owner). set_affiliation(Nidx, Owner, Affiliation) -> - node_hometree:set_affiliation(Nidx, Owner, Affiliation). + node_flat:set_affiliation(Nidx, Owner, Affiliation). get_entity_subscriptions(Host, Owner) -> - node_hometree:get_entity_subscriptions(Host, Owner). + node_flat:get_entity_subscriptions(Host, Owner). get_node_subscriptions(Nidx) -> - node_hometree:get_node_subscriptions(Nidx). + node_flat:get_node_subscriptions(Nidx). get_subscriptions(Nidx, Owner) -> - node_hometree:get_subscriptions(Nidx, Owner). + node_flat:get_subscriptions(Nidx, Owner). set_subscriptions(Nidx, Owner, Subscription, SubId) -> - node_hometree:set_subscriptions(Nidx, Owner, Subscription, SubId). + node_flat:set_subscriptions(Nidx, Owner, Subscription, SubId). get_pending_nodes(Host, Owner) -> - node_hometree:get_pending_nodes(Host, Owner). + node_flat:get_pending_nodes(Host, Owner). get_states(Nidx) -> - node_hometree:get_states(Nidx). + node_flat:get_states(Nidx). get_state(Nidx, JID) -> - node_hometree:get_state(Nidx, JID). + node_flat:get_state(Nidx, JID). set_state(State) -> - node_hometree:set_state(State). + node_flat:set_state(State). get_items(Nidx, From, RSM) -> - node_hometree:get_items(Nidx, From, RSM). + node_flat:get_items(Nidx, From, RSM). get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM) -> - node_hometree:get_items(Nidx, JID, AccessModel, + node_flat:get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM). get_item(Nidx, ItemId) -> - node_hometree:get_item(Nidx, ItemId). + node_flat:get_item(Nidx, ItemId). get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> - node_hometree:get_item(Nidx, ItemId, JID, AccessModel, + node_flat:get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId). set_item(Item) -> - node_hometree:set_item(Item). + node_flat:set_item(Item). get_item_name(Host, Node, Id) -> - node_hometree:get_item_name(Host, Node, Id). + node_flat:get_item_name(Host, Node, Id). node_to_path(Node) -> node_flat:node_to_path(Node). diff --git a/src/node_club.erl b/src/node_club.erl index 6917312f..a7ef35bd 100644 --- a/src/node_club.erl +++ b/src/node_club.erl @@ -1,28 +1,27 @@ -%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. +%%%---------------------------------------------------------------------- +%%% File : node_club.erl +%%% Author : Christophe Romain <christophe.romain@process-one.net> +%%% Purpose : +%%% Created : 1 Dec 2007 by Christophe Romain <christophe.romain@process-one.net> %%% %%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. +%%% ejabberd, Copyright (C) 2002-2016 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. %%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. +%%% 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. %%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Christophe Romain <christophe.romain@process-one.net> -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== +%%% 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(node_club). -behaviour(gen_pubsub_node). @@ -45,10 +44,10 @@ path_to_node/1]). init(Host, ServerHost, Opts) -> - node_hometree:init(Host, ServerHost, Opts). + node_flat:init(Host, ServerHost, Opts). terminate(Host, ServerHost) -> - node_hometree:terminate(Host, ServerHost). + node_flat:terminate(Host, ServerHost). options() -> [{deliver_payloads, true}, @@ -85,89 +84,89 @@ features() -> <<"subscription-notifications">>]. create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) -> - node_hometree:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). + node_flat:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). create_node(Nidx, Owner) -> - node_hometree:create_node(Nidx, Owner). + node_flat:create_node(Nidx, Owner). delete_node(Removed) -> - node_hometree:delete_node(Removed). + node_flat:delete_node(Removed). subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options) -> - node_hometree:subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, + node_flat:subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options). unsubscribe_node(Nidx, Sender, Subscriber, SubId) -> - node_hometree:unsubscribe_node(Nidx, Sender, Subscriber, SubId). + node_flat:unsubscribe_node(Nidx, Sender, Subscriber, SubId). publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload) -> - node_hometree:publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload). + node_flat:publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload). remove_extra_items(Nidx, MaxItems, ItemIds) -> - node_hometree:remove_extra_items(Nidx, MaxItems, ItemIds). + node_flat:remove_extra_items(Nidx, MaxItems, ItemIds). delete_item(Nidx, Publisher, PublishModel, ItemId) -> - node_hometree:delete_item(Nidx, Publisher, PublishModel, ItemId). + node_flat:delete_item(Nidx, Publisher, PublishModel, ItemId). purge_node(Nidx, Owner) -> - node_hometree:purge_node(Nidx, Owner). + node_flat:purge_node(Nidx, Owner). get_entity_affiliations(Host, Owner) -> - node_hometree:get_entity_affiliations(Host, Owner). + node_flat:get_entity_affiliations(Host, Owner). get_node_affiliations(Nidx) -> - node_hometree:get_node_affiliations(Nidx). + node_flat:get_node_affiliations(Nidx). get_affiliation(Nidx, Owner) -> - node_hometree:get_affiliation(Nidx, Owner). + node_flat:get_affiliation(Nidx, Owner). set_affiliation(Nidx, Owner, Affiliation) -> - node_hometree:set_affiliation(Nidx, Owner, Affiliation). + node_flat:set_affiliation(Nidx, Owner, Affiliation). get_entity_subscriptions(Host, Owner) -> - node_hometree:get_entity_subscriptions(Host, Owner). + node_flat:get_entity_subscriptions(Host, Owner). get_node_subscriptions(Nidx) -> - node_hometree:get_node_subscriptions(Nidx). + node_flat:get_node_subscriptions(Nidx). get_subscriptions(Nidx, Owner) -> - node_hometree:get_subscriptions(Nidx, Owner). + node_flat:get_subscriptions(Nidx, Owner). set_subscriptions(Nidx, Owner, Subscription, SubId) -> - node_hometree:set_subscriptions(Nidx, Owner, Subscription, SubId). + node_flat:set_subscriptions(Nidx, Owner, Subscription, SubId). get_pending_nodes(Host, Owner) -> - node_hometree:get_pending_nodes(Host, Owner). + node_flat:get_pending_nodes(Host, Owner). get_states(Nidx) -> - node_hometree:get_states(Nidx). + node_flat:get_states(Nidx). get_state(Nidx, JID) -> - node_hometree:get_state(Nidx, JID). + node_flat:get_state(Nidx, JID). set_state(State) -> - node_hometree:set_state(State). + node_flat:set_state(State). get_items(Nidx, From, RSM) -> - node_hometree:get_items(Nidx, From, RSM). + node_flat:get_items(Nidx, From, RSM). get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM) -> - node_hometree:get_items(Nidx, JID, AccessModel, + node_flat:get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM). get_item(Nidx, ItemId) -> - node_hometree:get_item(Nidx, ItemId). + node_flat:get_item(Nidx, ItemId). get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> - node_hometree:get_item(Nidx, ItemId, JID, AccessModel, + node_flat:get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId). set_item(Item) -> - node_hometree:set_item(Item). + node_flat:set_item(Item). get_item_name(Host, Node, Id) -> - node_hometree:get_item_name(Host, Node, Id). + node_flat:get_item_name(Host, Node, Id). node_to_path(Node) -> node_flat:node_to_path(Node). diff --git a/src/node_dag.erl b/src/node_dag.erl index f0cbf17b..cbb8e18c 100644 --- a/src/node_dag.erl +++ b/src/node_dag.erl @@ -1,21 +1,27 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. +%%%---------------------------------------------------------------------- +%%% File : node_dag.erl +%%% Author : Brian Cully <bjc@kublai.com> +%%% Purpose : experimental support of XEP-248 +%%% Created : 15 Jun 2009 by Brian Cully <bjc@kublai.com> %%% %%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. +%%% ejabberd, Copyright (C) 2002-2016 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. %%% -%%% @author Brian Cully <bjc@kublai.com> -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== +%%% 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(node_dag). -behaviour(gen_pubsub_node). diff --git a/src/node_dispatch.erl b/src/node_dispatch.erl index 7a123184..f9bfaf4f 100644 --- a/src/node_dispatch.erl +++ b/src/node_dispatch.erl @@ -1,28 +1,33 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. +%%%---------------------------------------------------------------------- +%%% File : node_dispatch.erl +%%% Author : Christophe Romain <christophe.romain@process-one.net> +%%% Purpose : Publish item to node and all child subnodes +%%% Created : 1 Dec 2007 by Christophe Romain <christophe.romain@process-one.net> %%% %%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. +%%% ejabberd, Copyright (C) 2002-2016 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. %%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. +%%% 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. %%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Christophe Romain <christophe.romain@process-one.net> -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== +%%% 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. +%%% +%%%---------------------------------------------------------------------- + +%%% @doc <p>The <strong>{@module}</strong> module is a PubSub plugin whose +%%% goal is to republished each published item to all its children.</p> +%%% <p>Users cannot subscribe to this node, but are supposed to subscribe to +%%% its children.</p> +%%% This module can not work with virtual nodetree -module(node_dispatch). -behaviour(gen_pubsub_node). @@ -31,12 +36,6 @@ -include("pubsub.hrl"). -include("jlib.hrl"). -%%% @doc <p>The <strong>{@module}</strong> module is a PubSub plugin whose -%%% goal is to republished each published item to all its children.</p> -%%% <p>Users cannot subscribe to this node, but are supposed to subscribe to -%%% its children.</p> -%%% This module can not work with virtual nodetree - -export([init/3, terminate/2, options/0, features/0, create_node_permission/6, create_node/2, delete_node/1, purge_node/2, subscribe_node/8, unsubscribe_node/4, @@ -179,7 +178,7 @@ get_item_name(Host, Node, Id) -> node_hometree:get_item_name(Host, Node, Id). node_to_path(Node) -> - node_flat:node_to_path(Node). + node_hometree:node_to_path(Node). path_to_node(Path) -> - node_flat:path_to_node(Path). + node_hometree:path_to_node(Path). diff --git a/src/node_flat.erl b/src/node_flat.erl index e2615438..3687bb64 100644 --- a/src/node_flat.erl +++ b/src/node_flat.erl @@ -1,28 +1,33 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. +%%%---------------------------------------------------------------------- +%%% File : node_flat.erl +%%% Author : Christophe Romain <christophe.romain@process-one.net> +%%% Purpose : Standard PubSub node plugin +%%% Created : 1 Dec 2007 by Christophe Romain <christophe.romain@process-one.net> %%% %%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. +%%% ejabberd, Copyright (C) 2002-2016 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. %%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. +%%% 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. %%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Christophe Romain <christophe.romain@process-one.net> -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== +%%% 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. +%%% +%%%---------------------------------------------------------------------- + +%%% @doc The module <strong>{@module}</strong> is the default PubSub plugin. +%%% <p>It is used as a default for all unknown PubSub node type. It can serve +%%% as a developer basis and reference to build its own custom pubsub node +%%% types.</p> +%%% <p>PubSub plugin nodes are using the {@link gen_node} behaviour.</p> -module(node_flat). -behaviour(gen_pubsub_node). @@ -42,10 +47,10 @@ get_pending_nodes/2, get_states/1, get_state/2, set_state/1, get_items/7, get_items/3, get_item/7, get_item/2, set_item/1, get_item_name/3, node_to_path/1, - path_to_node/1]). + path_to_node/1, can_fetch_item/2, is_subscribed/1]). init(_Host, _ServerHost, _Opts) -> - pubsub_subscription:init(), + %pubsub_subscription:init(), mnesia:create_table(pubsub_state, [{disc_copies, [node()]}, {type, ordered_set}, @@ -60,20 +65,56 @@ init(_Host, _ServerHost, _Opts) -> end, ok. -terminate(Host, ServerHost) -> - node_hometree:terminate(Host, ServerHost). +terminate(_Host, _ServerHost) -> + ok. options() -> - node_hometree:options(). + [{deliver_payloads, true}, + {notify_config, false}, + {notify_delete, false}, + {notify_retract, true}, + {purge_offline, false}, + {persist_items, true}, + {max_items, ?MAXITEMS}, + {subscribe, true}, + {access_model, open}, + {roster_groups_allowed, []}, + {publish_model, publishers}, + {notification_type, headline}, + {max_payload_size, ?MAX_PAYLOAD_SIZE}, + {send_last_published_item, on_sub_and_presence}, + {deliver_notifications, true}, + {presence_based_delivery, false}]. features() -> - node_hometree:features(). - -%% use same code as node_hometree, but do not limite node to -%% the home/localhost/user/... hierarchy -%% any node is allowed + [<<"create-nodes">>, + <<"auto-create">>, + <<"access-authorize">>, + <<"delete-nodes">>, + <<"delete-items">>, + <<"get-pending">>, + <<"instant-nodes">>, + <<"manage-subscriptions">>, + <<"modify-affiliations">>, + <<"outcast-affiliation">>, + <<"persistent-items">>, + <<"publish">>, + <<"publish-only-affiliation">>, + <<"purge-nodes">>, + <<"retract-items">>, + <<"retrieve-affiliations">>, + <<"retrieve-items">>, + <<"retrieve-subscriptions">>, + <<"subscribe">>, + <<"subscription-notifications">>]. +%%<<"subscription-options">> + +%% @doc Checks if the current user has the permission to create the requested node +%% <p>In flat node, any unused node name is allowed. The access parameter is also +%% checked. This parameter depends on the value of the +%% <tt>access_createnode</tt> ACL value in ejabberd config file.</p> create_node_permission(Host, ServerHost, _Node, _ParentNode, Owner, Access) -> - LOwner = jlib:jid_tolower(Owner), + LOwner = jid:tolower(Owner), Allowed = case LOwner of {<<"">>, Host, <<"">>} -> true; % pubsub service always allowed @@ -83,88 +124,684 @@ create_node_permission(Host, ServerHost, _Node, _ParentNode, Owner, Access) -> {result, Allowed}. create_node(Nidx, Owner) -> - node_hometree:create_node(Nidx, Owner). - -delete_node(Removed) -> - node_hometree:delete_node(Removed). - + OwnerKey = jid:tolower(jid:remove_resource(Owner)), + set_state(#pubsub_state{stateid = {OwnerKey, Nidx}, + affiliation = owner}), + {result, {default, broadcast}}. + +delete_node(Nodes) -> + Tr = fun (#pubsub_state{stateid = {J, _}, subscriptions = Ss}) -> + lists:map(fun (S) -> {J, S} end, Ss) + end, + Reply = lists:map(fun (#pubsub_node{id = Nidx} = PubsubNode) -> + {result, States} = get_states(Nidx), + lists:foreach(fun (#pubsub_state{stateid = {LJID, _}, items = Items}) -> + del_items(Nidx, Items), + del_state(Nidx, LJID) + end, States), + {PubsubNode, lists:flatmap(Tr, States)} + end, Nodes), + {result, {default, broadcast, Reply}}. + +%% @doc <p>Accepts or rejects subcription requests on a PubSub node.</p> +%% <p>The mechanism works as follow: +%% <ul> +%% <li>The main PubSub module prepares the subscription and passes the +%% result of the preparation as a record.</li> +%% <li>This function gets the prepared record and several other parameters and +%% can decide to:<ul> +%% <li>reject the subscription;</li> +%% <li>allow it as is, letting the main module perform the database +%% persistance;</li> +%% <li>allow it, modifying the record. The main module will store the +%% modified record;</li> +%% <li>allow it, but perform the needed persistance operations.</li></ul> +%% </li></ul></p> +%% <p>The selected behaviour depends on the return parameter: +%% <ul> +%% <li><tt>{error, Reason}</tt>: an IQ error result will be returned. No +%% subscription will actually be performed.</li> +%% <li><tt>true</tt>: Subscribe operation is allowed, based on the +%% unmodified record passed in parameter <tt>SubscribeResult</tt>. If this +%% parameter contains an error, no subscription will be performed.</li> +%% <li><tt>{true, PubsubState}</tt>: Subscribe operation is allowed, but +%% the {@link mod_pubsub:pubsubState()} record returned replaces the value +%% passed in parameter <tt>SubscribeResult</tt>.</li> +%% <li><tt>{true, done}</tt>: Subscribe operation is allowed, but the +%% {@link mod_pubsub:pubsubState()} will be considered as already stored and +%% no further persistance operation will be performed. This case is used, +%% when the plugin module is doing the persistance by itself or when it want +%% to completly disable persistance.</li></ul> +%% </p> +%% <p>In the default plugin module, the record is unchanged.</p> subscribe_node(Nidx, Sender, Subscriber, AccessModel, - SendLast, PresenceSubscription, RosterGroup, Options) -> - node_hometree:subscribe_node(Nidx, Sender, Subscriber, - AccessModel, SendLast, PresenceSubscription, - RosterGroup, Options). + SendLast, PresenceSubscription, RosterGroup, _Options) -> + SubKey = jid:tolower(Subscriber), + GenKey = jid:remove_resource(SubKey), + Authorized = jid:tolower(jid:remove_resource(Sender)) == GenKey, + GenState = get_state(Nidx, GenKey), + SubState = case SubKey of + GenKey -> GenState; + _ -> get_state(Nidx, SubKey) + end, + Affiliation = GenState#pubsub_state.affiliation, + Subscriptions = SubState#pubsub_state.subscriptions, + Whitelisted = lists:member(Affiliation, [member, publisher, owner]), + PendingSubscription = lists:any(fun + ({pending, _}) -> true; + (_) -> false + end, + Subscriptions), + Owner = Affiliation == owner, + if not Authorized -> + {error, + ?ERR_EXTENDED((?ERR_BAD_REQUEST), <<"invalid-jid">>)}; + (Affiliation == outcast) or (Affiliation == publish_only) -> + {error, ?ERR_FORBIDDEN}; + PendingSubscription -> + {error, + ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), <<"pending-subscription">>)}; + (AccessModel == presence) and (not PresenceSubscription) and (not Owner) -> + {error, + ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), <<"presence-subscription-required">>)}; + (AccessModel == roster) and (not RosterGroup) and (not Owner) -> + {error, + ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), <<"not-in-roster-group">>)}; + (AccessModel == whitelist) and (not Whitelisted) and (not Owner) -> + {error, + ?ERR_EXTENDED((?ERR_NOT_ALLOWED), <<"closed-node">>)}; + %%MustPay -> + %% % Payment is required for a subscription + %% {error, ?ERR_PAYMENT_REQUIRED}; + %%ForbiddenAnonymous -> + %% % Requesting entity is anonymous + %% {error, ?ERR_FORBIDDEN}; + true -> + %%SubId = pubsub_subscription:add_subscription(Subscriber, Nidx, Options), + {NewSub, SubId} = case Subscriptions of + [{subscribed, Id}|_] -> + {subscribed, Id}; + [] -> + Id = pubsub_subscription:make_subid(), + Sub = case AccessModel of + authorize -> pending; + _ -> subscribed + end, + set_state(SubState#pubsub_state{subscriptions = + [{Sub, Id} | Subscriptions]}), + {Sub, Id} + end, + case {NewSub, SendLast} of + {subscribed, never} -> + {result, {default, subscribed, SubId}}; + {subscribed, _} -> + {result, {default, subscribed, SubId, send_last}}; + {_, _} -> + {result, {default, pending, SubId}} + end + end. +%% @doc <p>Unsubscribe the <tt>Subscriber</tt> from the <tt>Node</tt>.</p> unsubscribe_node(Nidx, Sender, Subscriber, SubId) -> - node_hometree:unsubscribe_node(Nidx, Sender, Subscriber, SubId). + SubKey = jid:tolower(Subscriber), + GenKey = jid:remove_resource(SubKey), + Authorized = jid:tolower(jid:remove_resource(Sender)) == GenKey, + GenState = get_state(Nidx, GenKey), + SubState = case SubKey of + GenKey -> GenState; + _ -> get_state(Nidx, SubKey) + end, + Subscriptions = lists:filter(fun + ({_Sub, _SubId}) -> true; + (_SubId) -> false + end, + SubState#pubsub_state.subscriptions), + SubIdExists = case SubId of + <<>> -> false; + Binary when is_binary(Binary) -> true; + _ -> false + end, + if + %% Requesting entity is prohibited from unsubscribing entity + not Authorized -> + {error, ?ERR_FORBIDDEN}; + %% Entity did not specify SubId + %%SubId == "", ?? -> + %% {error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; + %% Invalid subscription identifier + %%InvalidSubId -> + %% {error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; + %% Requesting entity is not a subscriber + Subscriptions == [] -> + {error, + ?ERR_EXTENDED((?ERR_UNEXPECTED_REQUEST_CANCEL), <<"not-subscribed">>)}; + %% Subid supplied, so use that. + SubIdExists -> + Sub = first_in_list(fun + ({_, S}) when S == SubId -> true; + (_) -> false + end, + SubState#pubsub_state.subscriptions), + case Sub of + {value, S} -> + delete_subscriptions(SubKey, Nidx, [S], SubState), + {result, default}; + false -> + {error, + ?ERR_EXTENDED((?ERR_UNEXPECTED_REQUEST_CANCEL), <<"not-subscribed">>)} + end; + %% Asking to remove all subscriptions to the given node + SubId == all -> + delete_subscriptions(SubKey, Nidx, Subscriptions, SubState), + {result, default}; + %% No subid supplied, but there's only one matching subscription + length(Subscriptions) == 1 -> + delete_subscriptions(SubKey, Nidx, Subscriptions, SubState), + {result, default}; + %% No subid and more than one possible subscription match. + true -> + {error, + ?ERR_EXTENDED((?ERR_BAD_REQUEST), <<"subid-required">>)} + end. -publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload) -> - node_hometree:publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload). +delete_subscriptions(SubKey, Nidx, Subscriptions, SubState) -> + NewSubs = lists:foldl(fun ({Subscription, SubId}, Acc) -> + %%pubsub_subscription:delete_subscription(SubKey, Nidx, SubId), + Acc -- [{Subscription, SubId}] + end, SubState#pubsub_state.subscriptions, Subscriptions), + case {SubState#pubsub_state.affiliation, NewSubs} of + {none, []} -> del_state(Nidx, SubKey); + _ -> set_state(SubState#pubsub_state{subscriptions = NewSubs}) + end. -remove_extra_items(Nidx, MaxItems, ItemIds) -> - node_hometree:remove_extra_items(Nidx, MaxItems, ItemIds). +%% @doc <p>Publishes the item passed as parameter.</p> +%% <p>The mechanism works as follow: +%% <ul> +%% <li>The main PubSub module prepares the item to publish and passes the +%% result of the preparation as a {@link mod_pubsub:pubsubItem()} record.</li> +%% <li>This function gets the prepared record and several other parameters and can decide to:<ul> +%% <li>reject the publication;</li> +%% <li>allow the publication as is, letting the main module perform the database persistance;</li> +%% <li>allow the publication, modifying the record. The main module will store the modified record;</li> +%% <li>allow it, but perform the needed persistance operations.</li></ul> +%% </li></ul></p> +%% <p>The selected behaviour depends on the return parameter: +%% <ul> +%% <li><tt>{error, Reason}</tt>: an iq error result will be return. No +%% publication is actually performed.</li> +%% <li><tt>true</tt>: Publication operation is allowed, based on the +%% unmodified record passed in parameter <tt>Item</tt>. If the <tt>Item</tt> +%% parameter contains an error, no subscription will actually be +%% performed.</li> +%% <li><tt>{true, Item}</tt>: Publication operation is allowed, but the +%% {@link mod_pubsub:pubsubItem()} record returned replaces the value passed +%% in parameter <tt>Item</tt>. The persistance will be performed by the main +%% module.</li> +%% <li><tt>{true, done}</tt>: Publication operation is allowed, but the +%% {@link mod_pubsub:pubsubItem()} will be considered as already stored and +%% no further persistance operation will be performed. This case is used, +%% when the plugin module is doing the persistance by itself or when it want +%% to completly disable persistance.</li></ul> +%% </p> +%% <p>In the default plugin module, the record is unchanged.</p> +publish_item(Nidx, Publisher, PublishModel, MaxItems, ItemId, Payload) -> + SubKey = jid:tolower(Publisher), + GenKey = jid:remove_resource(SubKey), + GenState = get_state(Nidx, GenKey), + SubState = case SubKey of + GenKey -> GenState; + _ -> get_state(Nidx, SubKey) + end, + Affiliation = GenState#pubsub_state.affiliation, + Subscribed = case PublishModel of + subscribers -> is_subscribed(GenState#pubsub_state.subscriptions) orelse + is_subscribed(SubState#pubsub_state.subscriptions); + _ -> undefined + end, + if not ((PublishModel == open) or + (PublishModel == publishers) and + ((Affiliation == owner) + or (Affiliation == publisher) + or (Affiliation == publish_only)) + or (Subscribed == true)) -> + {error, ?ERR_FORBIDDEN}; + true -> + if MaxItems > 0 -> + Now = p1_time_compat:timestamp(), + PubId = {Now, SubKey}, + Item = case get_item(Nidx, ItemId) of + {result, OldItem} -> + OldItem#pubsub_item{modification = PubId, + payload = Payload}; + _ -> + #pubsub_item{itemid = {ItemId, Nidx}, + creation = {Now, GenKey}, + modification = PubId, + payload = Payload} + end, + Items = [ItemId | GenState#pubsub_state.items -- [ItemId]], + {result, {NI, OI}} = remove_extra_items(Nidx, MaxItems, Items), + set_item(Item), + set_state(GenState#pubsub_state{items = NI}), + {result, {default, broadcast, OI}}; + true -> + {result, {default, broadcast, []}} + end + end. +%% @doc <p>This function is used to remove extra items, most notably when the +%% maximum number of items has been reached.</p> +%% <p>This function is used internally by the core PubSub module, as no +%% permission check is performed.</p> +%% <p>In the default plugin module, the oldest items are removed, but other +%% rules can be used.</p> +%% <p>If another PubSub plugin wants to delegate the item removal (and if the +%% plugin is using the default pubsub storage), it can implements this function like this: +%% ```remove_extra_items(Nidx, MaxItems, ItemIds) -> +%% node_default:remove_extra_items(Nidx, MaxItems, ItemIds).'''</p> +remove_extra_items(_Nidx, unlimited, ItemIds) -> + {result, {ItemIds, []}}; +remove_extra_items(Nidx, MaxItems, ItemIds) -> + NewItems = lists:sublist(ItemIds, MaxItems), + OldItems = lists:nthtail(length(NewItems), ItemIds), + del_items(Nidx, OldItems), + {result, {NewItems, OldItems}}. + +%% @doc <p>Triggers item deletion.</p> +%% <p>Default plugin: The user performing the deletion must be the node owner +%% or a publisher, or PublishModel being open.</p> delete_item(Nidx, Publisher, PublishModel, ItemId) -> - node_hometree:delete_item(Nidx, Publisher, PublishModel, ItemId). + SubKey = jid:tolower(Publisher), + GenKey = jid:remove_resource(SubKey), + GenState = get_state(Nidx, GenKey), + #pubsub_state{affiliation = Affiliation, items = Items} = GenState, + Allowed = Affiliation == publisher orelse + Affiliation == owner orelse + PublishModel == open orelse + case get_item(Nidx, ItemId) of + {result, #pubsub_item{creation = {_, GenKey}}} -> true; + _ -> false + end, + if not Allowed -> + {error, ?ERR_FORBIDDEN}; + true -> + case lists:member(ItemId, Items) of + true -> + del_item(Nidx, ItemId), + set_state(GenState#pubsub_state{items = lists:delete(ItemId, Items)}), + {result, {default, broadcast}}; + false -> + case Affiliation of + owner -> + {result, States} = get_states(Nidx), + lists:foldl(fun + (#pubsub_state{items = PI} = S, Res) -> + case lists:member(ItemId, PI) of + true -> + Nitems = lists:delete(ItemId, PI), + del_item(Nidx, ItemId), + set_state(S#pubsub_state{items = Nitems}), + {result, {default, broadcast}}; + false -> + Res + end; + (_, Res) -> + Res + end, + {error, ?ERR_ITEM_NOT_FOUND}, States); + _ -> + {error, ?ERR_ITEM_NOT_FOUND} + end + end + end. purge_node(Nidx, Owner) -> - node_hometree:purge_node(Nidx, Owner). + SubKey = jid:tolower(Owner), + GenKey = jid:remove_resource(SubKey), + GenState = get_state(Nidx, GenKey), + case GenState of + #pubsub_state{affiliation = owner} -> + {result, States} = get_states(Nidx), + lists:foreach(fun + (#pubsub_state{items = []}) -> + ok; + (#pubsub_state{items = Items} = S) -> + del_items(Nidx, Items), + set_state(S#pubsub_state{items = []}) + end, + States), + {result, {default, broadcast}}; + _ -> + {error, ?ERR_FORBIDDEN} + end. +%% @doc <p>Return the current affiliations for the given user</p> +%% <p>The default module reads affiliations in the main Mnesia +%% <tt>pubsub_state</tt> table. If a plugin stores its data in the same +%% table, it should return an empty list, as the affiliation will be read by +%% the default PubSub module. Otherwise, it should return its own affiliation, +%% that will be added to the affiliation stored in the main +%% <tt>pubsub_state</tt> table.</p> get_entity_affiliations(Host, Owner) -> - node_hometree:get_entity_affiliations(Host, Owner). + SubKey = jid:tolower(Owner), + GenKey = jid:remove_resource(SubKey), + States = mnesia:match_object(#pubsub_state{stateid = {GenKey, '_'}, _ = '_'}), + NodeTree = mod_pubsub:tree(Host), + Reply = lists:foldl(fun (#pubsub_state{stateid = {_, N}, affiliation = A}, Acc) -> + case NodeTree:get_node(N) of + #pubsub_node{nodeid = {Host, _}} = Node -> [{Node, A} | Acc]; + _ -> Acc + end + end, + [], States), + {result, Reply}. get_node_affiliations(Nidx) -> - node_hometree:get_node_affiliations(Nidx). + {result, States} = get_states(Nidx), + Tr = fun (#pubsub_state{stateid = {J, _}, affiliation = A}) -> {J, A} end, + {result, lists:map(Tr, States)}. get_affiliation(Nidx, Owner) -> - node_hometree:get_affiliation(Nidx, Owner). + SubKey = jid:tolower(Owner), + GenKey = jid:remove_resource(SubKey), + #pubsub_state{affiliation = Affiliation} = get_state(Nidx, GenKey), + {result, Affiliation}. set_affiliation(Nidx, Owner, Affiliation) -> - node_hometree:set_affiliation(Nidx, Owner, Affiliation). + SubKey = jid:tolower(Owner), + GenKey = jid:remove_resource(SubKey), + GenState = get_state(Nidx, GenKey), + case {Affiliation, GenState#pubsub_state.subscriptions} of + {none, []} -> del_state(Nidx, GenKey); + _ -> set_state(GenState#pubsub_state{affiliation = Affiliation}) + end. +%% @doc <p>Return the current subscriptions for the given user</p> +%% <p>The default module reads subscriptions in the main Mnesia +%% <tt>pubsub_state</tt> table. If a plugin stores its data in the same +%% table, it should return an empty list, as the affiliation will be read by +%% the default PubSub module. Otherwise, it should return its own affiliation, +%% that will be added to the affiliation stored in the main +%% <tt>pubsub_state</tt> table.</p> get_entity_subscriptions(Host, Owner) -> - node_hometree:get_entity_subscriptions(Host, Owner). + {U, D, _} = SubKey = jid:tolower(Owner), + GenKey = jid:remove_resource(SubKey), + States = case SubKey of + GenKey -> + mnesia:match_object(#pubsub_state{stateid = {{U, D, '_'}, '_'}, _ = '_'}); + _ -> + mnesia:match_object(#pubsub_state{stateid = {GenKey, '_'}, _ = '_'}) + ++ + mnesia:match_object(#pubsub_state{stateid = {SubKey, '_'}, _ = '_'}) + end, + NodeTree = mod_pubsub:tree(Host), + Reply = lists:foldl(fun (#pubsub_state{stateid = {J, N}, subscriptions = Ss}, Acc) -> + case NodeTree:get_node(N) of + #pubsub_node{nodeid = {Host, _}} = Node -> + lists:foldl(fun ({Sub, SubId}, Acc2) -> + [{Node, Sub, SubId, J} | Acc2] + end, + Acc, Ss); + _ -> + Acc + end + end, + [], States), + {result, Reply}. get_node_subscriptions(Nidx) -> - node_hometree:get_node_subscriptions(Nidx). + {result, States} = get_states(Nidx), + Tr = fun (#pubsub_state{stateid = {J, _}, subscriptions = Subscriptions}) -> + case Subscriptions of + [_ | _] -> + lists:foldl(fun ({S, SubId}, Acc) -> + [{J, S, SubId} | Acc] + end, + [], Subscriptions); + [] -> + []; + _ -> + [{J, none}] + end + end, + {result, lists:flatmap(Tr, States)}. get_subscriptions(Nidx, Owner) -> - node_hometree:get_subscriptions(Nidx, Owner). + SubKey = jid:tolower(Owner), + SubState = get_state(Nidx, SubKey), + {result, SubState#pubsub_state.subscriptions}. set_subscriptions(Nidx, Owner, Subscription, SubId) -> - node_hometree:set_subscriptions(Nidx, Owner, Subscription, SubId). + SubKey = jid:tolower(Owner), + SubState = get_state(Nidx, SubKey), + case {SubId, SubState#pubsub_state.subscriptions} of + {_, []} -> + case Subscription of + none -> + {error, + ?ERR_EXTENDED((?ERR_BAD_REQUEST), <<"not-subscribed">>)}; + _ -> + new_subscription(Nidx, Owner, Subscription, SubState) + end; + {<<>>, [{_, SID}]} -> + case Subscription of + none -> unsub_with_subid(Nidx, SID, SubState); + _ -> replace_subscription({Subscription, SID}, SubState) + end; + {<<>>, [_ | _]} -> + {error, + ?ERR_EXTENDED((?ERR_BAD_REQUEST), <<"subid-required">>)}; + _ -> + case Subscription of + none -> unsub_with_subid(Nidx, SubId, SubState); + _ -> replace_subscription({Subscription, SubId}, SubState) + end + end. +replace_subscription(NewSub, SubState) -> + NewSubs = replace_subscription(NewSub, SubState#pubsub_state.subscriptions, []), + set_state(SubState#pubsub_state{subscriptions = NewSubs}). + +replace_subscription(_, [], Acc) -> Acc; +replace_subscription({Sub, SubId}, [{_, SubId} | T], Acc) -> + replace_subscription({Sub, SubId}, T, [{Sub, SubId} | Acc]). + +new_subscription(_Nidx, _Owner, Sub, SubState) -> + %%SubId = pubsub_subscription:add_subscription(Owner, Nidx, []), + SubId = pubsub_subscription:make_subid(), + Subs = SubState#pubsub_state.subscriptions, + set_state(SubState#pubsub_state{subscriptions = [{Sub, SubId} | Subs]}), + {Sub, SubId}. + +unsub_with_subid(Nidx, SubId, #pubsub_state{stateid = {Entity, _}} = SubState) -> + %%pubsub_subscription:delete_subscription(SubState#pubsub_state.stateid, Nidx, SubId), + NewSubs = [{S, Sid} + || {S, Sid} <- SubState#pubsub_state.subscriptions, + SubId =/= Sid], + case {NewSubs, SubState#pubsub_state.affiliation} of + {[], none} -> del_state(Nidx, Entity); + _ -> set_state(SubState#pubsub_state{subscriptions = NewSubs}) + end. + +%% @doc <p>Returns a list of Owner's nodes on Host with pending +%% subscriptions.</p> get_pending_nodes(Host, Owner) -> - node_hometree:get_pending_nodes(Host, Owner). + GenKey = jid:remove_resource(jid:tolower(Owner)), + States = mnesia:match_object(#pubsub_state{stateid = {GenKey, '_'}, + affiliation = owner, + _ = '_'}), + NodeIdxs = [Nidx || #pubsub_state{stateid = {_, Nidx}} <- States], + NodeTree = mod_pubsub:tree(Host), + Reply = mnesia:foldl(fun (#pubsub_state{stateid = {_, Nidx}} = S, Acc) -> + case lists:member(Nidx, NodeIdxs) of + true -> + case get_nodes_helper(NodeTree, S) of + {value, Node} -> [Node | Acc]; + false -> Acc + end; + false -> + Acc + end + end, + [], pubsub_state), + {result, Reply}. + +get_nodes_helper(NodeTree, #pubsub_state{stateid = {_, N}, subscriptions = Subs}) -> + HasPending = fun + ({pending, _}) -> true; + (pending) -> true; + (_) -> false + end, + case lists:any(HasPending, Subs) of + true -> + case NodeTree:get_node(N) of + #pubsub_node{nodeid = {_, Node}} -> {value, Node}; + _ -> false + end; + false -> + false + end. +%% @doc Returns the list of stored states for a given node. +%% <p>For the default PubSub module, states are stored in Mnesia database.</p> +%% <p>We can consider that the pubsub_state table have been created by the main +%% mod_pubsub module.</p> +%% <p>PubSub plugins can store the states where they wants (for example in a +%% relational database).</p> +%% <p>If a PubSub plugin wants to delegate the states storage to the default node, +%% they can implement this function like this: +%% ```get_states(Nidx) -> +%% node_default:get_states(Nidx).'''</p> get_states(Nidx) -> - node_hometree:get_states(Nidx). + States = case catch mnesia:match_object( + #pubsub_state{stateid = {'_', Nidx}, _ = '_'}) of + List when is_list(List) -> List; + _ -> [] + end, + {result, States}. + +%% @doc <p>Returns a state (one state list), given its reference.</p> +get_state(Nidx, Key) -> + StateId = {Key, Nidx}, + case catch mnesia:read({pubsub_state, StateId}) of + [State] when is_record(State, pubsub_state) -> State; + _ -> #pubsub_state{stateid = StateId} + end. -get_state(Nidx, JID) -> - node_hometree:get_state(Nidx, JID). +%% @doc <p>Write a state into database.</p> +set_state(State) when is_record(State, pubsub_state) -> + mnesia:write(State). +%set_state(_) -> {error, ?ERR_INTERNAL_SERVER_ERROR}. + +%% @doc <p>Delete a state from database.</p> +del_state(Nidx, Key) -> + mnesia:delete({pubsub_state, {Key, Nidx}}). + +%% @doc Returns the list of stored items for a given node. +%% <p>For the default PubSub module, items are stored in Mnesia database.</p> +%% <p>We can consider that the pubsub_item table have been created by the main +%% mod_pubsub module.</p> +%% <p>PubSub plugins can store the items where they wants (for example in a +%% relational database), or they can even decide not to persist any items.</p> +get_items(Nidx, _From, _RSM) -> + Items = mnesia:match_object(#pubsub_item{itemid = {'_', Nidx}, _ = '_'}), + {result, {lists:reverse(lists:keysort(#pubsub_item.modification, Items)), none}}. + +get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId, RSM) -> + SubKey = jid:tolower(JID), + GenKey = jid:remove_resource(SubKey), + GenState = get_state(Nidx, GenKey), + SubState = get_state(Nidx, SubKey), + Affiliation = GenState#pubsub_state.affiliation, + BareSubscriptions = GenState#pubsub_state.subscriptions, + FullSubscriptions = SubState#pubsub_state.subscriptions, + Whitelisted = can_fetch_item(Affiliation, BareSubscriptions) orelse + can_fetch_item(Affiliation, FullSubscriptions), + if %%SubId == "", ?? -> + %% Entity has multiple subscriptions to the node but does not specify a subscription ID + %{error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; + %%InvalidSubId -> + %% Entity is subscribed but specifies an invalid subscription ID + %{error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; + (Affiliation == outcast) or (Affiliation == publish_only) -> + {error, ?ERR_FORBIDDEN}; + (AccessModel == presence) and not PresenceSubscription -> + {error, + ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), <<"presence-subscription-required">>)}; + (AccessModel == roster) and not RosterGroup -> + {error, + ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), <<"not-in-roster-group">>)}; + (AccessModel == whitelist) and not Whitelisted -> + {error, + ?ERR_EXTENDED((?ERR_NOT_ALLOWED), <<"closed-node">>)}; + (AccessModel == authorize) and not Whitelisted -> + {error, ?ERR_FORBIDDEN}; + %%MustPay -> + %% % Payment is required for a subscription + %% {error, ?ERR_PAYMENT_REQUIRED}; + true -> + get_items(Nidx, JID, RSM) + end. -set_state(State) -> - node_hometree:set_state(State). +%% @doc <p>Returns an item (one item list), given its reference.</p> -get_items(Nidx, From, RSM) -> - node_hometree:get_items(Nidx, From, RSM). +get_item(Nidx, ItemId) -> + case mnesia:read({pubsub_item, {ItemId, Nidx}}) of + [Item] when is_record(Item, pubsub_item) -> {result, Item}; + _ -> {error, ?ERR_ITEM_NOT_FOUND} + end. -get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM) -> - node_hometree:get_items(Nidx, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId, RSM). +get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId) -> + SubKey = jid:tolower(JID), + GenKey = jid:remove_resource(SubKey), + GenState = get_state(Nidx, GenKey), + Affiliation = GenState#pubsub_state.affiliation, + Subscriptions = GenState#pubsub_state.subscriptions, + Whitelisted = can_fetch_item(Affiliation, Subscriptions), + if %%SubId == "", ?? -> + %% Entity has multiple subscriptions to the node but does not specify a subscription ID + %{error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; + %%InvalidSubId -> + %% Entity is subscribed but specifies an invalid subscription ID + %{error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; + (Affiliation == outcast) or (Affiliation == publish_only) -> + {error, ?ERR_FORBIDDEN}; + (AccessModel == presence) and not PresenceSubscription -> + {error, + ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), <<"presence-subscription-required">>)}; + (AccessModel == roster) and not RosterGroup -> + {error, + ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), <<"not-in-roster-group">>)}; + (AccessModel == whitelist) and not Whitelisted -> + {error, + ?ERR_EXTENDED((?ERR_NOT_ALLOWED), <<"closed-node">>)}; + (AccessModel == authorize) and not Whitelisted -> + {error, ?ERR_FORBIDDEN}; + %%MustPay -> + %% % Payment is required for a subscription + %% {error, ?ERR_PAYMENT_REQUIRED}; + true -> + get_item(Nidx, ItemId) + end. -get_item(Nidx, ItemId) -> - node_hometree:get_item(Nidx, ItemId). +%% @doc <p>Write an item into database.</p> +set_item(Item) when is_record(Item, pubsub_item) -> + mnesia:write(Item). +%set_item(_) -> {error, ?ERR_INTERNAL_SERVER_ERROR}. -get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> - node_hometree:get_item(Nidx, ItemId, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId). +%% @doc <p>Delete an item from database.</p> +del_item(Nidx, ItemId) -> + mnesia:delete({pubsub_item, {ItemId, Nidx}}). -set_item(Item) -> - node_hometree:set_item(Item). +del_items(Nidx, ItemIds) -> + lists:foreach(fun (ItemId) -> del_item(Nidx, ItemId) + end, + ItemIds). -get_item_name(Host, Node, Id) -> - node_hometree:get_item_name(Host, Node, Id). +get_item_name(_Host, _Node, Id) -> + Id. +%% @doc <p>Return the path of the node. In flat it's just node id.</p> node_to_path(Node) -> [(Node)]. @@ -178,3 +815,26 @@ path_to_node(Path) -> % default case (used by PEP for example) _ -> iolist_to_binary(Path) end. + +can_fetch_item(owner, _) -> true; +can_fetch_item(member, _) -> true; +can_fetch_item(publisher, _) -> true; +can_fetch_item(publish_only, _) -> false; +can_fetch_item(outcast, _) -> false; +can_fetch_item(none, Subscriptions) -> is_subscribed(Subscriptions). +%can_fetch_item(_Affiliation, _Subscription) -> false. + +is_subscribed(Subscriptions) -> + lists:any(fun + ({subscribed, _SubId}) -> true; + (_) -> false + end, + Subscriptions). + +first_in_list(_Pred, []) -> + false; +first_in_list(Pred, [H | T]) -> + case Pred(H) of + true -> {value, H}; + _ -> first_in_list(Pred, T) + end. diff --git a/src/node_flat_odbc.erl b/src/node_flat_odbc.erl index 9ac2643f..e3c57938 100644 --- a/src/node_flat_odbc.erl +++ b/src/node_flat_odbc.erl @@ -1,28 +1,33 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. +%%%---------------------------------------------------------------------- +%%% File : node_flat_odbc.erl +%%% Author : Christophe Romain <christophe.romain@process-one.net> +%%% Purpose : Standard PubSub node plugin with ODBC backend +%%% Created : 1 Dec 2007 by Christophe Romain <christophe.romain@process-one.net> %%% %%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. +%%% ejabberd, Copyright (C) 2002-2016 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. %%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. +%%% 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. %%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Christophe Romain <christophe.romain@process-one.net> -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== +%%% 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. +%%% +%%%---------------------------------------------------------------------- + +%%% @doc The module <strong>{@module}</strong> is the default PubSub plugin. +%%% <p>It is used as a default for all unknown PubSub node type. It can serve +%%% as a developer basis and reference to build its own custom pubsub node +%%% types.</p> +%%% <p>PubSub plugin nodes are using the {@link gen_node} behaviour.</p> -module(node_flat_odbc). -behaviour(gen_pubsub_node). @@ -45,11 +50,17 @@ path_to_node/1, get_entity_subscriptions_for_send_last/2, get_last_items/3]). -init(Host, ServerHost, Opts) -> - node_hometree_odbc:init(Host, ServerHost, Opts). +-export([decode_jid/1, encode_jid/1, + decode_affiliation/1, decode_subscriptions/1, + encode_affiliation/1, encode_subscriptions/1, + encode_host/1]). -terminate(Host, ServerHost) -> - node_hometree_odbc:terminate(Host, ServerHost). +init(_Host, _ServerHost, _Opts) -> + %%pubsub_subscription_odbc:init(), + ok. + +terminate(_Host, _ServerHost) -> + ok. options() -> [{odbc, true}, {rsm, true} | node_flat:options()]. @@ -57,110 +68,984 @@ options() -> features() -> [<<"rsm">> | node_flat:features()]. -%% use same code as node_hometree_odbc, but do not limite node to -%% the home/localhost/user/... hierarchy -%% any node is allowed create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) -> node_flat:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). create_node(Nidx, Owner) -> - node_hometree_odbc:create_node(Nidx, Owner). + {_U, _S, _R} = OwnerKey = jid:tolower(jid:remove_resource(Owner)), + State = #pubsub_state{stateid = {OwnerKey, Nidx}, affiliation = owner}, + catch ejabberd_odbc:sql_query_t([<<"insert into pubsub_state(nodeid, jid, affiliation, subscriptions) " + "values(">>, state_to_raw(Nidx, State), <<");">>]), + {result, {default, broadcast}}. -delete_node(Removed) -> - node_hometree_odbc:delete_node(Removed). +delete_node(Nodes) -> + Reply = lists:map(fun (#pubsub_node{id = Nidx} = PubsubNode) -> + Subscriptions = case catch + ejabberd_odbc:sql_query_t([<<"select jid, subscriptions " + "from pubsub_state where nodeid='">>, Nidx, <<"';">>]) + of + {selected, [<<"jid">>, <<"subscriptions">>], RItems} -> + [{decode_jid(SJID), decode_subscriptions(Subs)} || [SJID, Subs] <- RItems]; + _ -> + [] + end, + {PubsubNode, Subscriptions} + end, Nodes), + {result, {default, broadcast, Reply}}. subscribe_node(Nidx, Sender, Subscriber, AccessModel, - SendLast, PresenceSubscription, RosterGroup, Options) -> - node_hometree_odbc:subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, - PresenceSubscription, RosterGroup, Options). + SendLast, PresenceSubscription, RosterGroup, _Options) -> + SubKey = jid:tolower(Subscriber), + GenKey = jid:remove_resource(SubKey), + Authorized = jid:tolower(jid:remove_resource(Sender)) == GenKey, + {Affiliation, Subscriptions} = select_affiliation_subscriptions(Nidx, GenKey, SubKey), + Whitelisted = lists:member(Affiliation, [member, publisher, owner]), + PendingSubscription = lists:any(fun + ({pending, _}) -> true; + (_) -> false + end, + Subscriptions), + Owner = Affiliation == owner, + if not Authorized -> + {error, + ?ERR_EXTENDED((?ERR_BAD_REQUEST), <<"invalid-jid">>)}; + (Affiliation == outcast) or (Affiliation == publish_only) -> + {error, ?ERR_FORBIDDEN}; + PendingSubscription -> + {error, + ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), <<"pending-subscription">>)}; + (AccessModel == presence) and (not PresenceSubscription) and (not Owner) -> + {error, + ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), <<"presence-subscription-required">>)}; + (AccessModel == roster) and (not RosterGroup) and (not Owner) -> + {error, + ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), <<"not-in-roster-group">>)}; + (AccessModel == whitelist) and (not Whitelisted) and (not Owner) -> + {error, + ?ERR_EXTENDED((?ERR_NOT_ALLOWED), <<"closed-node">>)}; + %%MustPay -> + %% % Payment is required for a subscription + %% {error, ?ERR_PAYMENT_REQUIRED}; + %%ForbiddenAnonymous -> + %% % Requesting entity is anonymous + %% {error, ?ERR_FORBIDDEN}; + true -> + %%{result, SubId} = pubsub_subscription_odbc:subscribe_node(Subscriber, Nidx, Options), + {NewSub, SubId} = case Subscriptions of + [{subscribed, Id}|_] -> + {subscribed, Id}; + [] -> + Id = pubsub_subscription_odbc:make_subid(), + Sub = case AccessModel of + authorize -> pending; + _ -> subscribed + end, + update_subscription(Nidx, SubKey, [{Sub, Id} | Subscriptions]), + {Sub, Id} + end, + case {NewSub, SendLast} of + {subscribed, never} -> + {result, {default, subscribed, SubId}}; + {subscribed, _} -> + {result, {default, subscribed, SubId, send_last}}; + {_, _} -> + {result, {default, pending, SubId}} + end + end. unsubscribe_node(Nidx, Sender, Subscriber, SubId) -> - node_hometree_odbc:unsubscribe_node(Nidx, Sender, Subscriber, SubId). + SubKey = jid:tolower(Subscriber), + GenKey = jid:remove_resource(SubKey), + Authorized = jid:tolower(jid:remove_resource(Sender)) == GenKey, + {Affiliation, Subscriptions} = select_affiliation_subscriptions(Nidx, SubKey), + SubIdExists = case SubId of + <<>> -> false; + Binary when is_binary(Binary) -> true; + _ -> false + end, + if + %% Requesting entity is prohibited from unsubscribing entity + not Authorized -> + {error, ?ERR_FORBIDDEN}; + %% Entity did not specify SubId + %%SubId == "", ?? -> + %% {error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; + %% Invalid subscription identifier + %%InvalidSubId -> + %% {error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; + %% Requesting entity is not a subscriber + Subscriptions == [] -> + {error, + ?ERR_EXTENDED((?ERR_UNEXPECTED_REQUEST_CANCEL), <<"not-subscribed">>)}; + %% Subid supplied, so use that. + SubIdExists -> + Sub = first_in_list(fun + ({_, S}) when S == SubId -> true; + (_) -> false + end, + Subscriptions), + case Sub of + {value, S} -> + delete_subscription(SubKey, Nidx, S, Affiliation, Subscriptions), + {result, default}; + false -> + {error, + ?ERR_EXTENDED((?ERR_UNEXPECTED_REQUEST_CANCEL), <<"not-subscribed">>)} + end; + %% Asking to remove all subscriptions to the given node + SubId == all -> + [delete_subscription(SubKey, Nidx, S, Affiliation, Subscriptions) + || S <- Subscriptions], + {result, default}; + %% No subid supplied, but there's only one matching subscription + length(Subscriptions) == 1 -> + delete_subscription(SubKey, Nidx, hd(Subscriptions), Affiliation, Subscriptions), + {result, default}; + %% No subid and more than one possible subscription match. + true -> + {error, + ?ERR_EXTENDED((?ERR_BAD_REQUEST), <<"subid-required">>)} + end. + +delete_subscription(SubKey, Nidx, {Subscription, SubId}, Affiliation, Subscriptions) -> + NewSubs = Subscriptions -- [{Subscription, SubId}], + %%pubsub_subscription_odbc:unsubscribe_node(SubKey, Nidx, SubId), + case {Affiliation, NewSubs} of + {none, []} -> del_state(Nidx, SubKey); + _ -> update_subscription(Nidx, SubKey, NewSubs) + end. -publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload) -> - node_hometree_odbc:publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload). +publish_item(Nidx, Publisher, PublishModel, MaxItems, ItemId, Payload) -> + SubKey = jid:tolower(Publisher), + GenKey = jid:remove_resource(SubKey), + {Affiliation, Subscriptions} = select_affiliation_subscriptions(Nidx, GenKey, SubKey), + Subscribed = case PublishModel of + subscribers -> node_flat:is_subscribed(Subscriptions); + _ -> undefined + end, + if not ((PublishModel == open) or + (PublishModel == publishers) and + ((Affiliation == owner) + or (Affiliation == publisher) + or (Affiliation == publish_only)) + or (Subscribed == true)) -> + {error, ?ERR_FORBIDDEN}; + true -> + if MaxItems > 0 -> + PubId = {p1_time_compat:timestamp(), SubKey}, + set_item(#pubsub_item{itemid = {ItemId, Nidx}, + creation = {p1_time_compat:timestamp(), GenKey}, + modification = PubId, + payload = Payload}), + Items = [ItemId | itemids(Nidx, GenKey) -- [ItemId]], + {result, {_, OI}} = remove_extra_items(Nidx, MaxItems, Items), + {result, {default, broadcast, OI}}; + true -> + {result, {default, broadcast, []}} + end + end. +remove_extra_items(_Nidx, unlimited, ItemIds) -> + {result, {ItemIds, []}}; remove_extra_items(Nidx, MaxItems, ItemIds) -> - node_hometree_odbc:remove_extra_items(Nidx, MaxItems, ItemIds). + NewItems = lists:sublist(ItemIds, MaxItems), + OldItems = lists:nthtail(length(NewItems), ItemIds), + del_items(Nidx, OldItems), + {result, {NewItems, OldItems}}. delete_item(Nidx, Publisher, PublishModel, ItemId) -> - node_hometree_odbc:delete_item(Nidx, Publisher, PublishModel, ItemId). + SubKey = jid:tolower(Publisher), + GenKey = jid:remove_resource(SubKey), + {result, Affiliation} = get_affiliation(Nidx, GenKey), + Allowed = Affiliation == publisher orelse + Affiliation == owner orelse + PublishModel == open orelse + case get_item(Nidx, ItemId) of + {result, #pubsub_item{creation = {_, GenKey}}} -> true; + _ -> false + end, + if not Allowed -> + {error, ?ERR_FORBIDDEN}; + true -> + case del_item(Nidx, ItemId) of + {updated, 1} -> {result, {default, broadcast}}; + _ -> {error, ?ERR_ITEM_NOT_FOUND} + end + end. purge_node(Nidx, Owner) -> - node_hometree_odbc:purge_node(Nidx, Owner). + SubKey = jid:tolower(Owner), + GenKey = jid:remove_resource(SubKey), + GenState = get_state(Nidx, GenKey), + case GenState of + #pubsub_state{affiliation = owner} -> + {result, States} = get_states(Nidx), + lists:foreach(fun + (#pubsub_state{items = []}) -> ok; + (#pubsub_state{items = Items}) -> del_items(Nidx, Items) + end, + States), + {result, {default, broadcast}}; + _ -> + {error, ?ERR_FORBIDDEN} + end. get_entity_affiliations(Host, Owner) -> - node_hometree_odbc:get_entity_affiliations(Host, Owner). + SubKey = jid:tolower(Owner), + GenKey = jid:remove_resource(SubKey), + H = encode_host(Host), + J = encode_jid(GenKey), + Reply = case catch + ejabberd_odbc:sql_query_t([<<"select node, type, i.nodeid, affiliation " + "from pubsub_state i, pubsub_node n where " + "i.nodeid = n.nodeid and jid='">>, J, <<"' and host='">>, H, <<"';">>]) + of + {selected, [<<"node">>, <<"type">>, <<"nodeid">>, <<"affiliation">>], RItems} -> + [{nodetree_tree_odbc:raw_to_node(Host, [N, <<"">>, T, I]), decode_affiliation(A)} + || [N, T, I, A] <- RItems]; + _ -> + [] + end, + {result, Reply}. get_node_affiliations(Nidx) -> - node_hometree_odbc:get_node_affiliations(Nidx). + Reply = case catch + ejabberd_odbc:sql_query_t([<<"select jid, affiliation from pubsub_state " + "where nodeid='">>, Nidx, <<"';">>]) + of + {selected, [<<"jid">>, <<"affiliation">>], RItems} -> + [{decode_jid(J), decode_affiliation(A)} || [J, A] <- RItems]; + _ -> + [] + end, + {result, Reply}. get_affiliation(Nidx, Owner) -> - node_hometree_odbc:get_affiliation(Nidx, Owner). + SubKey = jid:tolower(Owner), + GenKey = jid:remove_resource(SubKey), + J = encode_jid(GenKey), + Reply = case catch + ejabberd_odbc:sql_query_t([<<"select affiliation from pubsub_state " + "where nodeid='">>, Nidx, <<"' and jid='">>, J, <<"';">>]) + of + {selected, [<<"affiliation">>], [[A]]} -> + decode_affiliation(A); + _ -> + none + end, + {result, Reply}. set_affiliation(Nidx, Owner, Affiliation) -> - node_hometree_odbc:set_affiliation(Nidx, Owner, Affiliation). + SubKey = jid:tolower(Owner), + GenKey = jid:remove_resource(SubKey), + {_, Subscriptions} = select_affiliation_subscriptions(Nidx, GenKey), + case {Affiliation, Subscriptions} of + {none, []} -> del_state(Nidx, GenKey); + _ -> update_affiliation(Nidx, GenKey, Affiliation) + end. get_entity_subscriptions(Host, Owner) -> - node_hometree_odbc:get_entity_subscriptions(Host, Owner). + SubKey = jid:tolower(Owner), + GenKey = jid:remove_resource(SubKey), + H = encode_host(Host), + SJ = encode_jid(SubKey), + GJ = encode_jid(GenKey), + Query = case SubKey of + GenKey -> + [<<"select node, type, i.nodeid, jid, subscriptions " + "from pubsub_state i, pubsub_node n " + "where i.nodeid = n.nodeid and jid like '">>, GJ, <<"%' and host='">>, H, <<"';">>]; + _ -> + [<<"select node, type, i.nodeid, jid, subscriptions " + "from pubsub_state i, pubsub_node n " + "where i.nodeid = n.nodeid and jid in ('">>, SJ, <<"', '">>, GJ, <<"') and host='">>, H, <<"';">>] + end, + Reply = case catch ejabberd_odbc:sql_query_t(Query) of + {selected, + [<<"node">>, <<"type">>, <<"nodeid">>, <<"jid">>, <<"subscriptions">>], RItems} -> + lists:foldl(fun ([N, T, I, J, S], Acc) -> + Node = nodetree_tree_odbc:raw_to_node(Host, [N, <<"">>, T, I]), + Jid = decode_jid(J), + case decode_subscriptions(S) of + [] -> + [{Node, none, Jid} | Acc]; + Subs -> + lists:foldl(fun ({Sub, SubId}, Acc2) -> + [{Node, Sub, SubId, Jid} | Acc2] + end, + Acc, Subs) + end + end, + [], RItems); + _ -> + [] + end, + {result, Reply}. +-spec(get_entity_subscriptions_for_send_last/2 :: + ( + Host :: mod_pubsub:hostPubsub(), + Owner :: jid()) + -> {result, + [{mod_pubsub:pubsubNode(), + mod_pubsub:subscription(), + mod_pubsub:subId(), + ljid()}] + } + ). get_entity_subscriptions_for_send_last(Host, Owner) -> - node_hometree_odbc:get_entity_subscriptions_for_send_last(Host, Owner). + SubKey = jid:tolower(Owner), + GenKey = jid:remove_resource(SubKey), + H = encode_host(Host), + SJ = encode_jid(SubKey), + GJ = encode_jid(GenKey), + Query = case SubKey of + GenKey -> + [<<"select node, type, i.nodeid, jid, subscriptions " + "from pubsub_state i, pubsub_node n, pubsub_node_option o " + "where i.nodeid = n.nodeid and n.nodeid = o.nodeid and name='send_last_published_item' " + "and val='on_sub_and_presence' and jid like '">>, GJ, <<"%' and host='">>, H, <<"';">>]; + _ -> + [<<"select node, type, i.nodeid, jid, subscriptions " + "from pubsub_state i, pubsub_node n, pubsub_node_option o " + "where i.nodeid = n.nodeid and n.nodeid = o.nodeid and name='send_last_published_item' " + "and val='on_sub_and_presence' and jid in ('">>, SJ, <<"', '">>, GJ, <<"') and host='">>, H, <<"';">>] + end, + Reply = case catch ejabberd_odbc:sql_query_t(Query) of + {selected, + [<<"node">>, <<"type">>, <<"nodeid">>, <<"jid">>, <<"subscriptions">>], RItems} -> + lists:foldl(fun ([N, T, I, J, S], Acc) -> + Node = nodetree_tree_odbc:raw_to_node(Host, [N, <<"">>, T, I]), + Jid = decode_jid(J), + case decode_subscriptions(S) of + [] -> + [{Node, none, Jid} | Acc]; + Subs -> + lists:foldl(fun ({Sub, SubId}, Acc2) -> + [{Node, Sub, SubId, Jid}| Acc2] + end, + Acc, Subs) + end + end, + [], RItems); + _ -> + [] + end, + {result, Reply}. get_node_subscriptions(Nidx) -> - node_hometree_odbc:get_node_subscriptions(Nidx). + Reply = case catch + ejabberd_odbc:sql_query_t([<<"select jid, subscriptions from pubsub_state " + "where nodeid='">>, Nidx, <<"';">>]) + of + {selected, [<<"jid">>, <<"subscriptions">>], RItems} -> + lists:foldl(fun ([J, S], Acc) -> + Jid = decode_jid(J), + case decode_subscriptions(S) of + [] -> + [{Jid, none} | Acc]; + Subs -> + lists:foldl(fun ({Sub, SubId}, Acc2) -> + [{Jid, Sub, SubId} | Acc2] + end, + Acc, Subs) + end + end, + [], RItems); + _ -> + [] + end, + {result, Reply}. get_subscriptions(Nidx, Owner) -> - node_hometree_odbc:get_subscriptions(Nidx, Owner). + SubKey = jid:tolower(Owner), + J = encode_jid(SubKey), + Reply = case catch + ejabberd_odbc:sql_query_t([<<"select subscriptions from pubsub_state where " + "nodeid='">>, Nidx, <<"' and jid='">>, J, <<"';">>]) + of + {selected, [<<"subscriptions">>], [[S]]} -> + decode_subscriptions(S); + _ -> + [] + end, + {result, Reply}. set_subscriptions(Nidx, Owner, Subscription, SubId) -> - node_hometree_odbc:set_subscriptions(Nidx, Owner, Subscription, SubId). + SubKey = jid:tolower(Owner), + SubState = get_state_without_itemids(Nidx, SubKey), + case {SubId, SubState#pubsub_state.subscriptions} of + {_, []} -> + case Subscription of + none -> + {error, + ?ERR_EXTENDED((?ERR_BAD_REQUEST), <<"not-subscribed">>)}; + _ -> + new_subscription(Nidx, Owner, Subscription, SubState) + end; + {<<>>, [{_, SID}]} -> + case Subscription of + none -> unsub_with_subid(Nidx, SID, SubState); + _ -> replace_subscription({Subscription, SID}, SubState) + end; + {<<>>, [_ | _]} -> + {error, + ?ERR_EXTENDED((?ERR_BAD_REQUEST), <<"subid-required">>)}; + _ -> + case Subscription of + none -> unsub_with_subid(Nidx, SubId, SubState); + _ -> replace_subscription({Subscription, SubId}, SubState) + end + end. + +replace_subscription(NewSub, SubState) -> + NewSubs = replace_subscription(NewSub, SubState#pubsub_state.subscriptions, []), + set_state(SubState#pubsub_state{subscriptions = NewSubs}). + +replace_subscription(_, [], Acc) -> Acc; +replace_subscription({Sub, SubId}, [{_, SubId} | T], Acc) -> + replace_subscription({Sub, SubId}, T, [{Sub, SubId} | Acc]). + +new_subscription(_Nidx, _Owner, Subscription, SubState) -> + %%{result, SubId} = pubsub_subscription_odbc:subscribe_node(Owner, Nidx, []), + SubId = pubsub_subscription_odbc:make_subid(), + Subscriptions = [{Subscription, SubId} | SubState#pubsub_state.subscriptions], + set_state(SubState#pubsub_state{subscriptions = Subscriptions}), + {Subscription, SubId}. + +unsub_with_subid(Nidx, SubId, SubState) -> + %%pubsub_subscription_odbc:unsubscribe_node(SubState#pubsub_state.stateid, Nidx, SubId), + NewSubs = [{S, Sid} + || {S, Sid} <- SubState#pubsub_state.subscriptions, + SubId =/= Sid], + case {NewSubs, SubState#pubsub_state.affiliation} of + {[], none} -> del_state(Nidx, element(1, SubState#pubsub_state.stateid)); + _ -> set_state(SubState#pubsub_state{subscriptions = NewSubs}) + end. get_pending_nodes(Host, Owner) -> - node_hometree_odbc:get_pending_nodes(Host, Owner). + GenKey = jid:remove_resource(jid:tolower(Owner)), + States = mnesia:match_object(#pubsub_state{stateid = {GenKey, '_'}, + affiliation = owner, _ = '_'}), + Nidxxs = [Nidx || #pubsub_state{stateid = {_, Nidx}} <- States], + NodeTree = mod_pubsub:tree(Host), + Reply = mnesia:foldl(fun (#pubsub_state{stateid = {_, Nidx}} = S, Acc) -> + case lists:member(Nidx, Nidxxs) of + true -> + case get_nodes_helper(NodeTree, S) of + {value, Node} -> [Node | Acc]; + false -> Acc + end; + false -> + Acc + end + end, + [], pubsub_state), + {result, Reply}. + +get_nodes_helper(NodeTree, #pubsub_state{stateid = {_, N}, subscriptions = Subs}) -> + HasPending = fun + ({pending, _}) -> true; + (pending) -> true; + (_) -> false + end, + case lists:any(HasPending, Subs) of + true -> + case NodeTree:get_node(N) of + #pubsub_node{nodeid = {_, Node}} -> {value, Node}; + _ -> false + end; + false -> + false + end. get_states(Nidx) -> - node_hometree_odbc:get_states(Nidx). + case catch + ejabberd_odbc:sql_query_t([<<"select jid, affiliation, subscriptions " + "from pubsub_state where nodeid='">>, Nidx, <<"';">>]) + of + {selected, + [<<"jid">>, <<"affiliation">>, <<"subscriptions">>], RItems} -> + {result, + lists:map(fun ([SJID, Aff, Subs]) -> + #pubsub_state{stateid = {decode_jid(SJID), Nidx}, + items = itemids(Nidx, SJID), + affiliation = decode_affiliation(Aff), + subscriptions = decode_subscriptions(Subs)} + end, + RItems)}; + _ -> + {result, []} + end. get_state(Nidx, JID) -> - node_hometree_odbc:get_state(Nidx, JID). + State = get_state_without_itemids(Nidx, JID), + {SJID, _} = State#pubsub_state.stateid, + State#pubsub_state{items = itemids(Nidx, SJID)}. + +-spec(get_state_without_itemids/2 :: + (Nidx :: mod_pubsub:nodeIdx(), + Key :: ljid()) -> + mod_pubsub:pubsubState() + ). +get_state_without_itemids(Nidx, JID) -> + J = encode_jid(JID), + case catch + ejabberd_odbc:sql_query_t([<<"select jid, affiliation, subscriptions " + "from pubsub_state where jid='">>, J, <<"' and nodeid='">>, Nidx, <<"';">>]) + of + {selected, + [<<"jid">>, <<"affiliation">>, <<"subscriptions">>], [[SJID, Aff, Subs]]} -> + #pubsub_state{stateid = {decode_jid(SJID), Nidx}, + affiliation = decode_affiliation(Aff), + subscriptions = decode_subscriptions(Subs)}; + _ -> + #pubsub_state{stateid = {JID, Nidx}} + end. set_state(State) -> - node_hometree_odbc:set_state(State). + {_, Nidx} = State#pubsub_state.stateid, + set_state(Nidx, State). + +set_state(Nidx, State) -> + {JID, _} = State#pubsub_state.stateid, + J = encode_jid(JID), + S = encode_subscriptions(State#pubsub_state.subscriptions), + A = encode_affiliation(State#pubsub_state.affiliation), + case catch + ejabberd_odbc:sql_query_t([<<"update pubsub_state set subscriptions='">>, S, <<"', affiliation='">>, A, + <<"' where nodeid='">>, Nidx, <<"' and jid='">>, J, <<"';">>]) + of + {updated, 1} -> + ok; + _ -> + catch + ejabberd_odbc:sql_query_t([<<"insert into pubsub_state(nodeid, jid, affiliation, subscriptions) " + "values('">>, + Nidx, <<"', '">>, J, <<"', '">>, A, <<"', '">>, S, <<"');">>]) + end, + ok. -get_items(Nidx, From, RSM) -> - node_hometree_odbc:get_items(Nidx, From, RSM). +del_state(Nidx, JID) -> + J = encode_jid(JID), + catch ejabberd_odbc:sql_query_t([<<"delete from pubsub_state where jid='">>, + J, <<"' and nodeid='">>, Nidx, <<"';">>]), + ok. -get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM) -> - node_hometree_odbc:get_items(Nidx, JID, AccessModel, - PresenceSubscription, RosterGroup, SubId, RSM). +%get_items(Nidx, _From) -> +% case catch +% ejabberd_odbc:sql_query_t([<<"select itemid, publisher, creation, modification, payload " +% "from pubsub_item where nodeid='">>, Nidx, +% <<"' order by modification desc;">>]) +% of +% {selected, +% [<<"itemid">>, <<"publisher">>, <<"creation">>, <<"modification">>, <<"payload">>], RItems} -> +% {result, [raw_to_item(Nidx, RItem) || RItem <- RItems]}; +% _ -> +% {result, []} +% end. + +get_items(Nidx, From, none) -> + MaxItems = case catch + ejabberd_odbc:sql_query_t([<<"select val from pubsub_node_option " + "where nodeid='">>, Nidx, <<"' and name='max_items';">>]) + of + {selected, [<<"val">>], [[Value]]} -> + Tokens = element(2, erl_scan:string(binary_to_list(<<Value/binary, ".">>))), + element(2, erl_parse:parse_term(Tokens)); + _ -> + ?MAXITEMS + end, + get_items(Nidx, From, #rsm_in{max = MaxItems}); +get_items(Nidx, _From, + #rsm_in{max = M, direction = Direction, id = I, index = IncIndex}) -> + Max = ejabberd_odbc:escape(jlib:i2l(M)), + {Way, Order} = case Direction of + % aft -> {<<"<">>, <<"desc">>}; + % before when I == <<>> -> {<<"is not">>, <<"asc">>}; + % before -> {<<">">>, <<"asc">>}; + % _ when IncIndex =/= undefined -> + % {<<"<">>, <<"desc">>}; % using index + _ -> + {<<"is not">>, <<"desc">>}% Can be better + end, + [AttrName, Id] = case I of + undefined when IncIndex =/= undefined -> + case catch + ejabberd_odbc:sql_query_t([<<"select modification from pubsub_item pi " + "where exists ( select count(*) as count1 " + "from pubsub_item where nodeid='">>, Nidx, + <<"' and modification > pi.modification having count1 = ">>, + ejabberd_odbc:escape(jlib:i2l(IncIndex)), <<" );">>]) + of + {selected, [_], [[O]]} -> + [<<"modification">>, <<"'", O/binary, "'">>]; + _ -> + [<<"modification">>, <<"null">>] + end; + undefined -> + [<<"modification">>, <<"null">>]; + <<>> -> + [<<"modification">>, <<"null">>]; + I -> + [A, B] = str:tokens(ejabberd_odbc:escape(jlib:i2l(I)), <<"@">>), + [A, <<"'", B/binary, "'">>] + end, + Count = case catch + ejabberd_odbc:sql_query_t([<<"select count(*) from pubsub_item where nodeid='">>, Nidx, <<"';">>]) + of + {selected, [_], [[C]]} -> C; + _ -> <<"0">> + end, + case catch + ejabberd_odbc:sql_query_t([<<"select itemid, publisher, creation, modification, payload " + "from pubsub_item where nodeid='">>, Nidx, + <<"' and ">>, AttrName, <<" ">>, Way, <<" ">>, Id, <<" order by ">>, + AttrName, <<" ">>, Order, <<" limit ">>, jlib:i2l(Max), <<" ;">>]) + of + {selected, + [<<"itemid">>, <<"publisher">>, <<"creation">>, <<"modification">>, <<"payload">>], RItems} -> + case RItems of + [[_, _, _, F, _]|_] -> + Index = case catch + ejabberd_odbc:sql_query_t([<<"select count(*) from pubsub_item " + "where nodeid='">>, Nidx, <<"' and ">>, + AttrName, <<" > '">>, F, <<"';">>]) + of + %{selected, [_], [{C}, {In}]} -> [string:strip(C, both, $"), string:strip(In, both, $")]; + {selected, [_], [[In]]} -> In; + _ -> <<"0">> + end, + [_, _, _, L, _] = lists:last(RItems), + RsmOut = #rsm_out{count = Count, index = Index, + first = <<"modification@", F/binary>>, + last = <<"modification@", (jlib:i2l(L))/binary>>}, + {result, {[raw_to_item(Nidx, RItem) || RItem <- RItems], RsmOut}}; + [] -> + {result, {[], #rsm_out{count = Count}}} + end; + _ -> + {result, {[], none}} + end. + +get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId, RSM) -> + SubKey = jid:tolower(JID), + GenKey = jid:remove_resource(SubKey), + {Affiliation, Subscriptions} = select_affiliation_subscriptions(Nidx, GenKey, SubKey), + Whitelisted = node_flat:can_fetch_item(Affiliation, Subscriptions), + if %%SubId == "", ?? -> + %% Entity has multiple subscriptions to the node but does not specify a subscription ID + %{error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; + %%InvalidSubId -> + %% Entity is subscribed but specifies an invalid subscription ID + %{error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; + (Affiliation == outcast) or (Affiliation == publish_only) -> + {error, ?ERR_FORBIDDEN}; + (AccessModel == presence) and not PresenceSubscription -> + {error, + ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), <<"presence-subscription-required">>)}; + (AccessModel == roster) and not RosterGroup -> + {error, + ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), <<"not-in-roster-group">>)}; + (AccessModel == whitelist) and not Whitelisted -> + {error, + ?ERR_EXTENDED((?ERR_NOT_ALLOWED), <<"closed-node">>)}; + (AccessModel == authorize) and not Whitelisted -> + {error, ?ERR_FORBIDDEN}; + %%MustPay -> + %% % Payment is required for a subscription + %% {error, ?ERR_PAYMENT_REQUIRED}; + true -> + get_items(Nidx, JID, RSM) + end. + +get_last_items(Nidx, _From, Count) -> + case catch + ejabberd_odbc:sql_query_t([<<"select itemid, publisher, creation, modification, payload " + "from pubsub_item where nodeid='">>, Nidx, + <<"' order by modification desc limit ">>, jlib:i2l(Count), <<";">>]) + of + {selected, + [<<"itemid">>, <<"publisher">>, <<"creation">>, <<"modification">>, <<"payload">>], RItems} -> + {result, [raw_to_item(Nidx, RItem) || RItem <- RItems]}; + _ -> + {result, []} + end. get_item(Nidx, ItemId) -> - node_hometree_odbc:get_item(Nidx, ItemId). + I = ejabberd_odbc:escape(ItemId), + case catch + ejabberd_odbc:sql_query_t([<<"select itemid, publisher, creation, " + "modification, payload from pubsub_item " + "where nodeid='">>, Nidx, <<"' and itemid='">>, I, <<"';">>]) + of + {selected, + [<<"itemid">>, <<"publisher">>, <<"creation">>, <<"modification">>, <<"payload">>], [RItem]} -> + {result, raw_to_item(Nidx, RItem)}; + _ -> + {error, ?ERR_ITEM_NOT_FOUND} + end. -get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> - node_hometree_odbc:get_item(Nidx, ItemId, JID, - AccessModel, PresenceSubscription, RosterGroup, SubId). +get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId) -> + SubKey = jid:tolower(JID), + GenKey = jid:remove_resource(SubKey), + {Affiliation, Subscriptions} = select_affiliation_subscriptions(Nidx, GenKey, SubKey), + Whitelisted = node_flat:can_fetch_item(Affiliation, Subscriptions), + if %%SubId == "", ?? -> + %% Entity has multiple subscriptions to the node but does not specify a subscription ID + %{error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; + %%InvalidSubId -> + %% Entity is subscribed but specifies an invalid subscription ID + %{error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; + (Affiliation == outcast) or (Affiliation == publish_only) -> + {error, ?ERR_FORBIDDEN}; + (AccessModel == presence) and not PresenceSubscription -> + {error, + ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), <<"presence-subscription-required">>)}; + (AccessModel == roster) and not RosterGroup -> + {error, + ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), <<"not-in-roster-group">>)}; + (AccessModel == whitelist) and not Whitelisted -> + {error, + ?ERR_EXTENDED((?ERR_NOT_ALLOWED), <<"closed-node">>)}; + (AccessModel == authorize) and not Whitelisted -> + {error, ?ERR_FORBIDDEN}; + %%MustPay -> + %% % Payment is required for a subscription + %% {error, ?ERR_PAYMENT_REQUIRED}; + true -> + get_item(Nidx, ItemId) + end. set_item(Item) -> - node_hometree_odbc:set_item(Item). + {ItemId, Nidx} = Item#pubsub_item.itemid, + I = ejabberd_odbc:escape(ItemId), + {C, _} = Item#pubsub_item.creation, + {M, JID} = Item#pubsub_item.modification, + P = encode_jid(JID), + Payload = Item#pubsub_item.payload, + XML = ejabberd_odbc:escape(str:join([fxml:element_to_binary(X) || X<-Payload], <<>>)), + S = fun ({T1, T2, T3}) -> + str:join([jlib:i2l(T1, 6), jlib:i2l(T2, 6), jlib:i2l(T3, 6)], <<":">>) + end, + case catch + ejabberd_odbc:sql_query_t([<<"update pubsub_item set publisher='">>, P, + <<"', modification='">>, S(M), + <<"', payload='">>, XML, + <<"' where nodeid='">>, Nidx, <<"' and itemid='">>, I, <<"';">>]) + of + {updated, 1} -> + ok; + _ -> + catch + ejabberd_odbc:sql_query_t([<<"insert into pubsub_item (nodeid, itemid, " + "publisher, creation, modification, payload) " + "values('">>, Nidx, <<"', '">>, I, <<"', '">>, P, + <<"', '">>, S(C), <<"', '">>, S(M), + <<"', '">>, XML, <<"');">>]) + end, + ok. + +del_item(Nidx, ItemId) -> + I = ejabberd_odbc:escape(ItemId), + catch ejabberd_odbc:sql_query_t([<<"delete from pubsub_item where itemid='">>, + I, <<"' and nodeid='">>, Nidx, <<"';">>]). -get_item_name(Host, Node, Id) -> - node_hometree_odbc:get_item_name(Host, Node, Id). +del_items(_, []) -> + ok; +del_items(Nidx, [ItemId]) -> + del_item(Nidx, ItemId); +del_items(Nidx, ItemIds) -> + I = str:join([[<<"'">>, ejabberd_odbc:escape(X), <<"'">>] || X <- ItemIds], <<",">>), + catch + ejabberd_odbc:sql_query_t([<<"delete from pubsub_item where itemid in (">>, + I, <<") and nodeid='">>, Nidx, <<"';">>]). -get_last_items(Nidx, From, Count) -> - node_hometree_odbc:get_last_items(Nidx, From, Count). +get_item_name(_Host, _Node, Id) -> + Id. node_to_path(Node) -> - [(Node)]. + node_flat:node_to_path(Node). path_to_node(Path) -> - case Path of - % default slot - [Node] -> iolist_to_binary(Node); - % handle old possible entries, used when migrating database content to new format - [Node | _] when is_binary(Node) -> - iolist_to_binary(str:join([<<"">> | Path], <<"/">>)); - % default case (used by PEP for example) - _ -> iolist_to_binary(Path) + node_flat:path_to_node(Path). + + +first_in_list(_Pred, []) -> + false; +first_in_list(Pred, [H | T]) -> + case Pred(H) of + true -> {value, H}; + _ -> first_in_list(Pred, T) + end. + +itemids(Nidx, {U, S, R}) -> + itemids(Nidx, encode_jid({U, S, R})); +itemids(Nidx, SJID) -> + case catch + ejabberd_odbc:sql_query_t([<<"select itemid from pubsub_item where " + "nodeid='">>, Nidx, <<"' and publisher like '">>, SJID, + <<"%' order by modification desc;">>]) + of + {selected, [<<"itemid">>], RItems} -> + [ItemId || [ItemId] <- RItems]; + _ -> + [] + end. + +select_affiliation_subscriptions(Nidx, JID) -> + J = encode_jid(JID), + case catch + ejabberd_odbc:sql_query_t([<<"select affiliation,subscriptions from " + "pubsub_state where nodeid='">>, + Nidx, <<"' and jid='">>, J, <<"';">>]) + of + {selected, [<<"affiliation">>, <<"subscriptions">>], [[A, S]]} -> + {decode_affiliation(A), decode_subscriptions(S)}; + _ -> + {none, []} end. + +select_affiliation_subscriptions(Nidx, JID, JID) -> + select_affiliation_subscriptions(Nidx, JID); +select_affiliation_subscriptions(Nidx, GenKey, SubKey) -> + {result, Affiliation} = get_affiliation(Nidx, GenKey), + {result, Subscriptions} = get_subscriptions(Nidx, SubKey), + {Affiliation, Subscriptions}. + +update_affiliation(Nidx, JID, Affiliation) -> + J = encode_jid(JID), + A = encode_affiliation(Affiliation), + case catch + ejabberd_odbc:sql_query_t([<<"update pubsub_state set affiliation='">>, + A, <<"' where nodeid='">>, Nidx, + <<"' and jid='">>, J, <<"';">>]) + of + {updated, 1} -> + ok; + _ -> + catch + ejabberd_odbc:sql_query_t([<<"insert into pubsub_state(nodeid, jid, affiliation, subscriptions) " + "values('">>, Nidx, <<"', '">>, J, <<"', '">>, A, <<"', '');">>]) + end. + +update_subscription(Nidx, JID, Subscription) -> + J = encode_jid(JID), + S = encode_subscriptions(Subscription), + case catch + ejabberd_odbc:sql_query_t([<<"update pubsub_state set subscriptions='">>, S, + <<"' where nodeid='">>, Nidx, <<"' and jid='">>, J, <<"';">>]) + of + {updated, 1} -> + ok; + _ -> + catch + ejabberd_odbc:sql_query_t([<<"insert into pubsub_state(nodeid, jid, affiliation, subscriptions) " + "values('">>, Nidx, <<"', '">>, J, <<"', 'n', '">>, S, <<"');">>]) + end. + +-spec(decode_jid/1 :: + ( SJID :: binary()) + -> ljid() + ). +decode_jid(SJID) -> + jid:tolower(jid:from_string(SJID)). + +-spec(decode_affiliation/1 :: + ( Arg :: binary()) + -> atom() + ). +decode_affiliation(<<"o">>) -> owner; +decode_affiliation(<<"p">>) -> publisher; +decode_affiliation(<<"u">>) -> publish_only; +decode_affiliation(<<"m">>) -> member; +decode_affiliation(<<"c">>) -> outcast; +decode_affiliation(_) -> none. + +-spec(decode_subscription/1 :: + ( Arg :: binary()) + -> atom() + ). +decode_subscription(<<"s">>) -> subscribed; +decode_subscription(<<"p">>) -> pending; +decode_subscription(<<"u">>) -> unconfigured; +decode_subscription(_) -> none. + +-spec(decode_subscriptions/1 :: + ( Subscriptions :: binary()) + -> [] | [{atom(), binary()},...] + ). +decode_subscriptions(Subscriptions) -> + lists:foldl(fun (Subscription, Acc) -> + case str:tokens(Subscription, <<":">>) of + [S, SubId] -> [{decode_subscription(S), SubId} | Acc]; + _ -> Acc + end + end, + [], str:tokens(Subscriptions, <<",">>)). + +-spec(encode_jid/1 :: + ( JID :: ljid()) + -> binary() + ). +encode_jid(JID) -> + ejabberd_odbc:escape(jid:to_string(JID)). + +-spec(encode_host/1 :: + ( Host :: host()) + -> binary() + ). +encode_host({_U, _S, _R} = LJID) -> encode_jid(LJID); +encode_host(Host) -> ejabberd_odbc:escape(Host). + +-spec(encode_affiliation/1 :: + ( Arg :: atom()) + -> binary() + ). +encode_affiliation(owner) -> <<"o">>; +encode_affiliation(publisher) -> <<"p">>; +encode_affiliation(publish_only) -> <<"u">>; +encode_affiliation(member) -> <<"m">>; +encode_affiliation(outcast) -> <<"c">>; +encode_affiliation(_) -> <<"n">>. + +-spec(encode_subscription/1 :: + ( Arg :: atom()) + -> binary() + ). +encode_subscription(subscribed) -> <<"s">>; +encode_subscription(pending) -> <<"p">>; +encode_subscription(unconfigured) -> <<"u">>; +encode_subscription(_) -> <<"n">>. + +-spec(encode_subscriptions/1 :: + ( Subscriptions :: [] | [{atom(), binary()},...]) + -> binary() + ). +encode_subscriptions(Subscriptions) -> + str:join([<<(encode_subscription(S))/binary, ":", SubId/binary>> + || {S, SubId} <- Subscriptions], <<",">>). + +%%% record getter/setter + +state_to_raw(Nidx, State) -> + {JID, _} = State#pubsub_state.stateid, + J = encode_jid(JID), + A = encode_affiliation(State#pubsub_state.affiliation), + S = encode_subscriptions(State#pubsub_state.subscriptions), + [<<"'">>, Nidx, <<"', '">>, J, <<"', '">>, A, <<"', '">>, S, <<"'">>]. + +raw_to_item(Nidx, [ItemId, SJID, Creation, Modification, XML]) -> + JID = decode_jid(SJID), + ToTime = fun (Str) -> + [T1, T2, T3] = str:tokens(Str, <<":">>), + {jlib:l2i(T1), jlib:l2i(T2), jlib:l2i(T3)} + end, + Payload = case fxml_stream:parse_element(XML) of + {error, _Reason} -> []; + El -> [El] + end, + #pubsub_item{itemid = {ItemId, Nidx}, + creation = {ToTime(Creation), JID}, + modification = {ToTime(Modification), JID}, + payload = Payload}. diff --git a/src/node_hometree.erl b/src/node_hometree.erl index 296fbdde..2d26c0ee 100644 --- a/src/node_hometree.erl +++ b/src/node_hometree.erl @@ -1,43 +1,27 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. +%%%---------------------------------------------------------------------- +%%% File : node_hometree.erl +%%% Author : Christophe Romain <christophe.romain@process-one.net> +%%% Purpose : Standard tree ordered node plugin +%%% Created : 1 Dec 2007 by Christophe Romain <christophe.romain@process-one.net> %%% %%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. +%%% ejabberd, Copyright (C) 2002-2016 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. %%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. +%%% 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. %%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Christophe Romain <christophe.romain@process-one.net> -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== - -%%% @todo The item table should be handled by the plugin, but plugin that do -%%% not want to manage it should be able to use the default behaviour. -%%% @todo Plugin modules should be able to register to receive presence update -%%% send to pubsub. - -%%% @doc The module <strong>{@module}</strong> is the default PubSub plugin. -%%% <p>It is used as a default for all unknown PubSub node type. It can serve -%%% as a developer basis and reference to build its own custom pubsub node -%%% types.</p> -%%% <p>PubSub plugin nodes are using the {@link gen_node} behaviour.</p> -%%% <p><strong>The API isn't stabilized yet</strong>. The pubsub plugin -%%% development is still a work in progress. However, the system is already -%%% useable and useful as is. Please, send us comments, feedback and -%%% improvements.</p> +%%% 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(node_hometree). -behaviour(gen_pubsub_node). @@ -55,84 +39,35 @@ get_entity_subscriptions/2, get_node_subscriptions/1, get_subscriptions/2, set_subscriptions/4, get_pending_nodes/2, get_states/1, get_state/2, - set_state/1, get_items/6, get_items/2, - get_items/7, get_items/3, get_item/7, + set_state/1, get_items/7, get_items/3, get_item/7, get_item/2, set_item/1, get_item_name/3, node_to_path/1, path_to_node/1]). -init(Host, ServerHost, _Opts) -> - pubsub_subscription:init(), - mnesia:create_table(pubsub_state, - [{disc_copies, [node()]}, - {type, ordered_set}, - {attributes, record_info(fields, pubsub_state)}]), - mnesia:create_table(pubsub_item, - [{disc_only_copies, [node()]}, - {attributes, record_info(fields, pubsub_item)}]), - ItemsFields = record_info(fields, pubsub_item), - case mnesia:table_info(pubsub_item, attributes) of - ItemsFields -> ok; - _ -> mnesia:transform_table(pubsub_item, ignore, ItemsFields) - end, +init(Host, ServerHost, Opts) -> + node_flat:init(Host, ServerHost, Opts), Owner = mod_pubsub:service_jid(Host), mod_pubsub:create_node(Host, ServerHost, <<"/home">>, Owner, <<"hometree">>), mod_pubsub:create_node(Host, ServerHost, <<"/home/", ServerHost/binary>>, Owner, <<"hometree">>), ok. -terminate(_Host, _ServerHost) -> - ok. +terminate(Host, ServerHost) -> + node_flat:terminate(Host, ServerHost). options() -> - [{deliver_payloads, true}, - {notify_config, false}, - {notify_delete, false}, - {notify_retract, true}, - {purge_offline, false}, - {persist_items, true}, - {max_items, ?MAXITEMS}, - {subscribe, true}, - {access_model, open}, - {roster_groups_allowed, []}, - {publish_model, publishers}, - {notification_type, headline}, - {max_payload_size, ?MAX_PAYLOAD_SIZE}, - {send_last_published_item, on_sub_and_presence}, - {deliver_notifications, true}, - {presence_based_delivery, false}]. + node_flat:options(). features() -> - [<<"create-nodes">>, - <<"auto-create">>, - <<"access-authorize">>, - <<"delete-nodes">>, - <<"delete-items">>, - <<"get-pending">>, - <<"instant-nodes">>, - <<"manage-subscriptions">>, - <<"modify-affiliations">>, - <<"multi-subscribe">>, - <<"outcast-affiliation">>, - <<"persistent-items">>, - <<"publish">>, - <<"publish-only-affiliation">>, - <<"purge-nodes">>, - <<"retract-items">>, - <<"retrieve-affiliations">>, - <<"retrieve-items">>, - <<"retrieve-subscriptions">>, - <<"subscribe">>, - <<"subscription-notifications">>, - <<"subscription-options">>]. + node_flat:features(). %% @doc Checks if the current user has the permission to create the requested node -%% <p>In {@link node_default}, the permission is decided by the place in the +%% <p>In hometree node, the permission is decided by the place in the %% hierarchy where the user is creating the node. The access parameter is also -%% checked in the default module. This parameter depends on the value of the +%% checked. This parameter depends on the value of the %% <tt>access_createnode</tt> ACL value in ejabberd config file.</p> -%% <p>This function also check that node can be created a a children of its +%% <p>This function also check that node can be created as a children of its %% parent node</p> create_node_permission(Host, ServerHost, Node, _ParentNode, Owner, Access) -> - LOwner = jlib:jid_tolower(Owner), + LOwner = jid:tolower(Owner), {User, Server, _Resource} = LOwner, Allowed = case LOwner of {<<"">>, Host, <<"">>} -> @@ -150,707 +85,92 @@ create_node_permission(Host, ServerHost, Node, _ParentNode, Owner, Access) -> {result, Allowed}. create_node(Nidx, Owner) -> - OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), - set_state(#pubsub_state{stateid = {OwnerKey, Nidx}, - affiliation = owner}), - {result, {default, broadcast}}. + node_flat:create_node(Nidx, Owner). delete_node(Nodes) -> - Tr = fun (#pubsub_state{stateid = {J, _}, subscriptions = Ss}) -> - lists:map(fun (S) -> {J, S} end, Ss) - end, - Reply = lists:map(fun (#pubsub_node{id = Nidx} = PubsubNode) -> - {result, States} = get_states(Nidx), - lists:foreach(fun (#pubsub_state{stateid = {LJID, _}, items = Items}) -> - del_items(Nidx, Items), - del_state(Nidx, LJID) - end, States), - {PubsubNode, lists:flatmap(Tr, States)} - end, Nodes), - {result, {default, broadcast, Reply}}. - -%% @doc <p>Accepts or rejects subcription requests on a PubSub node.</p> -%% <p>The mechanism works as follow: -%% <ul> -%% <li>The main PubSub module prepares the subscription and passes the -%% result of the preparation as a record.</li> -%% <li>This function gets the prepared record and several other parameters and -%% can decide to:<ul> -%% <li>reject the subscription;</li> -%% <li>allow it as is, letting the main module perform the database -%% persistance;</li> -%% <li>allow it, modifying the record. The main module will store the -%% modified record;</li> -%% <li>allow it, but perform the needed persistance operations.</li></ul> -%% </li></ul></p> -%% <p>The selected behaviour depends on the return parameter: -%% <ul> -%% <li><tt>{error, Reason}</tt>: an IQ error result will be returned. No -%% subscription will actually be performed.</li> -%% <li><tt>true</tt>: Subscribe operation is allowed, based on the -%% unmodified record passed in parameter <tt>SubscribeResult</tt>. If this -%% parameter contains an error, no subscription will be performed.</li> -%% <li><tt>{true, PubsubState}</tt>: Subscribe operation is allowed, but -%% the {@link mod_pubsub:pubsubState()} record returned replaces the value -%% passed in parameter <tt>SubscribeResult</tt>.</li> -%% <li><tt>{true, done}</tt>: Subscribe operation is allowed, but the -%% {@link mod_pubsub:pubsubState()} will be considered as already stored and -%% no further persistance operation will be performed. This case is used, -%% when the plugin module is doing the persistance by itself or when it want -%% to completly disable persistance.</li></ul> -%% </p> -%% <p>In the default plugin module, the record is unchanged.</p> + node_flat:delete_node(Nodes). + subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options) -> - SubKey = jlib:jid_tolower(Subscriber), - GenKey = jlib:jid_remove_resource(SubKey), - Authorized = jlib:jid_tolower(jlib:jid_remove_resource(Sender)) == GenKey, - GenState = get_state(Nidx, GenKey), - SubState = case SubKey of - GenKey -> GenState; - _ -> get_state(Nidx, SubKey) - end, - Affiliation = GenState#pubsub_state.affiliation, - Subscriptions = SubState#pubsub_state.subscriptions, - Whitelisted = lists:member(Affiliation, [member, publisher, owner]), - PendingSubscription = lists:any(fun - ({pending, _}) -> true; - (_) -> false - end, - Subscriptions), - Owner = Affiliation == owner, - if not Authorized -> - {error, - ?ERR_EXTENDED((?ERR_BAD_REQUEST), <<"invalid-jid">>)}; - (Affiliation == outcast) or (Affiliation == publish_only) -> - {error, ?ERR_FORBIDDEN}; - PendingSubscription -> - {error, - ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), <<"pending-subscription">>)}; - (AccessModel == presence) and (not PresenceSubscription) and (not Owner) -> - {error, - ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), <<"presence-subscription-required">>)}; - (AccessModel == roster) and (not RosterGroup) and (not Owner) -> - {error, - ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), <<"not-in-roster-group">>)}; - (AccessModel == whitelist) and (not Whitelisted) and (not Owner) -> - {error, - ?ERR_EXTENDED((?ERR_NOT_ALLOWED), <<"closed-node">>)}; - %%MustPay -> - %% % Payment is required for a subscription - %% {error, ?ERR_PAYMENT_REQUIRED}; - %%ForbiddenAnonymous -> - %% % Requesting entity is anonymous - %% {error, ?ERR_FORBIDDEN}; - true -> - SubId = pubsub_subscription:add_subscription(Subscriber, Nidx, Options), - NewSub = case AccessModel of - authorize -> pending; - _ -> subscribed - end, - set_state(SubState#pubsub_state{subscriptions = - [{NewSub, SubId} | Subscriptions]}), - case {NewSub, SendLast} of - {subscribed, never} -> - {result, {default, subscribed, SubId}}; - {subscribed, _} -> - {result, {default, subscribed, SubId, send_last}}; - {_, _} -> - {result, {default, pending, SubId}} - end - end. + node_flat:subscribe_node(Nidx, Sender, Subscriber, + AccessModel, SendLast, PresenceSubscription, + RosterGroup, Options). -%% @doc <p>Unsubscribe the <tt>Subscriber</tt> from the <tt>Node</tt>.</p> unsubscribe_node(Nidx, Sender, Subscriber, SubId) -> - SubKey = jlib:jid_tolower(Subscriber), - GenKey = jlib:jid_remove_resource(SubKey), - Authorized = jlib:jid_tolower(jlib:jid_remove_resource(Sender)) == GenKey, - GenState = get_state(Nidx, GenKey), - SubState = case SubKey of - GenKey -> GenState; - _ -> get_state(Nidx, SubKey) - end, - Subscriptions = lists:filter(fun - ({_Sub, _SubId}) -> true; - (_SubId) -> false - end, - SubState#pubsub_state.subscriptions), - SubIdExists = case SubId of - <<>> -> false; - Binary when is_binary(Binary) -> true; - _ -> false - end, - if - %% Requesting entity is prohibited from unsubscribing entity - not Authorized -> - {error, ?ERR_FORBIDDEN}; - %% Entity did not specify SubId - %%SubId == "", ?? -> - %% {error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; - %% Invalid subscription identifier - %%InvalidSubId -> - %% {error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; - %% Requesting entity is not a subscriber - Subscriptions == [] -> - {error, - ?ERR_EXTENDED((?ERR_UNEXPECTED_REQUEST_CANCEL), <<"not-subscribed">>)}; - %% Subid supplied, so use that. - SubIdExists -> - Sub = first_in_list(fun - ({_, S}) when S == SubId -> true; - (_) -> false - end, - SubState#pubsub_state.subscriptions), - case Sub of - {value, S} -> - delete_subscriptions(SubKey, Nidx, [S], SubState), - {result, default}; - false -> - {error, - ?ERR_EXTENDED((?ERR_UNEXPECTED_REQUEST_CANCEL), <<"not-subscribed">>)} - end; - %% Asking to remove all subscriptions to the given node - SubId == all -> - delete_subscriptions(SubKey, Nidx, Subscriptions, SubState), - {result, default}; - %% No subid supplied, but there's only one matching subscription - length(Subscriptions) == 1 -> - delete_subscriptions(SubKey, Nidx, Subscriptions, SubState), - {result, default}; - %% No subid and more than one possible subscription match. - true -> - {error, - ?ERR_EXTENDED((?ERR_BAD_REQUEST), <<"subid-required">>)} - end. - -delete_subscriptions(SubKey, Nidx, Subscriptions, SubState) -> - NewSubs = lists:foldl(fun ({Subscription, SubId}, Acc) -> - pubsub_subscription:delete_subscription(SubKey, Nidx, SubId), - Acc -- [{Subscription, SubId}] - end, SubState#pubsub_state.subscriptions, Subscriptions), - case {SubState#pubsub_state.affiliation, NewSubs} of - {none, []} -> del_state(Nidx, SubKey); - _ -> set_state(SubState#pubsub_state{subscriptions = NewSubs}) - end. - -%% @doc <p>Publishes the item passed as parameter.</p> -%% <p>The mechanism works as follow: -%% <ul> -%% <li>The main PubSub module prepares the item to publish and passes the -%% result of the preparation as a {@link mod_pubsub:pubsubItem()} record.</li> -%% <li>This function gets the prepared record and several other parameters and can decide to:<ul> -%% <li>reject the publication;</li> -%% <li>allow the publication as is, letting the main module perform the database persistance;</li> -%% <li>allow the publication, modifying the record. The main module will store the modified record;</li> -%% <li>allow it, but perform the needed persistance operations.</li></ul> -%% </li></ul></p> -%% <p>The selected behaviour depends on the return parameter: -%% <ul> -%% <li><tt>{error, Reason}</tt>: an iq error result will be return. No -%% publication is actually performed.</li> -%% <li><tt>true</tt>: Publication operation is allowed, based on the -%% unmodified record passed in parameter <tt>Item</tt>. If the <tt>Item</tt> -%% parameter contains an error, no subscription will actually be -%% performed.</li> -%% <li><tt>{true, Item}</tt>: Publication operation is allowed, but the -%% {@link mod_pubsub:pubsubItem()} record returned replaces the value passed -%% in parameter <tt>Item</tt>. The persistance will be performed by the main -%% module.</li> -%% <li><tt>{true, done}</tt>: Publication operation is allowed, but the -%% {@link mod_pubsub:pubsubItem()} will be considered as already stored and -%% no further persistance operation will be performed. This case is used, -%% when the plugin module is doing the persistance by itself or when it want -%% to completly disable persistance.</li></ul> -%% </p> -%% <p>In the default plugin module, the record is unchanged.</p> -publish_item(Nidx, Publisher, PublishModel, MaxItems, ItemId, Payload) -> - SubKey = jlib:jid_tolower(Publisher), - GenKey = jlib:jid_remove_resource(SubKey), - GenState = get_state(Nidx, GenKey), - SubState = case SubKey of - GenKey -> GenState; - _ -> get_state(Nidx, SubKey) - end, - Affiliation = GenState#pubsub_state.affiliation, - Subscribed = case PublishModel of - subscribers -> is_subscribed(SubState#pubsub_state.subscriptions); - _ -> undefined - end, - if not ((PublishModel == open) or - (PublishModel == publishers) and - ((Affiliation == owner) - or (Affiliation == publisher) - or (Affiliation == publish_only)) - or (Subscribed == true)) -> - {error, ?ERR_FORBIDDEN}; - true -> - if MaxItems > 0 -> - Now = now(), - PubId = {Now, SubKey}, - Item = case get_item(Nidx, ItemId) of - {result, OldItem} -> - OldItem#pubsub_item{modification = PubId, - payload = Payload}; - _ -> - #pubsub_item{itemid = {ItemId, Nidx}, - creation = {Now, GenKey}, - modification = PubId, - payload = Payload} - end, - Items = [ItemId | GenState#pubsub_state.items -- [ItemId]], - {result, {NI, OI}} = remove_extra_items(Nidx, MaxItems, Items), - set_item(Item), - set_state(GenState#pubsub_state{items = NI}), - {result, {default, broadcast, OI}}; - true -> - {result, {default, broadcast, []}} - end - end. - -%% @doc <p>This function is used to remove extra items, most notably when the -%% maximum number of items has been reached.</p> -%% <p>This function is used internally by the core PubSub module, as no -%% permission check is performed.</p> -%% <p>In the default plugin module, the oldest items are removed, but other -%% rules can be used.</p> -%% <p>If another PubSub plugin wants to delegate the item removal (and if the -%% plugin is using the default pubsub storage), it can implements this function like this: -%% ```remove_extra_items(Nidx, MaxItems, ItemIds) -> -%% node_default:remove_extra_items(Nidx, MaxItems, ItemIds).'''</p> -remove_extra_items(_Nidx, unlimited, ItemIds) -> - {result, {ItemIds, []}}; + node_flat:unsubscribe_node(Nidx, Sender, Subscriber, SubId). + +publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload) -> + node_flat:publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload). + remove_extra_items(Nidx, MaxItems, ItemIds) -> - NewItems = lists:sublist(ItemIds, MaxItems), - OldItems = lists:nthtail(length(NewItems), ItemIds), - del_items(Nidx, OldItems), - {result, {NewItems, OldItems}}. - -%% @doc <p>Triggers item deletion.</p> -%% <p>Default plugin: The user performing the deletion must be the node owner -%% or a publisher, or PublishModel being open.</p> + node_flat:remove_extra_items(Nidx, MaxItems, ItemIds). + delete_item(Nidx, Publisher, PublishModel, ItemId) -> - SubKey = jlib:jid_tolower(Publisher), - GenKey = jlib:jid_remove_resource(SubKey), - GenState = get_state(Nidx, GenKey), - #pubsub_state{affiliation = Affiliation, items = Items} = GenState, - Allowed = Affiliation == publisher orelse - Affiliation == owner orelse - PublishModel == open orelse - case get_item(Nidx, ItemId) of - {result, #pubsub_item{creation = {_, GenKey}}} -> true; - _ -> false - end, - if not Allowed -> - {error, ?ERR_FORBIDDEN}; - true -> - case lists:member(ItemId, Items) of - true -> - del_item(Nidx, ItemId), - set_state(GenState#pubsub_state{items = lists:delete(ItemId, Items)}), - {result, {default, broadcast}}; - false -> - case Affiliation of - owner -> - {result, States} = get_states(Nidx), - lists:foldl(fun - (#pubsub_state{items = PI} = S, Res) -> - case lists:member(ItemId, PI) of - true -> - Nitems = lists:delete(ItemId, PI), - del_item(Nidx, ItemId), - set_state(S#pubsub_state{items = Nitems}), - {result, {default, broadcast}}; - false -> - Res - end; - (_, Res) -> - Res - end, - {error, ?ERR_ITEM_NOT_FOUND}, States); - _ -> - {error, ?ERR_ITEM_NOT_FOUND} - end - end - end. + node_flat:delete_item(Nidx, Publisher, PublishModel, ItemId). purge_node(Nidx, Owner) -> - SubKey = jlib:jid_tolower(Owner), - GenKey = jlib:jid_remove_resource(SubKey), - GenState = get_state(Nidx, GenKey), - case GenState of - #pubsub_state{affiliation = owner} -> - {result, States} = get_states(Nidx), - lists:foreach(fun - (#pubsub_state{items = []}) -> - ok; - (#pubsub_state{items = Items} = S) -> - del_items(Nidx, Items), - set_state(S#pubsub_state{items = []}) - end, - States), - {result, {default, broadcast}}; - _ -> - {error, ?ERR_FORBIDDEN} - end. - -%% @doc <p>Return the current affiliations for the given user</p> -%% <p>The default module reads affiliations in the main Mnesia -%% <tt>pubsub_state</tt> table. If a plugin stores its data in the same -%% table, it should return an empty list, as the affiliation will be read by -%% the default PubSub module. Otherwise, it should return its own affiliation, -%% that will be added to the affiliation stored in the main -%% <tt>pubsub_state</tt> table.</p> + node_flat:purge_node(Nidx, Owner). + get_entity_affiliations(Host, Owner) -> - SubKey = jlib:jid_tolower(Owner), - GenKey = jlib:jid_remove_resource(SubKey), - States = mnesia:match_object(#pubsub_state{stateid = {GenKey, '_'}, _ = '_'}), - NodeTree = mod_pubsub:tree(Host), - Reply = lists:foldl(fun (#pubsub_state{stateid = {_, N}, affiliation = A}, Acc) -> - case NodeTree:get_node(N) of - #pubsub_node{nodeid = {Host, _}} = Node -> [{Node, A} | Acc]; - _ -> Acc - end - end, - [], States), - {result, Reply}. + node_flat:get_entity_affiliations(Host, Owner). get_node_affiliations(Nidx) -> - {result, States} = get_states(Nidx), - Tr = fun (#pubsub_state{stateid = {J, _}, affiliation = A}) -> {J, A} end, - {result, lists:map(Tr, States)}. + node_flat:get_node_affiliations(Nidx). get_affiliation(Nidx, Owner) -> - SubKey = jlib:jid_tolower(Owner), - GenKey = jlib:jid_remove_resource(SubKey), - #pubsub_state{affiliation = Affiliation} = get_state(Nidx, GenKey), - {result, Affiliation}. + node_flat:get_affiliation(Nidx, Owner). set_affiliation(Nidx, Owner, Affiliation) -> - SubKey = jlib:jid_tolower(Owner), - GenKey = jlib:jid_remove_resource(SubKey), - GenState = get_state(Nidx, GenKey), - case {Affiliation, GenState#pubsub_state.subscriptions} of - {none, []} -> del_state(Nidx, GenKey); - _ -> set_state(GenState#pubsub_state{affiliation = Affiliation}) - end. - -%% @doc <p>Return the current subscriptions for the given user</p> -%% <p>The default module reads subscriptions in the main Mnesia -%% <tt>pubsub_state</tt> table. If a plugin stores its data in the same -%% table, it should return an empty list, as the affiliation will be read by -%% the default PubSub module. Otherwise, it should return its own affiliation, -%% that will be added to the affiliation stored in the main -%% <tt>pubsub_state</tt> table.</p> + node_flat:set_affiliation(Nidx, Owner, Affiliation). + get_entity_subscriptions(Host, Owner) -> - {U, D, _} = SubKey = jlib:jid_tolower(Owner), - GenKey = jlib:jid_remove_resource(SubKey), - States = case SubKey of - GenKey -> - mnesia:match_object(#pubsub_state{stateid = {{U, D, '_'}, '_'}, _ = '_'}); - _ -> - mnesia:match_object(#pubsub_state{stateid = {GenKey, '_'}, _ = '_'}) - ++ - mnesia:match_object(#pubsub_state{stateid = {SubKey, '_'}, _ = '_'}) - end, - NodeTree = mod_pubsub:tree(Host), - Reply = lists:foldl(fun (#pubsub_state{stateid = {J, N}, subscriptions = Ss}, Acc) -> - case NodeTree:get_node(N) of - #pubsub_node{nodeid = {Host, _}} = Node -> - lists:foldl(fun ({Sub, SubId}, Acc2) -> - [{Node, Sub, SubId, J} | Acc2] - end, - Acc, Ss); - _ -> - Acc - end - end, - [], States), - {result, Reply}. + node_flat:get_entity_subscriptions(Host, Owner). get_node_subscriptions(Nidx) -> - {result, States} = get_states(Nidx), - Tr = fun (#pubsub_state{stateid = {J, _}, subscriptions = Subscriptions}) -> - case Subscriptions of - [_ | _] -> - lists:foldl(fun ({S, SubId}, Acc) -> - [{J, S, SubId} | Acc] - end, - [], Subscriptions); - [] -> - []; - _ -> - [{J, none}] - end - end, - {result, lists:flatmap(Tr, States)}. + node_flat:get_node_subscriptions(Nidx). get_subscriptions(Nidx, Owner) -> - SubKey = jlib:jid_tolower(Owner), - SubState = get_state(Nidx, SubKey), - {result, SubState#pubsub_state.subscriptions}. + node_flat:get_subscriptions(Nidx, Owner). set_subscriptions(Nidx, Owner, Subscription, SubId) -> - SubKey = jlib:jid_tolower(Owner), - SubState = get_state(Nidx, SubKey), - case {SubId, SubState#pubsub_state.subscriptions} of - {_, []} -> - case Subscription of - none -> - {error, - ?ERR_EXTENDED((?ERR_BAD_REQUEST), <<"not-subscribed">>)}; - _ -> - new_subscription(Nidx, Owner, Subscription, SubState) - end; - {<<>>, [{_, SID}]} -> - case Subscription of - none -> unsub_with_subid(Nidx, SID, SubState); - _ -> replace_subscription({Subscription, SID}, SubState) - end; - {<<>>, [_ | _]} -> - {error, - ?ERR_EXTENDED((?ERR_BAD_REQUEST), <<"subid-required">>)}; - _ -> - case Subscription of - none -> unsub_with_subid(Nidx, SubId, SubState); - _ -> replace_subscription({Subscription, SubId}, SubState) - end - end. - -replace_subscription(NewSub, SubState) -> - NewSubs = replace_subscription(NewSub, SubState#pubsub_state.subscriptions, []), - set_state(SubState#pubsub_state{subscriptions = NewSubs}). - -replace_subscription(_, [], Acc) -> Acc; -replace_subscription({Sub, SubId}, [{_, SubId} | T], Acc) -> - replace_subscription({Sub, SubId}, T, [{Sub, SubId} | Acc]). - -new_subscription(Nidx, Owner, Sub, SubState) -> - SubId = pubsub_subscription:add_subscription(Owner, Nidx, []), - Subs = SubState#pubsub_state.subscriptions, - set_state(SubState#pubsub_state{subscriptions = [{Sub, SubId} | Subs]}), - {Sub, SubId}. - -unsub_with_subid(Nidx, SubId, #pubsub_state{stateid = {Entity, _}} = SubState) -> - pubsub_subscription:delete_subscription(SubState#pubsub_state.stateid, Nidx, SubId), - NewSubs = [{S, Sid} - || {S, Sid} <- SubState#pubsub_state.subscriptions, - SubId =/= Sid], - case {NewSubs, SubState#pubsub_state.affiliation} of - {[], none} -> del_state(Nidx, Entity); - _ -> set_state(SubState#pubsub_state{subscriptions = NewSubs}) - end. - -%% @doc <p>Returns a list of Owner's nodes on Host with pending -%% subscriptions.</p> + node_flat:set_subscriptions(Nidx, Owner, Subscription, SubId). + get_pending_nodes(Host, Owner) -> - GenKey = jlib:jid_remove_resource(jlib:jid_tolower(Owner)), - States = mnesia:match_object(#pubsub_state{stateid = {GenKey, '_'}, - affiliation = owner, - _ = '_'}), - NodeIdxs = [Nidx || #pubsub_state{stateid = {_, Nidx}} <- States], - NodeTree = mod_pubsub:tree(Host), - Reply = mnesia:foldl(fun (#pubsub_state{stateid = {_, Nidx}} = S, Acc) -> - case lists:member(Nidx, NodeIdxs) of - true -> - case get_nodes_helper(NodeTree, S) of - {value, Node} -> [Node | Acc]; - false -> Acc - end; - false -> - Acc - end - end, - [], pubsub_state), - {result, Reply}. - -get_nodes_helper(NodeTree, #pubsub_state{stateid = {_, N}, subscriptions = Subs}) -> - HasPending = fun - ({pending, _}) -> true; - (pending) -> true; - (_) -> false - end, - case lists:any(HasPending, Subs) of - true -> - case NodeTree:get_node(N) of - #pubsub_node{nodeid = {_, Node}} -> {value, Node}; - _ -> false - end; - false -> - false - end. - -%% @doc Returns the list of stored states for a given node. -%% <p>For the default PubSub module, states are stored in Mnesia database.</p> -%% <p>We can consider that the pubsub_state table have been created by the main -%% mod_pubsub module.</p> -%% <p>PubSub plugins can store the states where they wants (for example in a -%% relational database).</p> -%% <p>If a PubSub plugin wants to delegate the states storage to the default node, -%% they can implement this function like this: -%% ```get_states(Nidx) -> -%% node_default:get_states(Nidx).'''</p> + node_flat:get_pending_nodes(Host, Owner). + get_states(Nidx) -> - States = case catch mnesia:match_object( - #pubsub_state{stateid = {'_', Nidx}, _ = '_'}) of - List when is_list(List) -> List; - _ -> [] - end, - {result, States}. - -%% @doc <p>Returns a state (one state list), given its reference.</p> -get_state(Nidx, Key) -> - StateId = {Key, Nidx}, - case catch mnesia:read({pubsub_state, StateId}) of - [State] when is_record(State, pubsub_state) -> State; - _ -> #pubsub_state{stateid = StateId} - end. - -%% @doc <p>Write a state into database.</p> -set_state(State) when is_record(State, pubsub_state) -> - mnesia:write(State). -%set_state(_) -> {error, ?ERR_INTERNAL_SERVER_ERROR}. - -%% @doc <p>Delete a state from database.</p> -del_state(Nidx, Key) -> - mnesia:delete({pubsub_state, {Key, Nidx}}). - -%% @doc Returns the list of stored items for a given node. -%% <p>For the default PubSub module, items are stored in Mnesia database.</p> -%% <p>We can consider that the pubsub_item table have been created by the main -%% mod_pubsub module.</p> -%% <p>PubSub plugins can store the items where they wants (for example in a -%% relational database), or they can even decide not to persist any items.</p> -%% <p>If a PubSub plugin wants to delegate the item storage to the default node, -%% they can implement this function like this: -%% ```get_items(Nidx, From) -> -%% node_default:get_items(Nidx, From).'''</p> -get_items(Nidx, From) -> - get_items(Nidx, From, none). -get_items(Nidx, _From, _RSM) -> - Items = mnesia:match_object(#pubsub_item{itemid = {'_', Nidx}, _ = '_'}), - {result, {lists:reverse(lists:keysort(#pubsub_item.modification, Items)), none}}. - -get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> - get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, none). -get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId, _RSM) -> - SubKey = jlib:jid_tolower(JID), - GenKey = jlib:jid_remove_resource(SubKey), - GenState = get_state(Nidx, GenKey), - SubState = get_state(Nidx, SubKey), - Affiliation = GenState#pubsub_state.affiliation, - Subscriptions = SubState#pubsub_state.subscriptions, - Whitelisted = can_fetch_item(Affiliation, Subscriptions), - if %%SubId == "", ?? -> - %% Entity has multiple subscriptions to the node but does not specify a subscription ID - %{error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; - %%InvalidSubId -> - %% Entity is subscribed but specifies an invalid subscription ID - %{error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; - (Affiliation == outcast) or (Affiliation == publish_only) -> - {error, ?ERR_FORBIDDEN}; - (AccessModel == presence) and not PresenceSubscription -> - {error, - ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), <<"presence-subscription-required">>)}; - (AccessModel == roster) and not RosterGroup -> - {error, - ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), <<"not-in-roster-group">>)}; - (AccessModel == whitelist) and not Whitelisted -> - {error, - ?ERR_EXTENDED((?ERR_NOT_ALLOWED), <<"closed-node">>)}; - (AccessModel == authorize) and not Whitelisted -> - {error, ?ERR_FORBIDDEN}; - %%MustPay -> - %% % Payment is required for a subscription - %% {error, ?ERR_PAYMENT_REQUIRED}; - true -> - get_items(Nidx, JID) - end. - -%% @doc <p>Returns an item (one item list), given its reference.</p> + node_flat:get_states(Nidx). + +get_state(Nidx, JID) -> + node_flat:get_state(Nidx, JID). + +set_state(State) -> + node_flat:set_state(State). + +get_items(Nidx, From, RSM) -> + node_flat:get_items(Nidx, From, RSM). + +get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM) -> + node_flat:get_items(Nidx, JID, AccessModel, + PresenceSubscription, RosterGroup, SubId, RSM). get_item(Nidx, ItemId) -> - case mnesia:read({pubsub_item, {ItemId, Nidx}}) of - [Item] when is_record(Item, pubsub_item) -> {result, Item}; - _ -> {error, ?ERR_ITEM_NOT_FOUND} - end. - -get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId) -> - SubKey = jlib:jid_tolower(JID), - GenKey = jlib:jid_remove_resource(SubKey), - GenState = get_state(Nidx, GenKey), - Affiliation = GenState#pubsub_state.affiliation, - Subscriptions = GenState#pubsub_state.subscriptions, - Whitelisted = can_fetch_item(Affiliation, Subscriptions), - if %%SubId == "", ?? -> - %% Entity has multiple subscriptions to the node but does not specify a subscription ID - %{error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; - %%InvalidSubId -> - %% Entity is subscribed but specifies an invalid subscription ID - %{error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; - (Affiliation == outcast) or (Affiliation == publish_only) -> - {error, ?ERR_FORBIDDEN}; - (AccessModel == presence) and not PresenceSubscription -> - {error, - ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), <<"presence-subscription-required">>)}; - (AccessModel == roster) and not RosterGroup -> - {error, - ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), <<"not-in-roster-group">>)}; - (AccessModel == whitelist) and not Whitelisted -> - {error, - ?ERR_EXTENDED((?ERR_NOT_ALLOWED), <<"closed-node">>)}; - (AccessModel == authorize) and not Whitelisted -> - {error, ?ERR_FORBIDDEN}; - %%MustPay -> - %% % Payment is required for a subscription - %% {error, ?ERR_PAYMENT_REQUIRED}; - true -> - get_item(Nidx, ItemId) - end. - -%% @doc <p>Write an item into database.</p> -set_item(Item) when is_record(Item, pubsub_item) -> - mnesia:write(Item). -%set_item(_) -> {error, ?ERR_INTERNAL_SERVER_ERROR}. - -%% @doc <p>Delete an item from database.</p> -del_item(Nidx, ItemId) -> - mnesia:delete({pubsub_item, {ItemId, Nidx}}). - -del_items(Nidx, ItemIds) -> - lists:foreach(fun (ItemId) -> del_item(Nidx, ItemId) - end, - ItemIds). - -get_item_name(_Host, _Node, Id) -> - Id. - -%% @doc <p>Return the name of the node if known: Default is to return -%% node id.</p> + node_flat:get_item(Nidx, ItemId). + +get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> + node_flat:get_item(Nidx, ItemId, JID, AccessModel, + PresenceSubscription, RosterGroup, SubId). + +set_item(Item) -> + node_flat:set_item(Item). + +get_item_name(Host, Node, Id) -> + node_flat:get_item_name(Host, Node, Id). + +%% @doc <p>Return the path of the node.</p> node_to_path(Node) -> str:tokens(Node, <<"/">>). path_to_node([]) -> <<>>; path_to_node(Path) -> iolist_to_binary(str:join([<<"">> | Path], <<"/">>)). -can_fetch_item(owner, _) -> true; -can_fetch_item(member, _) -> true; -can_fetch_item(publisher, _) -> true; -can_fetch_item(publish_only, _) -> false; -can_fetch_item(outcast, _) -> false; -can_fetch_item(none, Subscriptions) -> is_subscribed(Subscriptions). -%can_fetch_item(_Affiliation, _Subscription) -> false. - -is_subscribed(Subscriptions) -> - lists:any(fun - ({subscribed, _SubId}) -> true; - (_) -> false - end, - Subscriptions). - -first_in_list(_Pred, []) -> - false; -first_in_list(Pred, [H | T]) -> - case Pred(H) of - true -> {value, H}; - _ -> first_in_list(Pred, T) - end. diff --git a/src/node_hometree_odbc.erl b/src/node_hometree_odbc.erl index b9abac20..6ac5c37b 100644 --- a/src/node_hometree_odbc.erl +++ b/src/node_hometree_odbc.erl @@ -1,43 +1,27 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. +%%%---------------------------------------------------------------------- +%%% File : node_hometree_odbc.erl +%%% Author : Christophe Romain <christophe.romain@process-one.net> +%%% Purpose : Standard tree ordered node plugin with ODBC backend +%%% Created : 1 Dec 2007 by Christophe Romain <christophe.romain@process-one.net> %%% %%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. +%%% ejabberd, Copyright (C) 2002-2016 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. %%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. +%%% 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. %%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Christophe Romain <christophe.romain@process-one.net> -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== - -%%% @todo The item table should be handled by the plugin, but plugin that do -%%% not want to manage it should be able to use the default behaviour. -%%% @todo Plugin modules should be able to register to receive presence update -%%% send to pubsub. - -%%% @doc The module <strong>{@module}</strong> is the default PubSub plugin. -%%% <p>It is used as a default for all unknown PubSub node type. It can serve -%%% as a developer basis and reference to build its own custom pubsub node -%%% types.</p> -%%% <p>PubSub plugin nodes are using the {@link gen_node} behaviour.</p> -%%% <p><strong>The API isn't stabilized yet</strong>. The pubsub plugin -%%% development is still a work in progress. However, the system is already -%%% useable and useful as is. Please, send us comments, feedback and -%%% improvements.</p> +%%% 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(node_hometree_odbc). -behaviour(gen_pubsub_node). @@ -55,26 +39,20 @@ get_entity_subscriptions/2, get_node_subscriptions/1, get_subscriptions/2, set_subscriptions/4, get_pending_nodes/2, get_states/1, get_state/2, - set_state/1, get_items/7, get_items/3, - get_items/6, get_items/2, get_item/7, + set_state/1, get_items/7, get_items/3, get_item/7, get_item/2, set_item/1, get_item_name/3, node_to_path/1, path_to_node/1, get_entity_subscriptions_for_send_last/2, get_last_items/3]). --export([decode_jid/1, encode_jid/1, - decode_affiliation/1, decode_subscriptions/1, - encode_affiliation/1, encode_subscriptions/1, - encode_host/1]). - -init(Host, ServerHost, _Opts) -> - pubsub_subscription_odbc:init(), +init(Host, ServerHost, Opts) -> + node_flat_odbc:init(Host, ServerHost, Opts), Owner = mod_pubsub:service_jid(Host), mod_pubsub:create_node(Host, ServerHost, <<"/home">>, Owner, <<"hometree">>), mod_pubsub:create_node(Host, ServerHost, <<"/home/", ServerHost/binary>>, Owner, <<"hometree">>), ok. -terminate(_Host, _ServerHost) -> - ok. +terminate(Host, ServerHost) -> + node_flat_odbc:terminate(Host, ServerHost). options() -> [{odbc, true}, {rsm, true} | node_hometree:options()]. @@ -82,921 +60,96 @@ options() -> features() -> [<<"rsm">> | node_hometree:features()]. -%% @doc Checks if the current user has the permission to create the requested node -%% <p>In {@link node_default}, the permission is decided by the place in the -%% hierarchy where the user is creating the node. The access parameter is also -%% checked in the default module. This parameter depends on the value of the -%% <tt>access_createnode</tt> ACL value in ejabberd config file.</p> -%% <p>This function also check that node can be created a a children of its -%% parent node</p> create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) -> node_hometree:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). create_node(Nidx, Owner) -> - {_U, _S, _R} = OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), - State = #pubsub_state{stateid = {OwnerKey, Nidx}, affiliation = owner}, - catch ejabberd_odbc:sql_query_t([<<"insert into pubsub_state(nodeid, jid, affiliation, subscriptions) " - "values(">>, state_to_raw(Nidx, State), <<");">>]), - {result, {default, broadcast}}. + node_flat_odbc:create_node(Nidx, Owner). delete_node(Nodes) -> - Reply = lists:map(fun (#pubsub_node{id = Nidx} = Node) -> - Subscriptions = case catch - ejabberd_odbc:sql_query_t([<<"select jid, subscriptions " - "from pubsub_state where nodeid='">>, Nidx, <<"';">>]) - of - {selected, [<<"jid">>, <<"subscriptions">>], RItems} -> - [{decode_jid(SJID), decode_subscriptions(Subs)} || [SJID, Subs] <- RItems]; - _ -> - [] - end, - {Node, Subscriptions} - end, - Nodes), - {result, {default, broadcast, Reply}}. + node_flat_odbc:delete_node(Nodes). -%% @doc <p>Accepts or rejects subcription requests on a PubSub node.</p> -%% <p>The mechanism works as follow: -%% <ul> -%% <li>The main PubSub module prepares the subscription and passes the -%% result of the preparation as a record.</li> -%% <li>This function gets the prepared record and several other parameters and -%% can decide to:<ul> -%% <li>reject the subscription;</li> -%% <li>allow it as is, letting the main module perform the database -%% persistance;</li> -%% <li>allow it, modifying the record. The main module will store the -%% modified record;</li> -%% <li>allow it, but perform the needed persistance operations.</li></ul> -%% </li></ul></p> -%% <p>The selected behaviour depends on the return parameter: -%% <ul> -%% <li><tt>{error, Reason}</tt>: an IQ error result will be returned. No -%% subscription will actually be performed.</li> -%% <li><tt>true</tt>: Subscribe operation is allowed, based on the -%% unmodified record passed in parameter <tt>SubscribeResult</tt>. If this -%% parameter contains an error, no subscription will be performed.</li> -%% <li><tt>{true, PubsubState}</tt>: Subscribe operation is allowed, but -%% the {@link mod_pubsub:pubsubState()} record returned replaces the value -%% passed in parameter <tt>SubscribeResult</tt>.</li> -%% <li><tt>{true, done}</tt>: Subscribe operation is allowed, but the -%% {@link mod_pubsub:pubsubState()} will be considered as already stored and -%% no further persistance operation will be performed. This case is used, -%% when the plugin module is doing the persistance by itself or when it want -%% to completly disable persistance.</li></ul> -%% </p> -%% <p>In the default plugin module, the record is unchanged.</p> subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options) -> - SubKey = jlib:jid_tolower(Subscriber), - GenKey = jlib:jid_remove_resource(SubKey), - Authorized = jlib:jid_tolower(jlib:jid_remove_resource(Sender)) == GenKey, - {Affiliation, Subscriptions} = select_affiliation_subscriptions(Nidx, GenKey, SubKey), - Whitelisted = lists:member(Affiliation, [member, publisher, owner]), - PendingSubscription = lists:any(fun - ({pending, _}) -> true; - (_) -> false - end, - Subscriptions), - Owner = Affiliation == owner, - if not Authorized -> - {error, - ?ERR_EXTENDED((?ERR_BAD_REQUEST), <<"invalid-jid">>)}; - (Affiliation == outcast) or (Affiliation == publish_only) -> - {error, ?ERR_FORBIDDEN}; - PendingSubscription -> - {error, - ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), <<"pending-subscription">>)}; - (AccessModel == presence) and (not PresenceSubscription) and (not Owner) -> - {error, - ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), <<"presence-subscription-required">>)}; - (AccessModel == roster) and (not RosterGroup) and (not Owner) -> - {error, - ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), <<"not-in-roster-group">>)}; - (AccessModel == whitelist) and (not Whitelisted) and (not Owner) -> - {error, - ?ERR_EXTENDED((?ERR_NOT_ALLOWED), <<"closed-node">>)}; - %%MustPay -> - %% % Payment is required for a subscription - %% {error, ?ERR_PAYMENT_REQUIRED}; - %%ForbiddenAnonymous -> - %% % Requesting entity is anonymous - %% {error, ?ERR_FORBIDDEN}; - true -> - {result, SubId} = pubsub_subscription_odbc:subscribe_node(Subscriber, Nidx, Options), - NewSub = case AccessModel of - authorize -> pending; - _ -> subscribed - end, - update_subscription(Nidx, SubKey, [{NewSub, SubId} | Subscriptions]), - case {NewSub, SendLast} of - {subscribed, never} -> {result, {default, subscribed, SubId}}; - {subscribed, _} -> {result, {default, subscribed, SubId, send_last}}; - {_, _} -> {result, {default, pending, SubId}} - end - end. + node_flat_odbc:subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, + PresenceSubscription, RosterGroup, Options). -%% @doc <p>Unsubscribe the <tt>Subscriber</tt> from the <tt>Node</tt>.</p> unsubscribe_node(Nidx, Sender, Subscriber, SubId) -> - SubKey = jlib:jid_tolower(Subscriber), - GenKey = jlib:jid_remove_resource(SubKey), - Authorized = jlib:jid_tolower(jlib:jid_remove_resource(Sender)) == GenKey, - {Affiliation, Subscriptions} = select_affiliation_subscriptions(Nidx, SubKey), - SubIdExists = case SubId of - <<>> -> false; - Binary when is_binary(Binary) -> true; - _ -> false - end, - if - %% Requesting entity is prohibited from unsubscribing entity - not Authorized -> - {error, ?ERR_FORBIDDEN}; - %% Entity did not specify SubId - %%SubId == "", ?? -> - %% {error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; - %% Invalid subscription identifier - %%InvalidSubId -> - %% {error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; - %% Requesting entity is not a subscriber - Subscriptions == [] -> - {error, - ?ERR_EXTENDED((?ERR_UNEXPECTED_REQUEST_CANCEL), <<"not-subscribed">>)}; - %% Subid supplied, so use that. - SubIdExists -> - Sub = first_in_list(fun - ({_, S}) when S == SubId -> true; - (_) -> false - end, - Subscriptions), - case Sub of - {value, S} -> - delete_subscription(SubKey, Nidx, S, Affiliation, Subscriptions), - {result, default}; - false -> - {error, - ?ERR_EXTENDED((?ERR_UNEXPECTED_REQUEST_CANCEL), <<"not-subscribed">>)} - end; - %% Asking to remove all subscriptions to the given node - SubId == all -> - [delete_subscription(SubKey, Nidx, S, Affiliation, Subscriptions) - || S <- Subscriptions], - {result, default}; - %% No subid supplied, but there's only one matching subscription - length(Subscriptions) == 1 -> - delete_subscription(SubKey, Nidx, hd(Subscriptions), Affiliation, Subscriptions), - {result, default}; - %% No subid and more than one possible subscription match. - true -> - {error, - ?ERR_EXTENDED((?ERR_BAD_REQUEST), <<"subid-required">>)} - end. + node_flat_odbc:unsubscribe_node(Nidx, Sender, Subscriber, SubId). -delete_subscription(SubKey, Nidx, {Subscription, SubId}, Affiliation, Subscriptions) -> - NewSubs = Subscriptions -- [{Subscription, SubId}], - pubsub_subscription_odbc:unsubscribe_node(SubKey, Nidx, SubId), - case {Affiliation, NewSubs} of - {none, []} -> del_state(Nidx, SubKey); - _ -> update_subscription(Nidx, SubKey, NewSubs) - end. +publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload) -> + node_flat_odbc:publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload). -%% @doc <p>Publishes the item passed as parameter.</p> -%% <p>The mechanism works as follow: -%% <ul> -%% <li>The main PubSub module prepares the item to publish and passes the -%% result of the preparation as a {@link mod_pubsub:pubsubItem()} record.</li> -%% <li>This function gets the prepared record and several other parameters and can decide to:<ul> -%% <li>reject the publication;</li> -%% <li>allow the publication as is, letting the main module perform the database persistance;</li> -%% <li>allow the publication, modifying the record. The main module will store the modified record;</li> -%% <li>allow it, but perform the needed persistance operations.</li></ul> -%% </li></ul></p> -%% <p>The selected behaviour depends on the return parameter: -%% <ul> -%% <li><tt>{error, Reason}</tt>: an iq error result will be return. No -%% publication is actually performed.</li> -%% <li><tt>true</tt>: Publication operation is allowed, based on the -%% unmodified record passed in parameter <tt>Item</tt>. If the <tt>Item</tt> -%% parameter contains an error, no subscription will actually be -%% performed.</li> -%% <li><tt>{true, Item}</tt>: Publication operation is allowed, but the -%% {@link mod_pubsub:pubsubItem()} record returned replaces the value passed -%% in parameter <tt>Item</tt>. The persistance will be performed by the main -%% module.</li> -%% <li><tt>{true, done}</tt>: Publication operation is allowed, but the -%% {@link mod_pubsub:pubsubItem()} will be considered as already stored and -%% no further persistance operation will be performed. This case is used, -%% when the plugin module is doing the persistance by itself or when it want -%% to completly disable persistance.</li></ul> -%% </p> -%% <p>In the default plugin module, the record is unchanged.</p> -publish_item(Nidx, Publisher, PublishModel, MaxItems, ItemId, Payload) -> - SubKey = jlib:jid_tolower(Publisher), - GenKey = jlib:jid_remove_resource(SubKey), - {Affiliation, Subscriptions} = select_affiliation_subscriptions(Nidx, GenKey, SubKey), - Subscribed = case PublishModel of - subscribers -> is_subscribed(Subscriptions); - _ -> undefined - end, - if not ((PublishModel == open) or - (PublishModel == publishers) and - ((Affiliation == owner) - or (Affiliation == publisher) - or (Affiliation == publish_only)) - or (Subscribed == true)) -> - {error, ?ERR_FORBIDDEN}; - true -> - if MaxItems > 0 -> - PubId = {now(), SubKey}, - set_item(#pubsub_item{itemid = {ItemId, Nidx}, - creation = {now(), GenKey}, - modification = PubId, - payload = Payload}), - Items = [ItemId | itemids(Nidx, GenKey) -- [ItemId]], - {result, {_, OI}} = remove_extra_items(Nidx, MaxItems, Items), - {result, {default, broadcast, OI}}; - true -> - {result, {default, broadcast, []}} - end - end. - -%% @doc <p>This function is used to remove extra items, most notably when the -%% maximum number of items has been reached.</p> -%% <p>This function is used internally by the core PubSub module, as no -%% permission check is performed.</p> -%% <p>In the default plugin module, the oldest items are removed, but other -%% rules can be used.</p> -%% <p>If another PubSub plugin wants to delegate the item removal (and if the -%% plugin is using the default pubsub storage), it can implements this function like this: -%% ```remove_extra_items(Nidx, MaxItems, ItemIds) -> -%% node_default:remove_extra_items(Nidx, MaxItems, ItemIds).'''</p> -remove_extra_items(_Nidx, unlimited, ItemIds) -> - {result, {ItemIds, []}}; remove_extra_items(Nidx, MaxItems, ItemIds) -> - NewItems = lists:sublist(ItemIds, MaxItems), - OldItems = lists:nthtail(length(NewItems), ItemIds), - del_items(Nidx, OldItems), - {result, {NewItems, OldItems}}. + node_flat_odbc:remove_extra_items(Nidx, MaxItems, ItemIds). -%% @doc <p>Triggers item deletion.</p> -%% <p>Default plugin: The user performing the deletion must be the node owner -%% or a publisher, or PublishModel being open.</p> delete_item(Nidx, Publisher, PublishModel, ItemId) -> - SubKey = jlib:jid_tolower(Publisher), - GenKey = jlib:jid_remove_resource(SubKey), - {result, Affiliation} = get_affiliation(Nidx, GenKey), - Allowed = Affiliation == publisher orelse - Affiliation == owner orelse - PublishModel == open orelse - case get_item(Nidx, ItemId) of - {result, #pubsub_item{creation = {_, GenKey}}} -> true; - _ -> false - end, - if not Allowed -> - {error, ?ERR_FORBIDDEN}; - true -> - case del_item(Nidx, ItemId) of - {updated, 1} -> {result, {default, broadcast}}; - _ -> {error, ?ERR_ITEM_NOT_FOUND} - end - end. + node_flat_odbc:delete_item(Nidx, Publisher, PublishModel, ItemId). purge_node(Nidx, Owner) -> - SubKey = jlib:jid_tolower(Owner), - GenKey = jlib:jid_remove_resource(SubKey), - GenState = get_state(Nidx, GenKey), - case GenState of - #pubsub_state{affiliation = owner} -> - {result, States} = get_states(Nidx), - lists:foreach(fun - (#pubsub_state{items = []}) -> ok; - (#pubsub_state{items = Items}) -> del_items(Nidx, Items) - end, - States), - {result, {default, broadcast}}; - _ -> - {error, ?ERR_FORBIDDEN} - end. + node_flat_odbc:purge_node(Nidx, Owner). -%% @doc <p>Return the current affiliations for the given user</p> -%% <p>The default module reads affiliations in the main Mnesia -%% <tt>pubsub_state</tt> table. If a plugin stores its data in the same -%% table, it should return an empty list, as the affiliation will be read by -%% the default PubSub module. Otherwise, it should return its own affiliation, -%% that will be added to the affiliation stored in the main -%% <tt>pubsub_state</tt> table.</p> get_entity_affiliations(Host, Owner) -> - SubKey = jlib:jid_tolower(Owner), - GenKey = jlib:jid_remove_resource(SubKey), - H = encode_host(Host), - J = encode_jid(GenKey), - Reply = case catch - ejabberd_odbc:sql_query_t([<<"select node, type, i.nodeid, affiliation " - "from pubsub_state i, pubsub_node n where " - "i.nodeid = n.nodeid and jid='">>, J, <<"' and host='">>, H, <<"';">>]) - of - {selected, [<<"node">>, <<"type">>, <<"nodeid">>, <<"affiliation">>], RItems} -> - [{nodetree_tree_odbc:raw_to_node(Host, [N, <<"">>, T, I]), decode_affiliation(A)} - || [N, T, I, A] <- RItems]; - _ -> - [] - end, - {result, Reply}. + node_flat_odbc:get_entity_affiliations(Host, Owner). get_node_affiliations(Nidx) -> - Reply = case catch - ejabberd_odbc:sql_query_t([<<"select jid, affiliation from pubsub_state " - "where nodeid='">>, Nidx, <<"';">>]) - of - {selected, [<<"jid">>, <<"affiliation">>], RItems} -> - [{decode_jid(J), decode_affiliation(A)} || [J, A] <- RItems]; - _ -> - [] - end, - {result, Reply}. + node_flat_odbc:get_node_affiliations(Nidx). get_affiliation(Nidx, Owner) -> - SubKey = jlib:jid_tolower(Owner), - GenKey = jlib:jid_remove_resource(SubKey), - J = encode_jid(GenKey), - Reply = case catch - ejabberd_odbc:sql_query_t([<<"select affiliation from pubsub_state " - "where nodeid='">>, Nidx, <<"' and jid='">>, J, <<"';">>]) - of - {selected, [<<"affiliation">>], [[A]]} -> - decode_affiliation(A); - _ -> - none - end, - {result, Reply}. + node_flat_odbc:get_affiliation(Nidx, Owner). set_affiliation(Nidx, Owner, Affiliation) -> - SubKey = jlib:jid_tolower(Owner), - GenKey = jlib:jid_remove_resource(SubKey), - {_, Subscriptions} = select_affiliation_subscriptions(Nidx, GenKey), - case {Affiliation, Subscriptions} of - {none, []} -> del_state(Nidx, GenKey); - _ -> update_affiliation(Nidx, GenKey, Affiliation) - end. + node_flat_odbc:set_affiliation(Nidx, Owner, Affiliation). -%% @doc <p>Return the current subscriptions for the given user</p> -%% <p>The default module reads subscriptions in the main Mnesia -%% <tt>pubsub_state</tt> table. If a plugin stores its data in the same -%% table, it should return an empty list, as the affiliation will be read by -%% the default PubSub module. Otherwise, it should return its own affiliation, -%% that will be added to the affiliation stored in the main -%% <tt>pubsub_state</tt> table.</p> get_entity_subscriptions(Host, Owner) -> - SubKey = jlib:jid_tolower(Owner), - GenKey = jlib:jid_remove_resource(SubKey), - H = encode_host(Host), - SJ = encode_jid(SubKey), - GJ = encode_jid(GenKey), - Query = case SubKey of - GenKey -> - [<<"select node, type, i.nodeid, jid, subscriptions " - "from pubsub_state i, pubsub_node n " - "where i.nodeid = n.nodeid and jid like '">>, GJ, <<"%' and host='">>, H, <<"';">>]; - _ -> - [<<"select node, type, i.nodeid, jid, subscriptions " - "from pubsub_state i, pubsub_node n " - "where i.nodeid = n.nodeid and jid in ('">>, SJ, <<"', '">>, GJ, <<"') and host='">>, H, <<"';">>] - end, - Reply = case catch ejabberd_odbc:sql_query_t(Query) of - {selected, - [<<"node">>, <<"type">>, <<"nodeid">>, <<"jid">>, <<"subscriptions">>], RItems} -> - lists:foldl(fun ([N, T, I, J, S], Acc) -> - Node = nodetree_tree_odbc:raw_to_node(Host, [N, <<"">>, T, I]), - Jid = decode_jid(J), - case decode_subscriptions(S) of - [] -> - [{Node, none, Jid} | Acc]; - Subs -> - lists:foldl(fun ({Sub, SubId}, Acc2) -> - [{Node, Sub, SubId, Jid} | Acc2] - end, - Acc, Subs) - end - end, - [], RItems); - _ -> - [] - end, - {result, Reply}. + node_flat_odbc:get_entity_subscriptions(Host, Owner). -%% do the same as get_entity_subscriptions but filter result only to -%% nodes having send_last_published_item=on_sub_and_presence -%% as this call avoid seeking node, it must return node and type as well --spec(get_entity_subscriptions_for_send_last/2 :: - ( - Host :: mod_pubsub:hostPubsub(), - Owner :: jid()) - -> {result, - [{mod_pubsub:pubsubNode(), - mod_pubsub:subscription(), - mod_pubsub:subId(), - ljid()}] - } - ). get_entity_subscriptions_for_send_last(Host, Owner) -> - SubKey = jlib:jid_tolower(Owner), - GenKey = jlib:jid_remove_resource(SubKey), - H = encode_host(Host), - SJ = encode_jid(SubKey), - GJ = encode_jid(GenKey), - Query = case SubKey of - GenKey -> - [<<"select node, type, i.nodeid, jid, subscriptions " - "from pubsub_state i, pubsub_node n, pubsub_node_option o " - "where i.nodeid = n.nodeid and n.nodeid = o.nodeid and name='send_last_published_item' " - "and val='on_sub_and_presence' and jid like '">>, GJ, <<"%' and host='">>, H, <<"';">>]; - _ -> - [<<"select node, type, i.nodeid, jid, subscriptions " - "from pubsub_state i, pubsub_node n, pubsub_node_option o " - "where i.nodeid = n.nodeid and n.nodeid = o.nodeid and name='send_last_published_item' " - "and val='on_sub_and_presence' and jid in ('">>, SJ, <<"', '">>, GJ, <<"') and host='">>, H, <<"';">>] - end, - Reply = case catch ejabberd_odbc:sql_query_t(Query) of - {selected, - [<<"node">>, <<"type">>, <<"nodeid">>, <<"jid">>, <<"subscriptions">>], RItems} -> - lists:foldl(fun ([N, T, I, J, S], Acc) -> - Node = nodetree_tree_odbc:raw_to_node(Host, [N, <<"">>, T, I]), - Jid = decode_jid(J), - case decode_subscriptions(S) of - [] -> - [{Node, none, Jid} | Acc]; - Subs -> - lists:foldl(fun ({Sub, SubId}, Acc2) -> - [{Node, Sub, SubId, Jid}| Acc2] - end, - Acc, Subs) - end - end, - [], RItems); - _ -> - [] - end, - {result, Reply}. + node_flat_odbc:get_entity_subscriptions_for_send_last(Host, Owner). get_node_subscriptions(Nidx) -> - Reply = case catch - ejabberd_odbc:sql_query_t([<<"select jid, subscriptions from pubsub_state " - "where nodeid='">>, Nidx, <<"';">>]) - of - {selected, [<<"jid">>, <<"subscriptions">>], RItems} -> - lists:foldl(fun ([J, S], Acc) -> - Jid = decode_jid(J), - case decode_subscriptions(S) of - [] -> - [{Jid, none} | Acc]; - Subs -> - lists:foldl(fun ({Sub, SubId}, Acc2) -> - [{Jid, Sub, SubId} | Acc2] - end, - Acc, Subs) - end - end, - [], RItems); - _ -> - [] - end, - {result, Reply}. + node_flat_odbc:get_node_subscriptions(Nidx). get_subscriptions(Nidx, Owner) -> - SubKey = jlib:jid_tolower(Owner), - J = encode_jid(SubKey), - Reply = case catch - ejabberd_odbc:sql_query_t([<<"select subscriptions from pubsub_state where " - "nodeid='">>, Nidx, <<"' and jid='">>, J, <<"';">>]) - of - {selected, [<<"subscriptions">>], [[S]]} -> - decode_subscriptions(S); - _ -> - [] - end, - {result, Reply}. + node_flat_odbc:get_subscriptions(Nidx, Owner). set_subscriptions(Nidx, Owner, Subscription, SubId) -> - SubKey = jlib:jid_tolower(Owner), - SubState = get_state_without_itemids(Nidx, SubKey), - case {SubId, SubState#pubsub_state.subscriptions} of - {_, []} -> - case Subscription of - none -> - {error, - ?ERR_EXTENDED((?ERR_BAD_REQUEST), <<"not-subscribed">>)}; - _ -> - new_subscription(Nidx, Owner, Subscription, SubState) - end; - {<<>>, [{_, SID}]} -> - case Subscription of - none -> unsub_with_subid(Nidx, SID, SubState); - _ -> replace_subscription({Subscription, SID}, SubState) - end; - {<<>>, [_ | _]} -> - {error, - ?ERR_EXTENDED((?ERR_BAD_REQUEST), <<"subid-required">>)}; - _ -> - case Subscription of - none -> unsub_with_subid(Nidx, SubId, SubState); - _ -> replace_subscription({Subscription, SubId}, SubState) - end - end. - -replace_subscription(NewSub, SubState) -> - NewSubs = replace_subscription(NewSub, SubState#pubsub_state.subscriptions, []), - set_state(SubState#pubsub_state{subscriptions = NewSubs}). - -replace_subscription(_, [], Acc) -> Acc; -replace_subscription({Sub, SubId}, [{_, SubId} | T], Acc) -> - replace_subscription({Sub, SubId}, T, [{Sub, SubId} | Acc]). - -new_subscription(Nidx, Owner, Subscription, SubState) -> - {result, SubId} = pubsub_subscription_odbc:subscribe_node(Owner, Nidx, []), - Subscriptions = [{Subscription, SubId} | SubState#pubsub_state.subscriptions], - set_state(SubState#pubsub_state{subscriptions = Subscriptions}), - {Subscription, SubId}. - -unsub_with_subid(Nidx, SubId, SubState) -> - pubsub_subscription_odbc:unsubscribe_node(SubState#pubsub_state.stateid, Nidx, SubId), - NewSubs = [{S, Sid} - || {S, Sid} <- SubState#pubsub_state.subscriptions, - SubId =/= Sid], - case {NewSubs, SubState#pubsub_state.affiliation} of - {[], none} -> del_state(Nidx, element(1, SubState#pubsub_state.stateid)); - _ -> set_state(SubState#pubsub_state{subscriptions = NewSubs}) - end. + node_flat_odbc:set_subscriptions(Nidx, Owner, Subscription, SubId). -%% @doc <p>Returns a list of Owner's nodes on Host with pending -%% subscriptions.</p> get_pending_nodes(Host, Owner) -> - GenKey = jlib:jid_remove_resource(jlib:jid_tolower(Owner)), - States = mnesia:match_object(#pubsub_state{stateid = {GenKey, '_'}, - affiliation = owner, _ = '_'}), - Nidxxs = [Nidx || #pubsub_state{stateid = {_, Nidx}} <- States], - NodeTree = mod_pubsub:tree(Host), - Reply = mnesia:foldl(fun (#pubsub_state{stateid = {_, Nidx}} = S, Acc) -> - case lists:member(Nidx, Nidxxs) of - true -> - case get_nodes_helper(NodeTree, S) of - {value, Node} -> [Node | Acc]; - false -> Acc - end; - false -> - Acc - end - end, - [], pubsub_state), - {result, Reply}. + node_flat_odbc:get_pending_nodes(Host, Owner). -get_nodes_helper(NodeTree, #pubsub_state{stateid = {_, N}, subscriptions = Subs}) -> - HasPending = fun - ({pending, _}) -> true; - (pending) -> true; - (_) -> false - end, - case lists:any(HasPending, Subs) of - true -> - case NodeTree:get_node(N) of - #pubsub_node{nodeid = {_, Node}} -> {value, Node}; - _ -> false - end; - false -> - false - end. - -%% @doc Returns the list of stored states for a given node. -%% <p>For the default PubSub module, states are stored in Mnesia database.</p> -%% <p>We can consider that the pubsub_state table have been created by the main -%% mod_pubsub module.</p> -%% <p>PubSub plugins can store the states where they wants (for example in a -%% relational database).</p> -%% <p>If a PubSub plugin wants to delegate the states storage to the default node, -%% they can implement this function like this: -%% ```get_states(Nidx) -> -%% node_default:get_states(Nidx).'''</p> get_states(Nidx) -> - case catch - ejabberd_odbc:sql_query_t([<<"select jid, affiliation, subscriptions " - "from pubsub_state where nodeid='">>, Nidx, <<"';">>]) - of - {selected, - [<<"jid">>, <<"affiliation">>, <<"subscriptions">>], RItems} -> - {result, - lists:map(fun ([SJID, Aff, Subs]) -> - #pubsub_state{stateid = {decode_jid(SJID), Nidx}, - items = itemids(Nidx, SJID), - affiliation = decode_affiliation(Aff), - subscriptions = decode_subscriptions(Subs)} - end, - RItems)}; - _ -> - {result, []} - end. + node_flat_odbc:get_states(Nidx). -%% @doc <p>Returns a state (one state list), given its reference.</p> get_state(Nidx, JID) -> - State = get_state_without_itemids(Nidx, JID), - {SJID, _} = State#pubsub_state.stateid, - State#pubsub_state{items = itemids(Nidx, SJID)}. - --spec(get_state_without_itemids/2 :: - (Nidx :: mod_pubsub:nodeIdx(), - Key :: ljid()) -> - mod_pubsub:pubsubState() - ). -get_state_without_itemids(Nidx, JID) -> - J = encode_jid(JID), - case catch - ejabberd_odbc:sql_query_t([<<"select jid, affiliation, subscriptions " - "from pubsub_state where jid='">>, J, <<"' and nodeid='">>, Nidx, <<"';">>]) - of - {selected, - [<<"jid">>, <<"affiliation">>, <<"subscriptions">>], [[SJID, Aff, Subs]]} -> - #pubsub_state{stateid = {decode_jid(SJID), Nidx}, - affiliation = decode_affiliation(Aff), - subscriptions = decode_subscriptions(Subs)}; - _ -> - #pubsub_state{stateid = {JID, Nidx}} - end. + node_flat_odbc:get_state(Nidx, JID). -%% @doc <p>Write a state into database.</p> set_state(State) -> - {_, Nidx} = State#pubsub_state.stateid, - set_state(Nidx, State). - -set_state(Nidx, State) -> - {JID, _} = State#pubsub_state.stateid, - J = encode_jid(JID), - S = encode_subscriptions(State#pubsub_state.subscriptions), - A = encode_affiliation(State#pubsub_state.affiliation), - case catch - ejabberd_odbc:sql_query_t([<<"update pubsub_state set subscriptions='">>, S, <<"', affiliation='">>, A, - <<"' where nodeid='">>, Nidx, <<"' and jid='">>, J, <<"';">>]) - of - {updated, 1} -> - ok; - _ -> - catch - ejabberd_odbc:sql_query_t([<<"insert into pubsub_state(nodeid, jid, affiliation, subscriptions) " - "values('">>, - Nidx, <<"', '">>, J, <<"', '">>, A, <<"', '">>, S, <<"');">>]) - end, - ok. - -%% @doc <p>Delete a state from database.</p> -del_state(Nidx, JID) -> - J = encode_jid(JID), - catch ejabberd_odbc:sql_query_t([<<"delete from pubsub_state where jid='">>, - J, <<"' and nodeid='">>, Nidx, <<"';">>]), - ok. - -%% @doc Returns the list of stored items for a given node. -%% <p>For the default PubSub module, items are stored in Mnesia database.</p> -%% <p>We can consider that the pubsub_item table have been created by the main -%% mod_pubsub module.</p> -%% <p>PubSub plugins can store the items where they wants (for example in a -%% relational database), or they can even decide not to persist any items.</p> -%% <p>If a PubSub plugin wants to delegate the item storage to the default node, -%% they can implement this function like this: -%% ```get_items(Nidx, From) -> -%% node_default:get_items(Nidx, From).'''</p> -get_items(Nidx, _From) -> - case catch - ejabberd_odbc:sql_query_t([<<"select itemid, publisher, creation, modification, payload " - "from pubsub_item where nodeid='">>, Nidx, - <<"' order by modification desc;">>]) - of - {selected, - [<<"itemid">>, <<"publisher">>, <<"creation">>, <<"modification">>, <<"payload">>], RItems} -> - {result, [raw_to_item(Nidx, RItem) || RItem <- RItems]}; - _ -> - {result, []} - end. - -get_items(Nidx, From, none) -> - MaxItems = case catch - ejabberd_odbc:sql_query_t([<<"select val from pubsub_node_option " - "where nodeid='">>, Nidx, <<"' and name='max_items';">>]) - of - {selected, [<<"val">>], [[Value]]} -> - Tokens = element(2, erl_scan:string(binary_to_list(<<Value/binary, ".">>))), - element(2, erl_parse:parse_term(Tokens)); - _ -> - ?MAXITEMS - end, - get_items(Nidx, From, #rsm_in{max = MaxItems}); -get_items(Nidx, _From, - #rsm_in{max = M, direction = Direction, id = I, index = IncIndex}) -> - Max = ejabberd_odbc:escape(jlib:i2l(M)), - {Way, Order} = case Direction of - % aft -> {<<"<">>, <<"desc">>}; - % before when I == <<>> -> {<<"is not">>, <<"asc">>}; - % before -> {<<">">>, <<"asc">>}; - % _ when IncIndex =/= undefined -> - % {<<"<">>, <<"desc">>}; % using index - _ -> - {<<"is not">>, <<"desc">>}% Can be better - end, - [AttrName, Id] = case I of - undefined when IncIndex =/= undefined -> - case catch - ejabberd_odbc:sql_query_t([<<"select modification from pubsub_item pi " - "where exists ( select count(*) as count1 " - "from pubsub_item where nodeid='">>, Nidx, - <<"' and modification > pi.modification having count1 = ">>, - ejabberd_odbc:escape(jlib:i2l(IncIndex)), <<" );">>]) - of - {selected, [_], [[O]]} -> - [<<"modification">>, <<"'", O/binary, "'">>]; - _ -> - [<<"modification">>, <<"null">>] - end; - undefined -> - [<<"modification">>, <<"null">>]; - <<>> -> - [<<"modification">>, <<"null">>]; - I -> - [A, B] = str:tokens(ejabberd_odbc:escape(jlib:i2l(I)), <<"@">>), - [A, <<"'", B/binary, "'">>] - end, - Count = case catch - ejabberd_odbc:sql_query_t([<<"select count(*) from pubsub_item where nodeid='">>, Nidx, <<"';">>]) - of - {selected, [_], [[C]]} -> C; - _ -> <<"0">> - end, - case catch - ejabberd_odbc:sql_query_t([<<"select itemid, publisher, creation, modification, payload " - "from pubsub_item where nodeid='">>, Nidx, - <<"' and ">>, AttrName, <<" ">>, Way, <<" ">>, Id, <<" order by ">>, - AttrName, <<" ">>, Order, <<" limit ">>, jlib:i2l(Max), <<" ;">>]) - of - {selected, - [<<"itemid">>, <<"publisher">>, <<"creation">>, <<"modification">>, <<"payload">>], RItems} -> - case RItems of - [[_, _, _, F, _]|_] -> - Index = case catch - ejabberd_odbc:sql_query_t([<<"select count(*) from pubsub_item " - "where nodeid='">>, Nidx, <<"' and ">>, - AttrName, <<" > '">>, F, <<"';">>]) - of - %{selected, [_], [{C}, {In}]} -> [string:strip(C, both, $"), string:strip(In, both, $")]; - {selected, [_], [[In]]} -> In; - _ -> <<"0">> - end, - [_, _, _, L, _] = lists:last(RItems), - RsmOut = #rsm_out{count = Count, index = Index, - first = <<"modification@", F/binary>>, - last = <<"modification@", (jlib:i2l(L))/binary>>}, - {result, {[raw_to_item(Nidx, RItem) || RItem <- RItems], RsmOut}}; - [] -> - {result, {[], #rsm_out{count = Count}}} - end; - _ -> - {result, {[], none}} - end. + node_flat_odbc:set_state(State). -get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> - get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, none). +get_items(Nidx, From, RSM) -> + node_flat_odbc:get_items(Nidx, From, RSM). -get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId, RSM) -> - SubKey = jlib:jid_tolower(JID), - GenKey = jlib:jid_remove_resource(SubKey), - {Affiliation, Subscriptions} = select_affiliation_subscriptions(Nidx, GenKey, SubKey), - Whitelisted = can_fetch_item(Affiliation, Subscriptions), - if %%SubId == "", ?? -> - %% Entity has multiple subscriptions to the node but does not specify a subscription ID - %{error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; - %%InvalidSubId -> - %% Entity is subscribed but specifies an invalid subscription ID - %{error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; - (Affiliation == outcast) or (Affiliation == publish_only) -> - {error, ?ERR_FORBIDDEN}; - (AccessModel == presence) and not PresenceSubscription -> - {error, - ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), <<"presence-subscription-required">>)}; - (AccessModel == roster) and not RosterGroup -> - {error, - ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), <<"not-in-roster-group">>)}; - (AccessModel == whitelist) and not Whitelisted -> - {error, - ?ERR_EXTENDED((?ERR_NOT_ALLOWED), <<"closed-node">>)}; - (AccessModel == authorize) and not Whitelisted -> - {error, ?ERR_FORBIDDEN}; - %%MustPay -> - %% % Payment is required for a subscription - %% {error, ?ERR_PAYMENT_REQUIRED}; - true -> - get_items(Nidx, JID, RSM) - end. +get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM) -> + node_flat_odbc:get_items(Nidx, JID, AccessModel, + PresenceSubscription, RosterGroup, SubId, RSM). -get_last_items(Nidx, _From, Count) -> - case catch - ejabberd_odbc:sql_query_t([<<"select itemid, publisher, creation, modification, payload " - "from pubsub_item where nodeid='">>, Nidx, - <<"' order by modification desc limit ">>, jlib:i2l(Count), <<";">>]) - of - {selected, - [<<"itemid">>, <<"publisher">>, <<"creation">>, <<"modification">>, <<"payload">>], RItems} -> - {result, [raw_to_item(Nidx, RItem) || RItem <- RItems]}; - _ -> - {result, []} - end. - -%% @doc <p>Returns an item (one item list), given its reference.</p> get_item(Nidx, ItemId) -> - I = ejabberd_odbc:escape(ItemId), - case catch - ejabberd_odbc:sql_query_t([<<"select itemid, publisher, creation, " - "modification, payload from pubsub_item " - "where nodeid='">>, Nidx, <<"' and itemid='">>, I, <<"';">>]) - of - {selected, - [<<"itemid">>, <<"publisher">>, <<"creation">>, <<"modification">>, <<"payload">>], [RItem]} -> - {result, raw_to_item(Nidx, RItem)}; - _ -> - {error, ?ERR_ITEM_NOT_FOUND} - end. + node_flat_odbc:get_item(Nidx, ItemId). -get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId) -> - SubKey = jlib:jid_tolower(JID), - GenKey = jlib:jid_remove_resource(SubKey), - {Affiliation, Subscriptions} = select_affiliation_subscriptions(Nidx, GenKey, SubKey), - Whitelisted = can_fetch_item(Affiliation, Subscriptions), - if %%SubId == "", ?? -> - %% Entity has multiple subscriptions to the node but does not specify a subscription ID - %{error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; - %%InvalidSubId -> - %% Entity is subscribed but specifies an invalid subscription ID - %{error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; - (Affiliation == outcast) or (Affiliation == publish_only) -> - {error, ?ERR_FORBIDDEN}; - (AccessModel == presence) and not PresenceSubscription -> - {error, - ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), <<"presence-subscription-required">>)}; - (AccessModel == roster) and not RosterGroup -> - {error, - ?ERR_EXTENDED((?ERR_NOT_AUTHORIZED), <<"not-in-roster-group">>)}; - (AccessModel == whitelist) and not Whitelisted -> - {error, - ?ERR_EXTENDED((?ERR_NOT_ALLOWED), <<"closed-node">>)}; - (AccessModel == authorize) and not Whitelisted -> - {error, ?ERR_FORBIDDEN}; - %%MustPay -> - %% % Payment is required for a subscription - %% {error, ?ERR_PAYMENT_REQUIRED}; - true -> - get_item(Nidx, ItemId) - end. +get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> + node_flat_odbc:get_item(Nidx, ItemId, JID, + AccessModel, PresenceSubscription, RosterGroup, SubId). -%% @doc <p>Write an item into database.</p> set_item(Item) -> - {ItemId, Nidx} = Item#pubsub_item.itemid, - I = ejabberd_odbc:escape(ItemId), - {C, _} = Item#pubsub_item.creation, - {M, JID} = Item#pubsub_item.modification, - P = encode_jid(JID), - Payload = Item#pubsub_item.payload, - XML = ejabberd_odbc:escape(str:join([xml:element_to_binary(X) || X<-Payload], <<>>)), - S = fun ({T1, T2, T3}) -> - str:join([jlib:i2l(T1, 6), jlib:i2l(T2, 6), jlib:i2l(T3, 6)], <<":">>) - end, - case catch - ejabberd_odbc:sql_query_t([<<"update pubsub_item set publisher='">>, P, - <<"', modification='">>, S(M), - <<"', payload='">>, XML, - <<"' where nodeid='">>, Nidx, <<"' and itemid='">>, I, <<"';">>]) - of - {updated, 1} -> - ok; - _ -> - catch - ejabberd_odbc:sql_query_t([<<"insert into pubsub_item (nodeid, itemid, " - "publisher, creation, modification, payload) " - "values('">>, Nidx, <<"', '">>, I, <<"', '">>, P, - <<"', '">>, S(C), <<"', '">>, S(M), - <<"', '">>, XML, <<"');">>]) - end, - ok. + node_flat_odbc:set_item(Item). -%% @doc <p>Delete an item from database.</p> -del_item(Nidx, ItemId) -> - I = ejabberd_odbc:escape(ItemId), - catch ejabberd_odbc:sql_query_t([<<"delete from pubsub_item where itemid='">>, - I, <<"' and nodeid='">>, Nidx, <<"';">>]). +get_item_name(Host, Node, Id) -> + node_flat_odbc:get_item_name(Host, Node, Id). -del_items(_, []) -> - ok; -del_items(Nidx, [ItemId]) -> - del_item(Nidx, ItemId); -del_items(Nidx, ItemIds) -> - I = str:join([[<<"'">>, ejabberd_odbc:escape(X), <<"'">>] || X <- ItemIds], <<",">>), - catch - ejabberd_odbc:sql_query_t([<<"delete from pubsub_item where itemid in (">>, - I, <<") and nodeid='">>, Nidx, <<"';">>]). - -get_item_name(_Host, _Node, Id) -> - Id. +get_last_items(Nidx, From, Count) -> + node_flat_odbc:get_last_items(Nidx, From, Count). node_to_path(Node) -> node_hometree:node_to_path(Node). @@ -1004,194 +157,3 @@ node_to_path(Node) -> path_to_node(Path) -> node_hometree:path_to_node(Path). -can_fetch_item(owner, _) -> true; -can_fetch_item(member, _) -> true; -can_fetch_item(publisher, _) -> true; -can_fetch_item(publish_only, _) -> false; -can_fetch_item(outcast, _) -> false; -can_fetch_item(none, Subscriptions) -> is_subscribed(Subscriptions). -%can_fetch_item(_Affiliation, _Subscription) -> false. - -is_subscribed(Subscriptions) -> - lists:any(fun - ({subscribed, _SubId}) -> true; - (_) -> false - end, - Subscriptions). - -first_in_list(_Pred, []) -> - false; -first_in_list(Pred, [H | T]) -> - case Pred(H) of - true -> {value, H}; - _ -> first_in_list(Pred, T) - end. - -itemids(Nidx, {U, S, R}) -> - itemids(Nidx, encode_jid({U, S, R})); -itemids(Nidx, SJID) -> - case catch - ejabberd_odbc:sql_query_t([<<"select itemid from pubsub_item where " - "nodeid='">>, Nidx, <<"' and publisher like '">>, SJID, - <<"%' order by modification desc;">>]) - of - {selected, [<<"itemid">>], RItems} -> - [ItemId || [ItemId] <- RItems]; - _ -> - [] - end. - -select_affiliation_subscriptions(Nidx, JID) -> - J = encode_jid(JID), - case catch - ejabberd_odbc:sql_query_t([<<"select affiliation,subscriptions from " - "pubsub_state where nodeid='">>, - Nidx, <<"' and jid='">>, J, <<"';">>]) - of - {selected, [<<"affiliation">>, <<"subscriptions">>], [[A, S]]} -> - {decode_affiliation(A), decode_subscriptions(S)}; - _ -> - {none, []} - end. - -select_affiliation_subscriptions(Nidx, JID, JID) -> - select_affiliation_subscriptions(Nidx, JID); -select_affiliation_subscriptions(Nidx, GenKey, SubKey) -> - {result, Affiliation} = get_affiliation(Nidx, GenKey), - {result, Subscriptions} = get_subscriptions(Nidx, SubKey), - {Affiliation, Subscriptions}. - -update_affiliation(Nidx, JID, Affiliation) -> - J = encode_jid(JID), - A = encode_affiliation(Affiliation), - case catch - ejabberd_odbc:sql_query_t([<<"update pubsub_state set affiliation='">>, - A, <<"' where nodeid='">>, Nidx, - <<"' and jid='">>, J, <<"';">>]) - of - {updated, 1} -> - ok; - _ -> - catch - ejabberd_odbc:sql_query_t([<<"insert into pubsub_state(nodeid, jid, affiliation, subscriptions) " - "values('">>, Nidx, <<"', '">>, J, <<"', '">>, A, <<"', '');">>]) - end. - -update_subscription(Nidx, JID, Subscription) -> - J = encode_jid(JID), - S = encode_subscriptions(Subscription), - case catch - ejabberd_odbc:sql_query_t([<<"update pubsub_state set subscriptions='">>, S, - <<"' where nodeid='">>, Nidx, <<"' and jid='">>, J, <<"';">>]) - of - {updated, 1} -> - ok; - _ -> - catch - ejabberd_odbc:sql_query_t([<<"insert into pubsub_state(nodeid, jid, affiliation, subscriptions) " - "values('">>, Nidx, <<"', '">>, J, <<"', 'n', '">>, S, <<"');">>]) - end. - --spec(decode_jid/1 :: - ( SJID :: binary()) - -> ljid() - ). -decode_jid(SJID) -> - jlib:jid_tolower(jlib:string_to_jid(SJID)). - --spec(decode_affiliation/1 :: - ( Arg :: binary()) - -> atom() - ). -decode_affiliation(<<"o">>) -> owner; -decode_affiliation(<<"p">>) -> publisher; -decode_affiliation(<<"m">>) -> member; -decode_affiliation(<<"c">>) -> outcast; -decode_affiliation(_) -> none. - --spec(decode_subscription/1 :: - ( Arg :: binary()) - -> atom() - ). -decode_subscription(<<"s">>) -> subscribed; -decode_subscription(<<"p">>) -> pending; -decode_subscription(<<"u">>) -> unconfigured; -decode_subscription(_) -> none. - --spec(decode_subscriptions/1 :: - ( Subscriptions :: binary()) - -> [] | [{atom(), binary()},...] - ). -decode_subscriptions(Subscriptions) -> - lists:foldl(fun (Subscription, Acc) -> - case str:tokens(Subscription, <<":">>) of - [S, SubId] -> [{decode_subscription(S), SubId} | Acc]; - _ -> Acc - end - end, - [], str:tokens(Subscriptions, <<",">>)). - --spec(encode_jid/1 :: - ( JID :: ljid()) - -> binary() - ). -encode_jid(JID) -> - ejabberd_odbc:escape(jlib:jid_to_string(JID)). - --spec(encode_host/1 :: - ( Host :: host()) - -> binary() - ). -encode_host({_U, _S, _R} = LJID) -> encode_jid(LJID); -encode_host(Host) -> ejabberd_odbc:escape(Host). - --spec(encode_affiliation/1 :: - ( Arg :: atom()) - -> binary() - ). -encode_affiliation(owner) -> <<"o">>; -encode_affiliation(publisher) -> <<"p">>; -encode_affiliation(member) -> <<"m">>; -encode_affiliation(outcast) -> <<"c">>; -encode_affiliation(_) -> <<"n">>. - --spec(encode_subscription/1 :: - ( Arg :: atom()) - -> binary() - ). -encode_subscription(subscribed) -> <<"s">>; -encode_subscription(pending) -> <<"p">>; -encode_subscription(unconfigured) -> <<"u">>; -encode_subscription(_) -> <<"n">>. - --spec(encode_subscriptions/1 :: - ( Subscriptions :: [] | [{atom(), binary()},...]) - -> binary() - ). -encode_subscriptions(Subscriptions) -> - str:join([<<(encode_subscription(S))/binary, ":", SubId/binary>> - || {S, SubId} <- Subscriptions], <<",">>). - -%%% record getter/setter - -state_to_raw(Nidx, State) -> - {JID, _} = State#pubsub_state.stateid, - J = encode_jid(JID), - A = encode_affiliation(State#pubsub_state.affiliation), - S = encode_subscriptions(State#pubsub_state.subscriptions), - [<<"'">>, Nidx, <<"', '">>, J, <<"', '">>, A, <<"', '">>, S, <<"'">>]. - -raw_to_item(Nidx, [ItemId, SJID, Creation, Modification, XML]) -> - JID = decode_jid(SJID), - ToTime = fun (Str) -> - [T1, T2, T3] = str:tokens(Str, <<":">>), - {jlib:l2i(T1), jlib:l2i(T2), jlib:l2i(T3)} - end, - Payload = case xml_stream:parse_element(XML) of - {error, _Reason} -> []; - El -> [El] - end, - #pubsub_item{itemid = {ItemId, Nidx}, - creation = {ToTime(Creation), JID}, - modification = {ToTime(Modification), JID}, - payload = Payload}. diff --git a/src/node_mb.erl b/src/node_mb.erl index e48fd796..1213805c 100644 --- a/src/node_mb.erl +++ b/src/node_mb.erl @@ -1,43 +1,46 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. +%%%---------------------------------------------------------------------- +%%% File : node_mb.erl +%%% Author : Eric Cestari <ecestari@process-one.net> +%%% Purpose : PEP microglobing experimentation +%%% Created : 25 Sep 2008 by Eric Cestari <ecestari@process-one.net> %%% %%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. +%%% ejabberd, Copyright (C) 2002-2016 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. %%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. +%%% 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. %%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Eric Cestari <eric@ohmforce.com> -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== +%%% 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(node_mb). -behaviour(gen_pubsub_node). --author('eric@ohmforce.com'). +-author('ecestari@process-one.net'). -include("pubsub.hrl"). -include("jlib.hrl"). %%% @doc The module <strong>{@module}</strong> is the pep microblog PubSub plugin. -%%% <p> To be used, mod_pubsub must be configured : -%%% {mod_pubsub, [ % requires mod_caps -%%% {access_createnode, pubsub_createnode}, -%%% {plugins, ["default", "pep","mb"]}, -%%% {pep_mapping, [{"urn:xmpp:microblog", "mb"}]} -%%% ]}, -%%% </p> +%%% <p>To be used, mod_pubsub must be configured:<pre> +%%% mod_pubsub: +%%% access_createnode: pubsub_createnode +%%% ignore_pep_from_offline: false +%%% plugins: +%%% - "flat" +%%% - "pep" # Requires mod_caps. +%%% pep_mapping: +%%% "urn:xmpp:microblog:0": "mb" +%%% </pre></p> %%% <p>PubSub plugin nodes are using the {@link gen_pubsub_node} behaviour.</p> -export([init/3, terminate/2, options/0, features/0, diff --git a/src/node_mix.erl b/src/node_mix.erl new file mode 100644 index 00000000..b0410a8c --- /dev/null +++ b/src/node_mix.erl @@ -0,0 +1,167 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 8 Mar 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(node_mix). + +-behaviour(gen_pubsub_node). + +%% API +-export([init/3, terminate/2, options/0, features/0, + create_node_permission/6, create_node/2, delete_node/1, + purge_node/2, subscribe_node/8, unsubscribe_node/4, + publish_item/6, delete_item/4, remove_extra_items/3, + get_entity_affiliations/2, get_node_affiliations/1, + get_affiliation/2, set_affiliation/3, + get_entity_subscriptions/2, get_node_subscriptions/1, + get_subscriptions/2, set_subscriptions/4, + get_pending_nodes/2, get_states/1, get_state/2, + set_state/1, get_items/7, get_items/3, get_item/7, + get_item/2, set_item/1, get_item_name/3, node_to_path/1, + path_to_node/1]). + +-include("pubsub.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(Host, ServerHost, Opts) -> + node_flat:init(Host, ServerHost, Opts). + +terminate(Host, ServerHost) -> + node_flat:terminate(Host, ServerHost). + +options() -> + [{deliver_payloads, true}, + {notify_config, false}, + {notify_delete, false}, + {notify_retract, true}, + {purge_offline, false}, + {persist_items, true}, + {max_items, ?MAXITEMS}, + {subscribe, true}, + {access_model, open}, + {roster_groups_allowed, []}, + {publish_model, open}, + {notification_type, headline}, + {max_payload_size, ?MAX_PAYLOAD_SIZE}, + {send_last_published_item, never}, + {deliver_notifications, true}, + {broadcast_all_resources, true}, + {presence_based_delivery, false}]. + +features() -> + [<<"create-nodes">>, + <<"delete-nodes">>, + <<"delete-items">>, + <<"instant-nodes">>, + <<"item-ids">>, + <<"outcast-affiliation">>, + <<"persistent-items">>, + <<"publish">>, + <<"purge-nodes">>, + <<"retract-items">>, + <<"retrieve-affiliations">>, + <<"retrieve-items">>, + <<"retrieve-subscriptions">>, + <<"subscribe">>, + <<"subscription-notifications">>]. + +create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) -> + node_flat:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). + +create_node(Nidx, Owner) -> + node_flat:create_node(Nidx, Owner). + +delete_node(Removed) -> + node_flat:delete_node(Removed). + +subscribe_node(Nidx, Sender, Subscriber, AccessModel, + SendLast, PresenceSubscription, RosterGroup, Options) -> + node_flat:subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, + PresenceSubscription, RosterGroup, Options). + +unsubscribe_node(Nidx, Sender, Subscriber, SubId) -> + node_flat:unsubscribe_node(Nidx, Sender, Subscriber, SubId). + +publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload) -> + node_flat:publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload). + +remove_extra_items(Nidx, MaxItems, ItemIds) -> + node_flat:remove_extra_items(Nidx, MaxItems, ItemIds). + +delete_item(Nidx, Publisher, PublishModel, ItemId) -> + node_flat:delete_item(Nidx, Publisher, PublishModel, ItemId). + +purge_node(Nidx, Owner) -> + node_flat:purge_node(Nidx, Owner). + +get_entity_affiliations(Host, Owner) -> + node_flat:get_entity_affiliations(Host, Owner). + +get_node_affiliations(Nidx) -> + node_flat:get_node_affiliations(Nidx). + +get_affiliation(Nidx, Owner) -> + node_flat:get_affiliation(Nidx, Owner). + +set_affiliation(Nidx, Owner, Affiliation) -> + node_flat:set_affiliation(Nidx, Owner, Affiliation). + +get_entity_subscriptions(Host, Owner) -> + node_flat:get_entity_subscriptions(Host, Owner). + +get_node_subscriptions(Nidx) -> + node_flat:get_node_subscriptions(Nidx). + +get_subscriptions(Nidx, Owner) -> + node_flat:get_subscriptions(Nidx, Owner). + +set_subscriptions(Nidx, Owner, Subscription, SubId) -> + node_flat:set_subscriptions(Nidx, Owner, Subscription, SubId). + +get_pending_nodes(Host, Owner) -> + node_flat:get_pending_nodes(Host, Owner). + +get_states(Nidx) -> + node_flat:get_states(Nidx). + +get_state(Nidx, JID) -> + node_flat:get_state(Nidx, JID). + +set_state(State) -> + node_flat:set_state(State). + +get_items(Nidx, From, RSM) -> + node_flat:get_items(Nidx, From, RSM). + +get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM) -> + node_flat:get_items(Nidx, JID, AccessModel, + PresenceSubscription, RosterGroup, SubId, RSM). + +get_item(Nidx, ItemId) -> + node_flat:get_item(Nidx, ItemId). + +get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> + node_flat:get_item(Nidx, ItemId, JID, AccessModel, + PresenceSubscription, RosterGroup, SubId). + +set_item(Item) -> + node_flat:set_item(Item). + +get_item_name(Host, Node, Id) -> + node_flat:get_item_name(Host, Node, Id). + +node_to_path(Node) -> + node_flat:node_to_path(Node). + +path_to_node(Path) -> + node_flat:path_to_node(Path). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== diff --git a/src/node_mix_odbc.erl b/src/node_mix_odbc.erl new file mode 100644 index 00000000..e7cc6883 --- /dev/null +++ b/src/node_mix_odbc.erl @@ -0,0 +1,170 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 8 Mar 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(node_mix_odbc). + +-behaviour(gen_pubsub_node). + +%% API +-export([init/3, terminate/2, options/0, features/0, + create_node_permission/6, create_node/2, delete_node/1, + purge_node/2, subscribe_node/8, unsubscribe_node/4, + publish_item/6, delete_item/4, remove_extra_items/3, + get_entity_affiliations/2, get_node_affiliations/1, + get_affiliation/2, set_affiliation/3, + get_entity_subscriptions/2, get_node_subscriptions/1, + get_subscriptions/2, set_subscriptions/4, + get_pending_nodes/2, get_states/1, get_state/2, + set_state/1, get_items/7, get_items/3, get_item/7, + get_item/2, set_item/1, get_item_name/3, node_to_path/1, + path_to_node/1, get_entity_subscriptions_for_send_last/2]). + +-include("pubsub.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(Host, ServerHost, Opts) -> + node_flat_odbc:init(Host, ServerHost, Opts). + +terminate(Host, ServerHost) -> + node_flat_odbc:terminate(Host, ServerHost). + +options() -> + [{deliver_payloads, true}, + {notify_config, false}, + {notify_delete, false}, + {notify_retract, true}, + {purge_offline, false}, + {persist_items, true}, + {max_items, ?MAXITEMS}, + {subscribe, true}, + {access_model, open}, + {roster_groups_allowed, []}, + {publish_model, open}, + {notification_type, headline}, + {max_payload_size, ?MAX_PAYLOAD_SIZE}, + {send_last_published_item, never}, + {deliver_notifications, true}, + {broadcast_all_resources, true}, + {presence_based_delivery, false}]. + +features() -> + [<<"create-nodes">>, + <<"delete-nodes">>, + <<"delete-items">>, + <<"instant-nodes">>, + <<"item-ids">>, + <<"outcast-affiliation">>, + <<"persistent-items">>, + <<"publish">>, + <<"purge-nodes">>, + <<"retract-items">>, + <<"retrieve-affiliations">>, + <<"retrieve-items">>, + <<"retrieve-subscriptions">>, + <<"subscribe">>, + <<"subscription-notifications">>]. + +create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) -> + node_flat_odbc:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). + +create_node(Nidx, Owner) -> + node_flat_odbc:create_node(Nidx, Owner). + +delete_node(Removed) -> + node_flat_odbc:delete_node(Removed). + +subscribe_node(Nidx, Sender, Subscriber, AccessModel, + SendLast, PresenceSubscription, RosterGroup, Options) -> + node_flat_odbc:subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, + PresenceSubscription, RosterGroup, Options). + +unsubscribe_node(Nidx, Sender, Subscriber, SubId) -> + node_flat_odbc:unsubscribe_node(Nidx, Sender, Subscriber, SubId). + +publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload) -> + node_flat_odbc:publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload). + +remove_extra_items(Nidx, MaxItems, ItemIds) -> + node_flat_odbc:remove_extra_items(Nidx, MaxItems, ItemIds). + +delete_item(Nidx, Publisher, PublishModel, ItemId) -> + node_flat_odbc:delete_item(Nidx, Publisher, PublishModel, ItemId). + +purge_node(Nidx, Owner) -> + node_flat_odbc:purge_node(Nidx, Owner). + +get_entity_affiliations(Host, Owner) -> + node_flat_odbc:get_entity_affiliations(Host, Owner). + +get_node_affiliations(Nidx) -> + node_flat_odbc:get_node_affiliations(Nidx). + +get_affiliation(Nidx, Owner) -> + node_flat_odbc:get_affiliation(Nidx, Owner). + +set_affiliation(Nidx, Owner, Affiliation) -> + node_flat_odbc:set_affiliation(Nidx, Owner, Affiliation). + +get_entity_subscriptions(Host, Owner) -> + node_flat_odbc:get_entity_subscriptions(Host, Owner). + +get_node_subscriptions(Nidx) -> + node_flat_odbc:get_node_subscriptions(Nidx). + +get_subscriptions(Nidx, Owner) -> + node_flat_odbc:get_subscriptions(Nidx, Owner). + +set_subscriptions(Nidx, Owner, Subscription, SubId) -> + node_flat_odbc:set_subscriptions(Nidx, Owner, Subscription, SubId). + +get_pending_nodes(Host, Owner) -> + node_flat_odbc:get_pending_nodes(Host, Owner). + +get_states(Nidx) -> + node_flat_odbc:get_states(Nidx). + +get_state(Nidx, JID) -> + node_flat_odbc:get_state(Nidx, JID). + +set_state(State) -> + node_flat_odbc:set_state(State). + +get_items(Nidx, From, RSM) -> + node_flat_odbc:get_items(Nidx, From, RSM). + +get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM) -> + node_flat_odbc:get_items(Nidx, JID, AccessModel, + PresenceSubscription, RosterGroup, SubId, RSM). + +get_item(Nidx, ItemId) -> + node_flat_odbc:get_item(Nidx, ItemId). + +get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> + node_flat_odbc:get_item(Nidx, ItemId, JID, AccessModel, + PresenceSubscription, RosterGroup, SubId). + +set_item(Item) -> + node_flat_odbc:set_item(Item). + +get_item_name(Host, Node, Id) -> + node_flat_odbc:get_item_name(Host, Node, Id). + +node_to_path(Node) -> + node_flat_odbc:node_to_path(Node). + +path_to_node(Path) -> + node_flat_odbc:path_to_node(Path). + +get_entity_subscriptions_for_send_last(Host, Owner) -> + node_flat_odbc:get_entity_subscriptions_for_send_last(Host, Owner). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== diff --git a/src/node_online.erl b/src/node_online.erl new file mode 100644 index 00000000..1e9e2953 --- /dev/null +++ b/src/node_online.erl @@ -0,0 +1,173 @@ +%%%---------------------------------------------------------------------- +%%% File : node_online.erl +%%% Author : Christophe Romain <christophe.romain@process-one.net> +%%% Purpose : Handle only online users, remove offline subscriptions and nodes +%%% Created : 15 Dec 2015 by Christophe Romain <christophe.romain@process-one.net> +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2016 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(node_online). +-behaviour(gen_pubsub_node). +-author('christophe.romain@process-one.net'). + +-include("pubsub.hrl"). +-include("jlib.hrl"). + +-export([init/3, terminate/2, options/0, features/0, + create_node_permission/6, create_node/2, delete_node/1, + purge_node/2, subscribe_node/8, unsubscribe_node/4, + publish_item/6, delete_item/4, remove_extra_items/3, + get_entity_affiliations/2, get_node_affiliations/1, + get_affiliation/2, set_affiliation/3, + get_entity_subscriptions/2, get_node_subscriptions/1, + get_subscriptions/2, set_subscriptions/4, + get_pending_nodes/2, get_states/1, get_state/2, + set_state/1, get_items/7, get_items/3, get_item/7, + get_item/2, set_item/1, get_item_name/3, node_to_path/1, + path_to_node/1]). + +-export([user_offline/3]). + +init(Host, ServerHost, Opts) -> + node_flat:init(Host, ServerHost, Opts), + ejabberd_hooks:add(sm_remove_connection_hook, ServerHost, + ?MODULE, user_offline, 75), + ok. + +terminate(Host, ServerHost) -> + node_flat:terminate(Host, ServerHost), + ejabberd_hooks:delete(sm_remove_connection_hook, ServerHost, + ?MODULE, user_offline, 75), + ok. + +user_offline(_SID, #jid{luser=LUser,lserver=LServer}, _Info) -> + mod_pubsub:remove_user(LUser, LServer). + +options() -> + [{deliver_payloads, true}, + {notify_config, false}, + {notify_delete, false}, + {notify_retract, false}, + {purge_offline, true}, + {persist_items, true}, + {max_items, ?MAXITEMS}, + {subscribe, true}, + {access_model, open}, + {roster_groups_allowed, []}, + {publish_model, publishers}, + {notification_type, headline}, + {max_payload_size, ?MAX_PAYLOAD_SIZE}, + {send_last_published_item, on_sub_and_presence}, + {deliver_notifications, true}, + {presence_based_delivery, true}]. + +features() -> + node_flat:features(). + +create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) -> + node_flat:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). + +create_node(Nidx, Owner) -> + node_flat:create_node(Nidx, Owner). + +delete_node(Removed) -> + node_flat:delete_node(Removed). + +subscribe_node(Nidx, Sender, Subscriber, AccessModel, + SendLast, PresenceSubscription, RosterGroup, Options) -> + node_flat:subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, + PresenceSubscription, RosterGroup, Options). + +unsubscribe_node(Nidx, Sender, Subscriber, SubId) -> + node_flat:unsubscribe_node(Nidx, Sender, Subscriber, SubId). + +publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload) -> + node_flat:publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload). + +remove_extra_items(Nidx, MaxItems, ItemIds) -> + node_flat:remove_extra_items(Nidx, MaxItems, ItemIds). + +delete_item(Nidx, Publisher, PublishModel, ItemId) -> + node_flat:delete_item(Nidx, Publisher, PublishModel, ItemId). + +purge_node(Nidx, Owner) -> + node_flat:purge_node(Nidx, Owner). + +get_entity_affiliations(Host, Owner) -> + node_flat:get_entity_affiliations(Host, Owner). + +get_node_affiliations(Nidx) -> + node_flat:get_node_affiliations(Nidx). + +get_affiliation(Nidx, Owner) -> + node_flat:get_affiliation(Nidx, Owner). + +set_affiliation(Nidx, Owner, Affiliation) -> + node_flat:set_affiliation(Nidx, Owner, Affiliation). + +get_entity_subscriptions(Host, Owner) -> + node_flat:get_entity_subscriptions(Host, Owner). + +get_node_subscriptions(Nidx) -> + node_flat:get_node_subscriptions(Nidx). + +get_subscriptions(Nidx, Owner) -> + node_flat:get_subscriptions(Nidx, Owner). + +set_subscriptions(Nidx, Owner, Subscription, SubId) -> + node_flat:set_subscriptions(Nidx, Owner, Subscription, SubId). + +get_pending_nodes(Host, Owner) -> + node_flat:get_pending_nodes(Host, Owner). + +get_states(Nidx) -> + node_flat:get_states(Nidx). + +get_state(Nidx, JID) -> + node_flat:get_state(Nidx, JID). + +set_state(State) -> + node_flat:set_state(State). + +get_items(Nidx, From, RSM) -> + node_flat:get_items(Nidx, From, RSM). + +get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM) -> + node_flat:get_items(Nidx, JID, AccessModel, + PresenceSubscription, RosterGroup, SubId, RSM). + +get_item(Nidx, ItemId) -> + node_flat:get_item(Nidx, ItemId). + +get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> + node_flat:get_item(Nidx, ItemId, JID, AccessModel, + PresenceSubscription, RosterGroup, SubId). + +set_item(Item) -> + node_flat:set_item(Item). + +get_item_name(Host, Node, Id) -> + node_flat:get_item_name(Host, Node, Id). + +node_to_path(Node) -> + node_flat:node_to_path(Node). + +path_to_node(Path) -> + node_flat:path_to_node(Path). diff --git a/src/node_pep.erl b/src/node_pep.erl index 8eeb28f0..da032539 100644 --- a/src/node_pep.erl +++ b/src/node_pep.erl @@ -1,28 +1,30 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. +%%%---------------------------------------------------------------------- +%%% File : node_pep.erl +%%% Author : Christophe Romain <christophe.romain@process-one.net> +%%% Purpose : Standard PubSub PEP plugin +%%% Created : 1 Dec 2007 by Christophe Romain <christophe.romain@process-one.net> %%% %%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. +%%% ejabberd, Copyright (C) 2002-2016 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. %%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. +%%% 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. %%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Christophe Romain <christophe.romain@process-one.net> -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== +%%% 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. +%%% +%%%---------------------------------------------------------------------- + +%%% @doc The module <strong>{@module}</strong> is the pep PubSub plugin. +%%% <p>PubSub plugin nodes are using the {@link gen_pubsub_node} behaviour.</p> -module(node_pep). -behaviour(gen_pubsub_node). @@ -32,9 +34,6 @@ -include("jlib.hrl"). -include("logger.hrl"). -%%% @doc The module <strong>{@module}</strong> is the pep PubSub plugin. -%%% <p>PubSub plugin nodes are using the {@link gen_pubsub_node} behaviour.</p> - -export([init/3, terminate/2, options/0, features/0, create_node_permission/6, create_node/2, delete_node/1, purge_node/2, subscribe_node/8, unsubscribe_node/4, @@ -49,12 +48,13 @@ path_to_node/1]). init(Host, ServerHost, Opts) -> - node_hometree:init(Host, ServerHost, Opts), + node_flat:init(Host, ServerHost, Opts), complain_if_modcaps_disabled(ServerHost), ok. terminate(Host, ServerHost) -> - node_hometree:terminate(Host, ServerHost), ok. + node_flat:terminate(Host, ServerHost), + ok. options() -> [{deliver_payloads, true}, @@ -62,7 +62,7 @@ options() -> {notify_delete, false}, {notify_retract, false}, {purge_offline, false}, - {persist_items, false}, + {persist_items, true}, {max_items, 1}, {subscribe, true}, {access_model, presence}, @@ -93,7 +93,7 @@ features() -> <<"subscribe">>]. create_node_permission(Host, ServerHost, _Node, _ParentNode, Owner, Access) -> - LOwner = jlib:jid_tolower(Owner), + LOwner = jid:tolower(Owner), {User, Server, _Resource} = LOwner, Allowed = case LOwner of {<<"">>, Host, <<"">>} -> @@ -112,39 +112,39 @@ create_node_permission(Host, ServerHost, _Node, _ParentNode, Owner, Access) -> {result, Allowed}. create_node(Nidx, Owner) -> - node_hometree:create_node(Nidx, Owner). + node_flat:create_node(Nidx, Owner). delete_node(Nodes) -> - {result, {_, _, Result}} = node_hometree:delete_node(Nodes), + {result, {_, _, Result}} = node_flat:delete_node(Nodes), {result, {[], Result}}. subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options) -> - node_hometree:subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, + node_flat:subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options). unsubscribe_node(Nidx, Sender, Subscriber, SubId) -> - case node_hometree:unsubscribe_node(Nidx, Sender, Subscriber, SubId) of + case node_flat:unsubscribe_node(Nidx, Sender, Subscriber, SubId) of {error, Error} -> {error, Error}; {result, _} -> {result, []} end. publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload) -> - node_hometree:publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload). + node_flat:publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload). remove_extra_items(Nidx, MaxItems, ItemIds) -> - node_hometree:remove_extra_items(Nidx, MaxItems, ItemIds). + node_flat:remove_extra_items(Nidx, MaxItems, ItemIds). delete_item(Nidx, Publisher, PublishModel, ItemId) -> - node_hometree:delete_item(Nidx, Publisher, PublishModel, ItemId). + node_flat:delete_item(Nidx, Publisher, PublishModel, ItemId). purge_node(Nidx, Owner) -> - node_hometree:purge_node(Nidx, Owner). + node_flat:purge_node(Nidx, Owner). get_entity_affiliations(Host, Owner) -> - {_, D, _} = SubKey = jlib:jid_tolower(Owner), - SubKey = jlib:jid_tolower(Owner), - GenKey = jlib:jid_remove_resource(SubKey), + {_, D, _} = SubKey = jid:tolower(Owner), + SubKey = jid:tolower(Owner), + GenKey = jid:remove_resource(SubKey), States = mnesia:match_object(#pubsub_state{stateid = {GenKey, '_'}, _ = '_'}), NodeTree = mod_pubsub:tree(Host), Reply = lists:foldl(fun (#pubsub_state{stateid = {_, N}, affiliation = A}, Acc) -> @@ -158,17 +158,17 @@ get_entity_affiliations(Host, Owner) -> get_node_affiliations(Nidx) -> - node_hometree:get_node_affiliations(Nidx). + node_flat:get_node_affiliations(Nidx). get_affiliation(Nidx, Owner) -> - node_hometree:get_affiliation(Nidx, Owner). + node_flat:get_affiliation(Nidx, Owner). set_affiliation(Nidx, Owner, Affiliation) -> - node_hometree:set_affiliation(Nidx, Owner, Affiliation). + node_flat:set_affiliation(Nidx, Owner, Affiliation). get_entity_subscriptions(Host, Owner) -> - {U, D, _} = SubKey = jlib:jid_tolower(Owner), - GenKey = jlib:jid_remove_resource(SubKey), + {U, D, _} = SubKey = jid:tolower(Owner), + GenKey = jid:remove_resource(SubKey), States = case SubKey of GenKey -> mnesia:match_object(#pubsub_state{stateid = {{U, D, '_'}, '_'}, _ = '_'}); @@ -198,45 +198,45 @@ get_entity_subscriptions(Host, Owner) -> {result, Reply}. get_node_subscriptions(Nidx) -> - node_hometree:get_node_subscriptions(Nidx). + node_flat:get_node_subscriptions(Nidx). get_subscriptions(Nidx, Owner) -> - node_hometree:get_subscriptions(Nidx, Owner). + node_flat:get_subscriptions(Nidx, Owner). set_subscriptions(Nidx, Owner, Subscription, SubId) -> - node_hometree:set_subscriptions(Nidx, Owner, Subscription, SubId). + node_flat:set_subscriptions(Nidx, Owner, Subscription, SubId). get_pending_nodes(Host, Owner) -> - node_hometree:get_pending_nodes(Host, Owner). + node_flat:get_pending_nodes(Host, Owner). get_states(Nidx) -> - node_hometree:get_states(Nidx). + node_flat:get_states(Nidx). get_state(Nidx, JID) -> - node_hometree:get_state(Nidx, JID). + node_flat:get_state(Nidx, JID). set_state(State) -> - node_hometree:set_state(State). + node_flat:set_state(State). get_items(Nidx, From, RSM) -> - node_hometree:get_items(Nidx, From, RSM). + node_flat:get_items(Nidx, From, RSM). get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM) -> - node_hometree:get_items(Nidx, JID, AccessModel, + node_flat:get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM). get_item(Nidx, ItemId) -> - node_hometree:get_item(Nidx, ItemId). + node_flat:get_item(Nidx, ItemId). get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> - node_hometree:get_item(Nidx, ItemId, JID, AccessModel, + node_flat:get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId). set_item(Item) -> - node_hometree:set_item(Item). + node_flat:set_item(Item). get_item_name(Host, Node, Id) -> - node_hometree:get_item_name(Host, Node, Id). + node_flat:get_item_name(Host, Node, Id). node_to_path(Node) -> node_flat:node_to_path(Node). @@ -257,7 +257,7 @@ complain_if_modcaps_disabled(ServerHost) -> false -> ?WARNING_MSG("The PEP plugin is enabled in mod_pubsub " "of host ~p. This plugin requires mod_caps " - "to be enabled, but it isn't.", + "but it does not seems enabled, please check config.", [ServerHost]); true -> ok end. diff --git a/src/node_pep_odbc.erl b/src/node_pep_odbc.erl index d80e686f..6eb0de04 100644 --- a/src/node_pep_odbc.erl +++ b/src/node_pep_odbc.erl @@ -1,28 +1,30 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. +%%%---------------------------------------------------------------------- +%%% File : node_pep_odbc.erl +%%% Author : Christophe Romain <christophe.romain@process-one.net> +%%% Purpose : Standard PubSub PEP plugin with ODBC backend +%%% Created : 1 Dec 2007 by Christophe Romain <christophe.romain@process-one.net> %%% %%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. +%%% ejabberd, Copyright (C) 2002-2016 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. %%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. +%%% 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. %%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Christophe Romain <christophe.romain@process-one.net> -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== +%%% 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. +%%% +%%%---------------------------------------------------------------------- + +%%% @doc The module <strong>{@module}</strong> is the pep PubSub plugin. +%%% <p>PubSub plugin nodes are using the {@link gen_pubsub_node} behaviour.</p> -module(node_pep_odbc). -behaviour(gen_pubsub_node). @@ -32,9 +34,6 @@ -include("jlib.hrl"). -include("logger.hrl"). -%%% @doc The module <strong>{@module}</strong> is the pep PubSub plugin. -%%% <p>PubSub plugin nodes are using the {@link gen_pubsub_node} behaviour.</p> - -export([init/3, terminate/2, options/0, features/0, create_node_permission/6, create_node/2, delete_node/1, purge_node/2, subscribe_node/8, unsubscribe_node/4, @@ -50,12 +49,13 @@ get_entity_subscriptions_for_send_last/2, get_last_items/3]). init(Host, ServerHost, Opts) -> - node_hometree_odbc:init(Host, ServerHost, Opts), + node_flat_odbc:init(Host, ServerHost, Opts), complain_if_modcaps_disabled(ServerHost), ok. terminate(Host, ServerHost) -> - node_hometree_odbc:terminate(Host, ServerHost), ok. + node_flat_odbc:terminate(Host, ServerHost), + ok. options() -> [{odbc, true}, {rsm, true} | node_pep:options()]. @@ -67,56 +67,56 @@ create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) -> node_pep:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). create_node(Nidx, Owner) -> - node_hometree_odbc:create_node(Nidx, Owner), + node_flat_odbc:create_node(Nidx, Owner), {result, {default, broadcast}}. delete_node(Nodes) -> - {result, {_, _, Result}} = node_hometree_odbc:delete_node(Nodes), + {result, {_, _, Result}} = node_flat_odbc:delete_node(Nodes), {result, {[], Result}}. subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options) -> - node_hometree_odbc:subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, + node_flat_odbc:subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options). unsubscribe_node(Nidx, Sender, Subscriber, SubId) -> - case node_hometree_odbc:unsubscribe_node(Nidx, Sender, Subscriber, SubId) of + case node_flat_odbc:unsubscribe_node(Nidx, Sender, Subscriber, SubId) of {error, Error} -> {error, Error}; {result, _} -> {result, []} end. publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload) -> - node_hometree_odbc:publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload). + node_flat_odbc:publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload). remove_extra_items(Nidx, MaxItems, ItemIds) -> - node_hometree_odbc:remove_extra_items(Nidx, MaxItems, ItemIds). + node_flat_odbc:remove_extra_items(Nidx, MaxItems, ItemIds). delete_item(Nidx, Publisher, PublishModel, ItemId) -> - node_hometree_odbc:delete_item(Nidx, Publisher, PublishModel, ItemId). + node_flat_odbc:delete_item(Nidx, Publisher, PublishModel, ItemId). purge_node(Nidx, Owner) -> - node_hometree_odbc:purge_node(Nidx, Owner). + node_flat_odbc:purge_node(Nidx, Owner). get_entity_affiliations(_Host, Owner) -> - OwnerKey = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), - node_hometree_odbc:get_entity_affiliations(OwnerKey, Owner). + OwnerKey = jid:tolower(jid:remove_resource(Owner)), + node_flat_odbc:get_entity_affiliations(OwnerKey, Owner). get_node_affiliations(Nidx) -> - node_hometree_odbc:get_node_affiliations(Nidx). + node_flat_odbc:get_node_affiliations(Nidx). get_affiliation(Nidx, Owner) -> - node_hometree_odbc:get_affiliation(Nidx, Owner). + node_flat_odbc:get_affiliation(Nidx, Owner). set_affiliation(Nidx, Owner, Affiliation) -> - node_hometree_odbc:set_affiliation(Nidx, Owner, Affiliation). + node_flat_odbc:set_affiliation(Nidx, Owner, Affiliation). get_entity_subscriptions(_Host, Owner) -> - SubKey = jlib:jid_tolower(Owner), - GenKey = jlib:jid_remove_resource(SubKey), - Host = node_hometree_odbc:encode_host(element(2, SubKey)), - SJ = node_hometree_odbc:encode_jid(SubKey), - GJ = node_hometree_odbc:encode_jid(GenKey), + SubKey = jid:tolower(Owner), + GenKey = jid:remove_resource(SubKey), + Host = node_flat_odbc:encode_host(element(2, SubKey)), + SJ = node_flat_odbc:encode_jid(SubKey), + GJ = node_flat_odbc:encode_jid(GenKey), Query = case SubKey of GenKey -> [<<"select host, node, type, i.nodeid, jid, " @@ -134,11 +134,11 @@ get_entity_subscriptions(_Host, Owner) -> [<<"host">>, <<"node">>, <<"type">>, <<"nodeid">>, <<"jid">>, <<"subscriptions">>], RItems} -> lists:map(fun ([H, N, T, I, J, S]) -> - O = node_hometree_odbc:decode_jid(H), + O = node_flat_odbc:decode_jid(H), Node = nodetree_tree_odbc:raw_to_node(O, [N, <<"">>, T, I]), {Node, - node_hometree_odbc:decode_subscriptions(S), - node_hometree_odbc:decode_jid(J)} + node_flat_odbc:decode_subscriptions(S), + node_flat_odbc:decode_jid(J)} end, RItems); _ -> @@ -147,11 +147,11 @@ get_entity_subscriptions(_Host, Owner) -> {result, Reply}. get_entity_subscriptions_for_send_last(_Host, Owner) -> - SubKey = jlib:jid_tolower(Owner), - GenKey = jlib:jid_remove_resource(SubKey), - Host = node_hometree_odbc:encode_host(element(2, SubKey)), - SJ = node_hometree_odbc:encode_jid(SubKey), - GJ = node_hometree_odbc:encode_jid(GenKey), + SubKey = jid:tolower(Owner), + GenKey = jid:remove_resource(SubKey), + Host = node_flat_odbc:encode_host(element(2, SubKey)), + SJ = node_flat_odbc:encode_jid(SubKey), + GJ = node_flat_odbc:encode_jid(GenKey), Query = case SubKey of GenKey -> [<<"select host, node, type, i.nodeid, jid, " @@ -173,11 +173,11 @@ get_entity_subscriptions_for_send_last(_Host, Owner) -> [<<"host">>, <<"node">>, <<"type">>, <<"nodeid">>, <<"jid">>, <<"subscriptions">>], RItems} -> lists:map(fun ([H, N, T, I, J, S]) -> - O = node_hometree_odbc:decode_jid(H), + O = node_flat_odbc:decode_jid(H), Node = nodetree_tree_odbc:raw_to_node(O, [N, <<"">>, T, I]), {Node, - node_hometree_odbc:decode_subscriptions(S), - node_hometree_odbc:decode_jid(J)} + node_flat_odbc:decode_subscriptions(S), + node_flat_odbc:decode_jid(J)} end, RItems); _ -> @@ -186,48 +186,48 @@ get_entity_subscriptions_for_send_last(_Host, Owner) -> {result, Reply}. get_node_subscriptions(Nidx) -> - node_hometree_odbc:get_node_subscriptions(Nidx). + node_flat_odbc:get_node_subscriptions(Nidx). get_subscriptions(Nidx, Owner) -> - node_hometree_odbc:get_subscriptions(Nidx, Owner). + node_flat_odbc:get_subscriptions(Nidx, Owner). set_subscriptions(Nidx, Owner, Subscription, SubId) -> - node_hometree_odbc:set_subscriptions(Nidx, Owner, Subscription, SubId). + node_flat_odbc:set_subscriptions(Nidx, Owner, Subscription, SubId). get_pending_nodes(Host, Owner) -> - node_hometree_odbc:get_pending_nodes(Host, Owner). + node_flat_odbc:get_pending_nodes(Host, Owner). get_states(Nidx) -> - node_hometree_odbc:get_states(Nidx). + node_flat_odbc:get_states(Nidx). get_state(Nidx, JID) -> - node_hometree_odbc:get_state(Nidx, JID). + node_flat_odbc:get_state(Nidx, JID). set_state(State) -> - node_hometree_odbc:set_state(State). + node_flat_odbc:set_state(State). get_items(Nidx, From, RSM) -> - node_hometree_odbc:get_items(Nidx, From, RSM). + node_flat_odbc:get_items(Nidx, From, RSM). get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM) -> - node_hometree_odbc:get_items(Nidx, JID, AccessModel, + node_flat_odbc:get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM). get_last_items(Nidx, JID, Count) -> - node_hometree_odbc:get_last_items(Nidx, JID, Count). + node_flat_odbc:get_last_items(Nidx, JID, Count). get_item(Nidx, ItemId) -> - node_hometree_odbc:get_item(Nidx, ItemId). + node_flat_odbc:get_item(Nidx, ItemId). get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> - node_hometree_odbc:get_item(Nidx, ItemId, JID, AccessModel, + node_flat_odbc:get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId). set_item(Item) -> - node_hometree_odbc:set_item(Item). + node_flat_odbc:set_item(Item). get_item_name(Host, Node, Id) -> - node_hometree_odbc:get_item_name(Host, Node, Id). + node_flat_odbc:get_item_name(Host, Node, Id). node_to_path(Node) -> node_flat_odbc:node_to_path(Node). diff --git a/src/node_private.erl b/src/node_private.erl index eab06d56..de80d870 100644 --- a/src/node_private.erl +++ b/src/node_private.erl @@ -1,28 +1,27 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. +%%%---------------------------------------------------------------------- +%%% File : node_private.erl +%%% Author : Christophe Romain <christophe.romain@process-one.net> +%%% Purpose : +%%% Created : 1 Dec 2007 by Christophe Romain <christophe.romain@process-one.net> %%% %%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. +%%% ejabberd, Copyright (C) 2002-2016 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. %%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. +%%% 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. %%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Christophe Romain <christophe.romain@process-one.net> -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== +%%% 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(node_private). -behaviour(gen_pubsub_node). @@ -45,10 +44,10 @@ path_to_node/1]). init(Host, ServerHost, Opts) -> - node_hometree:init(Host, ServerHost, Opts). + node_flat:init(Host, ServerHost, Opts). terminate(Host, ServerHost) -> - node_hometree:terminate(Host, ServerHost). + node_flat:terminate(Host, ServerHost). options() -> [{deliver_payloads, true}, @@ -85,89 +84,89 @@ features() -> <<"subscription-notifications">>]. create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) -> - node_hometree:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). + node_flat:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). create_node(Nidx, Owner) -> - node_hometree:create_node(Nidx, Owner). + node_flat:create_node(Nidx, Owner). delete_node(Removed) -> - node_hometree:delete_node(Removed). + node_flat:delete_node(Removed). subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options) -> - node_hometree:subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, + node_flat:subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options). unsubscribe_node(Nidx, Sender, Subscriber, SubId) -> - node_hometree:unsubscribe_node(Nidx, Sender, Subscriber, SubId). + node_flat:unsubscribe_node(Nidx, Sender, Subscriber, SubId). publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload) -> - node_hometree:publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload). + node_flat:publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload). remove_extra_items(Nidx, MaxItems, ItemIds) -> - node_hometree:remove_extra_items(Nidx, MaxItems, ItemIds). + node_flat:remove_extra_items(Nidx, MaxItems, ItemIds). delete_item(Nidx, Publisher, PublishModel, ItemId) -> - node_hometree:delete_item(Nidx, Publisher, PublishModel, ItemId). + node_flat:delete_item(Nidx, Publisher, PublishModel, ItemId). purge_node(Nidx, Owner) -> - node_hometree:purge_node(Nidx, Owner). + node_flat:purge_node(Nidx, Owner). get_entity_affiliations(Host, Owner) -> - node_hometree:get_entity_affiliations(Host, Owner). + node_flat:get_entity_affiliations(Host, Owner). get_node_affiliations(Nidx) -> - node_hometree:get_node_affiliations(Nidx). + node_flat:get_node_affiliations(Nidx). get_affiliation(Nidx, Owner) -> - node_hometree:get_affiliation(Nidx, Owner). + node_flat:get_affiliation(Nidx, Owner). set_affiliation(Nidx, Owner, Affiliation) -> - node_hometree:set_affiliation(Nidx, Owner, Affiliation). + node_flat:set_affiliation(Nidx, Owner, Affiliation). get_entity_subscriptions(Host, Owner) -> - node_hometree:get_entity_subscriptions(Host, Owner). + node_flat:get_entity_subscriptions(Host, Owner). get_node_subscriptions(Nidx) -> - node_hometree:get_node_subscriptions(Nidx). + node_flat:get_node_subscriptions(Nidx). get_subscriptions(Nidx, Owner) -> - node_hometree:get_subscriptions(Nidx, Owner). + node_flat:get_subscriptions(Nidx, Owner). set_subscriptions(Nidx, Owner, Subscription, SubId) -> - node_hometree:set_subscriptions(Nidx, Owner, Subscription, SubId). + node_flat:set_subscriptions(Nidx, Owner, Subscription, SubId). get_pending_nodes(Host, Owner) -> - node_hometree:get_pending_nodes(Host, Owner). + node_flat:get_pending_nodes(Host, Owner). get_states(Nidx) -> - node_hometree:get_states(Nidx). + node_flat:get_states(Nidx). get_state(Nidx, JID) -> - node_hometree:get_state(Nidx, JID). + node_flat:get_state(Nidx, JID). set_state(State) -> - node_hometree:set_state(State). + node_flat:set_state(State). get_items(Nidx, From, RSM) -> - node_hometree:get_items(Nidx, From, RSM). + node_flat:get_items(Nidx, From, RSM). get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM) -> - node_hometree:get_items(Nidx, JID, AccessModel, + node_flat:get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM). get_item(Nidx, ItemId) -> - node_hometree:get_item(Nidx, ItemId). + node_flat:get_item(Nidx, ItemId). get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> - node_hometree:get_item(Nidx, ItemId, JID, AccessModel, + node_flat:get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId). set_item(Item) -> - node_hometree:set_item(Item). + node_flat:set_item(Item). get_item_name(Host, Node, Id) -> - node_hometree:get_item_name(Host, Node, Id). + node_flat:get_item_name(Host, Node, Id). node_to_path(Node) -> node_flat:node_to_path(Node). diff --git a/src/node_public.erl b/src/node_public.erl index efefd67f..df4f1c08 100644 --- a/src/node_public.erl +++ b/src/node_public.erl @@ -1,28 +1,27 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. +%%%---------------------------------------------------------------------- +%%% File : node_public.erl +%%% Author : Christophe Romain <christophe.romain@process-one.net> +%%% Purpose : +%%% Created : 1 Dec 2007 by Christophe Romain <christophe.romain@process-one.net> %%% %%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. +%%% ejabberd, Copyright (C) 2002-2016 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. %%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. +%%% 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. %%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Christophe Romain <christophe.romain@process-one.net> -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== +%%% 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(node_public). -behaviour(gen_pubsub_node). @@ -45,10 +44,10 @@ path_to_node/1]). init(Host, ServerHost, Opts) -> - node_hometree:init(Host, ServerHost, Opts). + node_flat:init(Host, ServerHost, Opts). terminate(Host, ServerHost) -> - node_hometree:terminate(Host, ServerHost). + node_flat:terminate(Host, ServerHost). options() -> [{deliver_payloads, true}, @@ -85,89 +84,89 @@ features() -> <<"subscription-notifications">>]. create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) -> - node_hometree:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). + node_flat:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). create_node(Nidx, Owner) -> - node_hometree:create_node(Nidx, Owner). + node_flat:create_node(Nidx, Owner). delete_node(Removed) -> - node_hometree:delete_node(Removed). + node_flat:delete_node(Removed). subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options) -> - node_hometree:subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, + node_flat:subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options). unsubscribe_node(Nidx, Sender, Subscriber, SubId) -> - node_hometree:unsubscribe_node(Nidx, Sender, Subscriber, SubId). + node_flat:unsubscribe_node(Nidx, Sender, Subscriber, SubId). publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload) -> - node_hometree:publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload). + node_flat:publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload). remove_extra_items(Nidx, MaxItems, ItemIds) -> - node_hometree:remove_extra_items(Nidx, MaxItems, ItemIds). + node_flat:remove_extra_items(Nidx, MaxItems, ItemIds). delete_item(Nidx, Publisher, PublishModel, ItemId) -> - node_hometree:delete_item(Nidx, Publisher, PublishModel, ItemId). + node_flat:delete_item(Nidx, Publisher, PublishModel, ItemId). purge_node(Nidx, Owner) -> - node_hometree:purge_node(Nidx, Owner). + node_flat:purge_node(Nidx, Owner). get_entity_affiliations(Host, Owner) -> - node_hometree:get_entity_affiliations(Host, Owner). + node_flat:get_entity_affiliations(Host, Owner). get_node_affiliations(Nidx) -> - node_hometree:get_node_affiliations(Nidx). + node_flat:get_node_affiliations(Nidx). get_affiliation(Nidx, Owner) -> - node_hometree:get_affiliation(Nidx, Owner). + node_flat:get_affiliation(Nidx, Owner). set_affiliation(Nidx, Owner, Affiliation) -> - node_hometree:set_affiliation(Nidx, Owner, Affiliation). + node_flat:set_affiliation(Nidx, Owner, Affiliation). get_entity_subscriptions(Host, Owner) -> - node_hometree:get_entity_subscriptions(Host, Owner). + node_flat:get_entity_subscriptions(Host, Owner). get_node_subscriptions(Nidx) -> - node_hometree:get_node_subscriptions(Nidx). + node_flat:get_node_subscriptions(Nidx). get_subscriptions(Nidx, Owner) -> - node_hometree:get_subscriptions(Nidx, Owner). + node_flat:get_subscriptions(Nidx, Owner). set_subscriptions(Nidx, Owner, Subscription, SubId) -> - node_hometree:set_subscriptions(Nidx, Owner, Subscription, SubId). + node_flat:set_subscriptions(Nidx, Owner, Subscription, SubId). get_pending_nodes(Host, Owner) -> - node_hometree:get_pending_nodes(Host, Owner). + node_flat:get_pending_nodes(Host, Owner). get_states(Nidx) -> - node_hometree:get_states(Nidx). + node_flat:get_states(Nidx). get_state(Nidx, JID) -> - node_hometree:get_state(Nidx, JID). + node_flat:get_state(Nidx, JID). set_state(State) -> - node_hometree:set_state(State). + node_flat:set_state(State). get_items(Nidx, From, RSM) -> - node_hometree:get_items(Nidx, From, RSM). + node_flat:get_items(Nidx, From, RSM). get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM) -> - node_hometree:get_items(Nidx, JID, AccessModel, + node_flat:get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM). get_item(Nidx, ItemId) -> - node_hometree:get_item(Nidx, ItemId). + node_flat:get_item(Nidx, ItemId). get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> - node_hometree:get_item(Nidx, ItemId, JID, AccessModel, + node_flat:get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId). set_item(Item) -> - node_hometree:set_item(Item). + node_flat:set_item(Item). get_item_name(Host, Node, Id) -> - node_hometree:get_item_name(Host, Node, Id). + node_flat:get_item_name(Host, Node, Id). node_to_path(Node) -> node_flat:node_to_path(Node). diff --git a/src/nodetree_dag.erl b/src/nodetree_dag.erl index b2d4fade..8ac56b27 100644 --- a/src/nodetree_dag.erl +++ b/src/nodetree_dag.erl @@ -1,21 +1,27 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. -%%% +%%%---------------------------------------------------------------------- +%%% File : nodetree_dag.erl +%%% Author : Brian Cully <bjc@kublai.com> +%%% Purpose : experimental support of XEP-248 +%%% Created : 15 Jun 2009 by Brian Cully <bjc@kublai.com> %%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. -%%% %%% -%%% @author Brian Cully <bjc@kublai.com> -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== +%%% ejabberd, Copyright (C) 2002-2016 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(nodetree_dag). -behaviour(gen_pubsub_nodetree). @@ -51,7 +57,7 @@ set_node(#pubsub_node{nodeid = {Key, _}, owners = Owners, options = Options} = N end. create_node(Key, Node, Type, Owner, Options, Parents) -> - OwnerJID = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), + OwnerJID = jid:tolower(jid:remove_resource(Owner)), case find_node(Key, Node) of false -> Nidx = pubsub_index:new(node), diff --git a/src/nodetree_tree.erl b/src/nodetree_tree.erl index e34eabf3..e3f7e251 100644 --- a/src/nodetree_tree.erl +++ b/src/nodetree_tree.erl @@ -1,29 +1,27 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. -%%% +%%%---------------------------------------------------------------------- +%%% File : nodetree_tree.erl +%%% Author : Christophe Romain <christophe.romain@process-one.net> +%%% Purpose : Standard node tree plugin +%%% Created : 1 Dec 2007 by Christophe Romain <christophe.romain@process-one.net> %%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. -%%% %%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. +%%% ejabberd, Copyright (C) 2002-2016 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. %%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Christophe Romain <christophe.romain@process-one.net> -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== +%%% 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. +%%% +%%%---------------------------------------------------------------------- %%% @doc The module <strong>{@module}</strong> is the default PubSub node tree plugin. %%% <p>It is used as a default for all unknown PubSub node type. It can serve @@ -148,7 +146,7 @@ get_subnodes_tree(Host, Node) -> end. create_node(Host, Node, Type, Owner, Options, Parents) -> - BJID = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), + BJID = jid:tolower(jid:remove_resource(Owner)), case catch mnesia:read({pubsub_node, {Host, Node}}) of [] -> ParentExists = case Host of diff --git a/src/nodetree_tree_odbc.erl b/src/nodetree_tree_odbc.erl index 38fb51c2..ef1c20b6 100644 --- a/src/nodetree_tree_odbc.erl +++ b/src/nodetree_tree_odbc.erl @@ -1,29 +1,27 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. -%%% +%%%---------------------------------------------------------------------- +%%% File : nodetree_tree_odbc.erl +%%% Author : Christophe Romain <christophe.romain@process-one.net> +%%% Purpose : Standard node tree plugin with ODBC backend +%%% Created : 1 Dec 2007 by Christophe Romain <christophe.romain@process-one.net> %%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. -%%% %%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. +%%% ejabberd, Copyright (C) 2002-2016 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. %%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Christophe Romain <christophe.romain@process-one.net> -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== +%%% 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. +%%% +%%%---------------------------------------------------------------------- %%% @doc The module <strong>{@module}</strong> is the default PubSub node tree plugin. %%% <p>It is used as a default for all unknown PubSub node type. It can serve @@ -66,7 +64,7 @@ set_node(Record) when is_record(Record, pubsub_node) -> [First | _] -> First end, Type = Record#pubsub_node.type, - H = node_hometree_odbc:encode_host(Host), + H = node_flat_odbc:encode_host(Host), N = ejabberd_odbc:escape(Node), P = ejabberd_odbc:escape(Parent), Nidx = case nodeidx(Host, Node) of @@ -116,7 +114,7 @@ get_node(Host, Node, _From) -> get_node(Host, Node). get_node(Host, Node) -> - H = node_hometree_odbc:encode_host(Host), + H = node_flat_odbc:encode_host(Host), N = ejabberd_odbc:escape(Node), case catch ejabberd_odbc:sql_query_t([<<"select node, parent, type, nodeid from " @@ -151,7 +149,7 @@ get_nodes(Host, _From) -> get_nodes(Host). get_nodes(Host) -> - H = node_hometree_odbc:encode_host(Host), + H = node_flat_odbc:encode_host(Host), case catch ejabberd_odbc:sql_query_t([<<"select node, parent, type, nodeid from " "pubsub_node where host='">>, H, <<"';">>]) @@ -178,7 +176,7 @@ get_subnodes(Host, Node, _From) -> get_subnodes(Host, Node). get_subnodes(Host, Node) -> - H = node_hometree_odbc:encode_host(Host), + H = node_flat_odbc:encode_host(Host), N = ejabberd_odbc:escape(Node), case catch ejabberd_odbc:sql_query_t([<<"select node, parent, type, nodeid from " @@ -196,7 +194,7 @@ get_subnodes_tree(Host, Node, _From) -> get_subnodes_tree(Host, Node). get_subnodes_tree(Host, Node) -> - H = node_hometree_odbc:encode_host(Host), + H = node_flat_odbc:encode_host(Host), N = ejabberd_odbc:escape(Node), case catch ejabberd_odbc:sql_query_t([<<"select node, parent, type, nodeid from " @@ -211,7 +209,7 @@ get_subnodes_tree(Host, Node) -> end. create_node(Host, Node, Type, Owner, Options, Parents) -> - BJID = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), + BJID = jid:tolower(jid:remove_resource(Owner)), case nodeidx(Host, Node) of {error, ?ERR_ITEM_NOT_FOUND} -> ParentExists = case Host of @@ -256,7 +254,7 @@ create_node(Host, Node, Type, Owner, Options, Parents) -> end. delete_node(Host, Node) -> - H = node_hometree_odbc:encode_host(Host), + H = node_flat_odbc:encode_host(Host), N = ejabberd_odbc:escape(Node), Removed = get_subnodes_tree(Host, Node), catch ejabberd_odbc:sql_query_t([<<"delete from pubsub_node where host='">>, @@ -295,7 +293,7 @@ raw_to_node(Host, [Node, Parent, Type, Nidx]) -> id = Nidx, type = Type, options = Options}. nodeidx(Host, Node) -> - H = node_hometree_odbc:encode_host(Host), + H = node_flat_odbc:encode_host(Host), N = ejabberd_odbc:escape(Node), case catch ejabberd_odbc:sql_query_t([<<"select nodeid from pubsub_node where " @@ -311,5 +309,5 @@ nodeidx(Host, Node) -> end. nodeowners(Nidx) -> - {result, Res} = node_hometree_odbc:get_node_affiliations(Nidx), + {result, Res} = node_flat_odbc:get_node_affiliations(Nidx), [LJID || {LJID, Aff} <- Res, Aff =:= owner]. diff --git a/src/nodetree_virtual.erl b/src/nodetree_virtual.erl index d56be860..934950dd 100644 --- a/src/nodetree_virtual.erl +++ b/src/nodetree_virtual.erl @@ -1,29 +1,27 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. -%%% +%%%---------------------------------------------------------------------- +%%% File : nodetree_virtual.erl +%%% Author : Christophe Romain <christophe.romain@process-one.net> +%%% Purpose : Standard node tree plugin using no storage backend +%%% Created : 1 Dec 2007 by Christophe Romain <christophe.romain@process-one.net> %%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. -%%% %%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. +%%% ejabberd, Copyright (C) 2002-2016 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. %%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Christophe Romain <christophe.romain@process-one.net> -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== +%%% 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. +%%% +%%%---------------------------------------------------------------------- %%% @doc The module <strong>{@module}</strong> is the PubSub node tree plugin that %%% allow virtual nodes handling. This prevent storage of nodes. @@ -61,14 +59,12 @@ get_node(Host, Node, _From) -> get_node(Host, Node). get_node(Host, Node) -> - get_node(nodeidx(Host, Node)). + Nidx = nodeidx(Host, Node), + node_record(Host, Node, Nidx). get_node(Nidx) -> {Host, Node} = nodeid(Nidx), - Record = #pubsub_node{nodeid = Node, id = Nidx}, - Module = jlib:binary_to_atom(<<"node_", (Record#pubsub_node.type)/binary>>), - Record#pubsub_node{owners = [{<<"">>, Host, <<"">>}], - options = Module:options()}. + node_record(Host, Node, Nidx). get_nodes(Host, _From) -> get_nodes(Host). @@ -80,10 +76,7 @@ get_parentnodes(_Host, _Node, _From) -> []. get_parentnodes_tree(Host, Node, From) -> - case get_node(Host, Node, From) of - Node when is_record(Node, pubsub_node) -> [{0, [Node]}]; - _Error -> [] - end. + [{0, [get_node(Host, Node, From)]}]. get_subnodes(Host, Node, _From) -> get_subnodes(Host, Node). @@ -98,12 +91,35 @@ get_subnodes_tree(_Host, _Node) -> []. create_node(Host, Node, _Type, _Owner, _Options, _Parents) -> - {error, {virtual, {Host, Node}}}. + {error, {virtual, nodeidx(Host, Node)}}. delete_node(Host, Node) -> [get_node(Host, Node)]. %% internal helper -nodeidx(Host, Node) -> term_to_binary({Host, Node}). -nodeid(Nidx) -> binary_to_term(Nidx). +node_record({U,S,R}, Node, Nidx) -> + Host = mod_pubsub:host(S), + Type = <<"pep">>, + Module = mod_pubsub:plugin(Host, Type), + #pubsub_node{nodeid = {{U,S,R},Node}, id = Nidx, type = Type, + owners = [{U,S,R}], + options = Module:options()}; +node_record(Host, Node, Nidx) -> + [Type|_] = mod_pubsub:plugins(Host), + Module = mod_pubsub:plugin(Host, Type), + #pubsub_node{nodeid = {Host, Node}, id = Nidx, type = Type, + owners = [{<<"">>, Host, <<"">>}], + options = Module:options()}. + +nodeidx({U,S,R}, Node) -> + JID = jid:to_string(jid:make(U,S,R)), + <<JID/binary, ":", Node/binary>>; +nodeidx(Host, Node) -> + <<Host/binary, ":", Node/binary>>. +nodeid(Nidx) -> + [Head, Node] = binary:split(Nidx, <<":">>), + case jid:from_string(Head) of + {jid,<<>>,Host,<<>>,_,_,_} -> {Host, Node}; + {jid,U,S,R,_,_,_} -> {{U,S,R}, Node} + end. diff --git a/src/odbc_queries.erl b/src/odbc_queries.erl index 7dee1a04..c12931c6 100644 --- a/src/odbc_queries.erl +++ b/src/odbc_queries.erl @@ -5,7 +5,7 @@ %%% Created : by Mickael Remond <mremond@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,55 +25,44 @@ -module(odbc_queries). +-compile([{parse_transform, ejabberd_sql_pt}]). + +-behaviour(ejabberd_config). + -author("mremond@process-one.net"). --export([get_db_type/0, update/5, update_t/4, sql_transaction/2, - get_last/2, set_last_t/4, del_last/2, - get_password/2, get_password_scram/2, - set_password_t/3, set_password_scram_t/6, - add_user/3, add_user_scram/6, del_user/2, - del_user_return_password/3, list_users/1, list_users/2, - users_number/1, users_number/2, add_spool_sql/2, - add_spool/2, get_and_del_spool_msg_t/2, del_spool_msg/2, - get_roster/2, get_roster_jid_groups/2, +-export([get_db_type/0, update/5, update_t/4, + sql_transaction/2, get_last/2, set_last_t/4, del_last/2, + get_password/2, get_password_scram/2, set_password_t/3, + set_password_scram_t/6, add_user/3, add_user_scram/6, + del_user/2, del_user_return_password/3, list_users/1, + list_users/2, users_number/1, users_number/2, + add_spool_sql/2, add_spool/2, get_and_del_spool_msg_t/2, + del_spool_msg/2, get_roster/2, get_roster_jid_groups/2, get_roster_groups/3, del_user_roster_t/2, get_roster_by_jid/3, get_rostergroup_by_jid/3, del_roster/3, del_roster_sql/2, update_roster/5, - update_roster_sql/4, roster_subscribe/4, + update_roster_sql/4, roster_subscribe/1, get_subscription/3, set_private_data/4, - set_private_data_sql/3, get_private_data/3, get_private_data/2, - del_user_private_storage/2, get_default_privacy_list/2, + set_private_data_sql/3, get_private_data/3, + get_private_data/2, del_user_private_storage/2, + get_default_privacy_list/2, get_default_privacy_list_t/1, get_privacy_list_names/2, get_privacy_list_names_t/1, get_privacy_list_id/3, get_privacy_list_id_t/2, get_privacy_list_data/3, - get_privacy_list_data_by_id/2, get_privacy_list_data_t/2, + get_privacy_list_data_by_id/2, + get_privacy_list_data_t/2, get_privacy_list_data_by_id_t/1, set_default_privacy_list/2, - unset_default_privacy_list/2, - remove_privacy_list/2, - add_privacy_list/2, - set_privacy_list/2, - del_privacy_lists/3, - set_vcard/26, - get_vcard/2, - escape/1, - count_records_where/3, - get_roster_version/2, - set_roster_version/2]). - -%% We have only two compile time options for db queries: -%-define(generic, true). -%-define(mssql, true). --ifndef(mssql). - --undef(generic). - --define(generic, true). - --endif. + unset_default_privacy_list/2, remove_privacy_list/2, + add_privacy_list/2, set_privacy_list/2, + del_privacy_lists/2, set_vcard/26, get_vcard/2, + escape/1, count_records_where/3, get_roster_version/2, + set_roster_version/2, opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). +-include("ejabberd_sql_pt.hrl"). %% Almost a copy of string:join/2. %% We use this version because string:join/2 is relatively @@ -81,10 +70,6 @@ join([], _Sep) -> []; join([H | T], Sep) -> [H, [[Sep, X] || X <- T]]. -%% ----------------- -%% Generic queries --ifdef(generic). - get_db_type() -> generic. %% Safe atomic update. @@ -137,95 +122,92 @@ update(LServer, Table, Fields, Vals, Where) -> sql_transaction(LServer, F) -> ejabberd_odbc:sql_transaction(LServer, F). -get_last(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"select seconds, state from last where " - "username='">>, - Username, <<"'">>]). +get_last(LServer, LUser) -> + ejabberd_odbc:sql_query( + LServer, + ?SQL("select @(seconds)d, @(state)s from last" + " where username=%(LUser)s")). -set_last_t(LServer, Username, Seconds, State) -> - update(LServer, <<"last">>, - [<<"username">>, <<"seconds">>, <<"state">>], - [Username, Seconds, State], - [<<"username='">>, Username, <<"'">>]). +set_last_t(LServer, LUser, TimeStamp, Status) -> + ?SQL_UPSERT(LServer, "last", + ["!username=%(LUser)s", + "seconds=%(TimeStamp)d", + "state=%(Status)s"]). -del_last(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"delete from last where username='">>, Username, - <<"'">>]). +del_last(LServer, LUser) -> + ejabberd_odbc:sql_query( + LServer, + ?SQL("delete from last where username=%(LUser)s")). -get_password(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"select password from users where username='">>, - Username, <<"';">>]). +get_password(LServer, LUser) -> + ejabberd_odbc:sql_query( + LServer, + ?SQL("select @(password)s from users where username=%(LUser)s")). -get_password_scram(LServer, Username) -> +get_password_scram(LServer, LUser) -> ejabberd_odbc:sql_query( LServer, - [<<"select password, serverkey, salt, iterationcount from users where " - "username='">>, Username, <<"';">>]). - -set_password_t(LServer, Username, Pass) -> - ejabberd_odbc:sql_transaction(LServer, - fun () -> - update_t(<<"users">>, - [<<"username">>, - <<"password">>], - [Username, Pass], - [<<"username='">>, Username, - <<"'">>]) - end). - -set_password_scram_t(LServer, Username, + ?SQL("select @(password)s, @(serverkey)s, @(salt)s, @(iterationcount)d" + " from users" + " where username=%(LUser)s")). + +set_password_t(LServer, LUser, Password) -> + ejabberd_odbc:sql_transaction( + LServer, + fun () -> + ?SQL_UPSERT_T( + "users", + ["!username=%(LUser)s", + "password=%(Password)s"]) + end). + +set_password_scram_t(LServer, LUser, StoredKey, ServerKey, Salt, IterationCount) -> - ejabberd_odbc:sql_transaction(LServer, - fun () -> - update_t(<<"users">>, - [<<"username">>, - <<"password">>, - <<"serverkey">>, - <<"salt">>, - <<"iterationcount">>], - [Username, StoredKey, - ServerKey, Salt, - IterationCount], - [<<"username='">>, Username, - <<"'">>]) - end). - -add_user(LServer, Username, Pass) -> - ejabberd_odbc:sql_query(LServer, - [<<"insert into users(username, password) " - "values ('">>, - Username, <<"', '">>, Pass, <<"');">>]). + ejabberd_odbc:sql_transaction( + LServer, + fun () -> + ?SQL_UPSERT_T( + "users", + ["!username=%(LUser)s", + "password=%(StoredKey)s", + "serverkey=%(ServerKey)s", + "salt=%(Salt)s", + "iterationcount=%(IterationCount)d"]) + end). + +add_user(LServer, LUser, Password) -> + ejabberd_odbc:sql_query( + LServer, + ?SQL("insert into users(username, password) " + "values (%(LUser)s, %(Password)s)")). -add_user_scram(LServer, Username, +add_user_scram(LServer, LUser, StoredKey, ServerKey, Salt, IterationCount) -> - ejabberd_odbc:sql_query(LServer, - [<<"insert into users(username, password, serverkey, salt, iterationcount) " - "values ('">>, - Username, <<"', '">>, StoredKey, <<"', '">>, - ServerKey, <<"', '">>, - Salt, <<"', '">>, - IterationCount, <<"');">>]). - -del_user(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"delete from users where username='">>, Username, - <<"';">>]). + ejabberd_odbc:sql_query( + LServer, + ?SQL("insert into users(username, password, serverkey, salt, " + "iterationcount) " + "values (%(LUser)s, %(StoredKey)s, %(ServerKey)s," + " %(Salt)s, %(IterationCount)d)")). + +del_user(LServer, LUser) -> + ejabberd_odbc:sql_query( + LServer, + ?SQL("delete from users where username=%(LUser)s")). -del_user_return_password(_LServer, Username, Pass) -> +del_user_return_password(_LServer, LUser, Password) -> P = - ejabberd_odbc:sql_query_t([<<"select password from users where username='">>, - Username, <<"';">>]), - ejabberd_odbc:sql_query_t([<<"delete from users where username='">>, - Username, <<"' and password='">>, Pass, - <<"';">>]), + ejabberd_odbc:sql_query_t( + ?SQL("select @(password)s from users where username=%(LUser)s")), + ejabberd_odbc:sql_query_t( + ?SQL("delete from users" + " where username=%(LUser)s and password=%(Password)s")), P. list_users(LServer) -> - ejabberd_odbc:sql_query(LServer, - [<<"select username from users">>]). + ejabberd_odbc:sql_query( + LServer, + ?SQL("select @(username)s from users")). list_users(LServer, [{from, Start}, {to, End}]) when is_integer(Start) and is_integer(End) -> @@ -240,64 +222,54 @@ list_users(LServer, {offset, Start - 1}]); list_users(LServer, [{limit, Limit}, {offset, Offset}]) when is_integer(Limit) and is_integer(Offset) -> - ejabberd_odbc:sql_query(LServer, - [list_to_binary( - io_lib:format( - "select username from users " ++ - "order by username " ++ - "limit ~w offset ~w", - [Limit, Offset]))]); + ejabberd_odbc:sql_query( + LServer, + ?SQL("select @(username)s from users " + "order by username " + "limit %(Limit)d offset %(Offset)d")); list_users(LServer, [{prefix, Prefix}, {limit, Limit}, {offset, Offset}]) when is_binary(Prefix) and is_integer(Limit) and is_integer(Offset) -> - ejabberd_odbc:sql_query(LServer, - [list_to_binary( - io_lib:format( - "select username from users " ++ - "where username like '~s%' " ++ - "order by username " ++ - "limit ~w offset ~w ", - [Prefix, Limit, Offset]))]). + SPrefix = ejabberd_odbc:escape_like_arg(Prefix), + SPrefix2 = <<SPrefix/binary, $%>>, + ejabberd_odbc:sql_query( + LServer, + ?SQL("select @(username)s from users " + "where username like %(SPrefix2)s " + "order by username " + "limit %(Limit)d offset %(Offset)d")). users_number(LServer) -> - Type = ejabberd_config:get_option({odbc_type, LServer}, - fun(pgsql) -> pgsql; - (mysql) -> mysql; - (sqlite) -> sqlite; - (odbc) -> odbc - end, odbc), - case Type of - pgsql -> - case - ejabberd_config:get_option( - {pgsql_users_number_estimate, LServer}, - fun(V) when is_boolean(V) -> V end, - false) - of - true -> - ejabberd_odbc:sql_query(LServer, - [<<"select reltuples from pg_class where " - "oid = 'users'::regclass::oid">>]); - _ -> - ejabberd_odbc:sql_query(LServer, - [<<"select count(*) from users">>]) + ejabberd_odbc:sql_query( + LServer, + fun(pgsql, _) -> + case + ejabberd_config:get_option( + {pgsql_users_number_estimate, LServer}, + fun(V) when is_boolean(V) -> V end, + false) of + true -> + ejabberd_odbc:sql_query_t( + ?SQL("select @(reltuples :: bigint)d from pg_class" + " where oid = 'users'::regclass::oid")); + _ -> + ejabberd_odbc:sql_query_t( + ?SQL("select @(count(*))d from users")) end; - _ -> - ejabberd_odbc:sql_query(LServer, - [<<"select count(*) from users">>]) - end. + (_Type, _) -> + ejabberd_odbc:sql_query_t( + ?SQL("select @(count(*))d from users")) + end). users_number(LServer, [{prefix, Prefix}]) when is_binary(Prefix) -> - ejabberd_odbc:sql_query(LServer, - [list_to_binary( - io_lib:fwrite( - "select count(*) from users " ++ - %% Warning: Escape prefix at higher level to prevent SQL - %% injection. - "where username like '~s%'", - [Prefix]))]); + SPrefix = ejabberd_odbc:escape_like_arg(Prefix), + SPrefix2 = <<SPrefix/binary, $%>>, + ejabberd_odbc:sql_query( + LServer, + ?SQL("select @(count(*))d from users " + "where username like %(SPrefix2)s")); users_number(LServer, []) -> users_number(LServer). @@ -309,74 +281,71 @@ add_spool_sql(Username, XML) -> add_spool(LServer, Queries) -> ejabberd_odbc:sql_transaction(LServer, Queries). -get_and_del_spool_msg_t(LServer, Username) -> +get_and_del_spool_msg_t(LServer, LUser) -> F = fun () -> Result = - ejabberd_odbc:sql_query_t([<<"select username, xml from spool where " - "username='">>, - Username, - <<"' order by seq;">>]), - ejabberd_odbc:sql_query_t([<<"delete from spool where username='">>, - Username, <<"';">>]), + ejabberd_odbc:sql_query_t( + ?SQL("select @(username)s, @(xml)s from spool where " + "username=%(LUser)s order by seq;")), + ejabberd_odbc:sql_query_t( + ?SQL("delete from spool where username=%(LUser)s;")), Result end, ejabberd_odbc:sql_transaction(LServer, F). -del_spool_msg(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"delete from spool where username='">>, Username, - <<"';">>]). +del_spool_msg(LServer, LUser) -> + ejabberd_odbc:sql_query( + LServer, + ?SQL("delete from spool where username=%(LUser)s")). -get_roster(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"select username, jid, nick, subscription, " - "ask, askmessage, server, subscribe, " - "type from rosterusers where username='">>, - Username, <<"'">>]). +get_roster(LServer, LUser) -> + ejabberd_odbc:sql_query( + LServer, + ?SQL("select @(username)s, @(jid)s, @(nick)s, @(subscription)s, " + "@(ask)s, @(askmessage)s, @(server)s, @(subscribe)s, " + "@(type)s from rosterusers where username=%(LUser)s")). -get_roster_jid_groups(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"select jid, grp from rostergroups where " - "username='">>, - Username, <<"'">>]). - -get_roster_groups(_LServer, Username, SJID) -> - ejabberd_odbc:sql_query_t([<<"select grp from rostergroups where username='">>, - Username, <<"' and jid='">>, SJID, <<"';">>]). - -del_user_roster_t(LServer, Username) -> - ejabberd_odbc:sql_transaction(LServer, - fun () -> - ejabberd_odbc:sql_query_t([<<"delete from rosterusers where " - "username='">>, - Username, - <<"';">>]), - ejabberd_odbc:sql_query_t([<<"delete from rostergroups where " - "username='">>, - Username, - <<"';">>]) - end). - -get_roster_by_jid(_LServer, Username, SJID) -> - ejabberd_odbc:sql_query_t([<<"select username, jid, nick, subscription, " - "ask, askmessage, server, subscribe, " - "type from rosterusers where username='">>, - Username, <<"' and jid='">>, SJID, <<"';">>]). - -get_rostergroup_by_jid(LServer, Username, SJID) -> - ejabberd_odbc:sql_query(LServer, - [<<"select grp from rostergroups where username='">>, - Username, <<"' and jid='">>, SJID, <<"'">>]). - -del_roster(_LServer, Username, SJID) -> - ejabberd_odbc:sql_query_t([<<"delete from rosterusers where " - "username='">>, - Username, <<"' and jid='">>, SJID, - <<"';">>]), - ejabberd_odbc:sql_query_t([<<"delete from rostergroups where " - "username='">>, - Username, <<"' and jid='">>, SJID, - <<"';">>]). +get_roster_jid_groups(LServer, LUser) -> + ejabberd_odbc:sql_query( + LServer, + ?SQL("select @(jid)s, @(grp)s from rostergroups where " + "username=%(LUser)s")). + +get_roster_groups(_LServer, LUser, SJID) -> + ejabberd_odbc:sql_query_t( + ?SQL("select @(grp)s from rostergroups" + " where username=%(LUser)s and jid=%(SJID)s")). + +del_user_roster_t(LServer, LUser) -> + ejabberd_odbc:sql_transaction( + LServer, + fun () -> + ejabberd_odbc:sql_query_t( + ?SQL("delete from rosterusers where username=%(LUser)s")), + ejabberd_odbc:sql_query_t( + ?SQL("delete from rostergroups where username=%(LUser)s")) + end). + +get_roster_by_jid(_LServer, LUser, SJID) -> + ejabberd_odbc:sql_query_t( + ?SQL("select @(username)s, @(jid)s, @(nick)s, @(subscription)s," + " @(ask)s, @(askmessage)s, @(server)s, @(subscribe)s," + " @(type)s from rosterusers" + " where username=%(LUser)s and jid=%(SJID)s")). + +get_rostergroup_by_jid(LServer, LUser, SJID) -> + ejabberd_odbc:sql_query( + LServer, + ?SQL("select @(grp)s from rostergroups" + " where username=%(LUser)s and jid=%(SJID)s")). + +del_roster(_LServer, LUser, SJID) -> + ejabberd_odbc:sql_query_t( + ?SQL("delete from rosterusers" + " where username=%(LUser)s and jid=%(SJID)s")), + ejabberd_odbc:sql_query_t( + ?SQL("delete from rostergroups" + " where username=%(LUser)s and jid=%(SJID)s")). del_roster_sql(Username, SJID) -> [[<<"delete from rosterusers where " @@ -386,27 +355,19 @@ del_roster_sql(Username, SJID) -> "username='">>, Username, <<"' and jid='">>, SJID, <<"';">>]]. -update_roster(_LServer, Username, SJID, ItemVals, +update_roster(_LServer, LUser, SJID, ItemVals, ItemGroups) -> - update_t(<<"rosterusers">>, - [<<"username">>, <<"jid">>, <<"nick">>, - <<"subscription">>, <<"ask">>, <<"askmessage">>, - <<"server">>, <<"subscribe">>, <<"type">>], - ItemVals, - [<<"username='">>, Username, <<"' and jid='">>, SJID, - <<"'">>]), - ejabberd_odbc:sql_query_t([<<"delete from rostergroups where " - "username='">>, - Username, <<"' and jid='">>, SJID, - <<"';">>]), - lists:foreach(fun (ItemGroup) -> - ejabberd_odbc:sql_query_t([<<"insert into rostergroups( " - " username, jid, grp) values ('">>, - join(ItemGroup, - <<"', '">>), - <<"');">>]) - end, - ItemGroups). + roster_subscribe(ItemVals), + ejabberd_odbc:sql_query_t( + ?SQL("delete from rostergroups" + " where username=%(LUser)s and jid=%(SJID)s")), + lists:foreach( + fun(ItemGroup) -> + ejabberd_odbc:sql_query_t( + ?SQL("insert into rostergroups(username, jid, grp) " + "values (%(LUser)s, %(SJID)s, %(ItemGroup)s)")) + end, + ItemGroups). update_roster_sql(Username, SJID, ItemVals, ItemGroups) -> @@ -428,27 +389,31 @@ update_roster_sql(Username, SJID, ItemVals, join(ItemGroup, <<"', '">>), <<"');">>] || ItemGroup <- ItemGroups]. -roster_subscribe(_LServer, Username, SJID, ItemVals) -> - update_t(<<"rosterusers">>, - [<<"username">>, <<"jid">>, <<"nick">>, - <<"subscription">>, <<"ask">>, <<"askmessage">>, - <<"server">>, <<"subscribe">>, <<"type">>], - ItemVals, - [<<"username='">>, Username, <<"' and jid='">>, SJID, - <<"'">>]). - -get_subscription(LServer, Username, SJID) -> - ejabberd_odbc:sql_query(LServer, - [<<"select subscription from rosterusers " - "where username='">>, - Username, <<"' and jid='">>, SJID, <<"'">>]). +roster_subscribe({LUser, SJID, Name, SSubscription, SAsk, AskMessage}) -> + ?SQL_UPSERT_T( + "rosterusers", + ["!username=%(LUser)s", + "!jid=%(SJID)s", + "nick=%(Name)s", + "subscription=%(SSubscription)s", + "ask=%(SAsk)s", + "askmessage=%(AskMessage)s", + "server='N'", + "subscribe=''", + "type='item'"]). + +get_subscription(LServer, LUser, SJID) -> + ejabberd_odbc:sql_query( + LServer, + ?SQL("select @(subscription)s from rosterusers " + "where username=%(LUser)s and jid=%(SJID)s")). -set_private_data(_LServer, Username, LXMLNS, SData) -> - update_t(<<"private_storage">>, - [<<"username">>, <<"namespace">>, <<"data">>], - [Username, LXMLNS, SData], - [<<"username='">>, Username, <<"' and namespace='">>, - LXMLNS, <<"'">>]). +set_private_data(_LServer, LUser, XMLNS, SData) -> + ?SQL_UPSERT_T( + "private_storage", + ["!username=%(LUser)s", + "!namespace=%(XMLNS)s", + "data=%(SData)s"]). set_private_data_sql(Username, LXMLNS, SData) -> [[<<"delete from private_storage where username='">>, @@ -458,187 +423,189 @@ set_private_data_sql(Username, LXMLNS, SData) -> Username, <<"', '">>, LXMLNS, <<"', '">>, SData, <<"');">>]]. -get_private_data(LServer, Username, LXMLNS) -> - ejabberd_odbc:sql_query(LServer, - [<<"select data from private_storage where " - "username='">>, - Username, <<"' and namespace='">>, LXMLNS, - <<"';">>]). +get_private_data(LServer, LUser, XMLNS) -> + ejabberd_odbc:sql_query( + LServer, + ?SQL("select @(data)s from private_storage" + " where username=%(LUser)s and namespace=%(XMLNS)s")). -get_private_data(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"select namespace, data from private_storage " - "where username='">>, Username, <<"';">>]). +get_private_data(LServer, LUser) -> + ejabberd_odbc:sql_query( + LServer, + ?SQL("select @(namespace)s, @(data)s from private_storage" + " where username=%(LUser)s")). -del_user_private_storage(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"delete from private_storage where username='">>, - Username, <<"';">>]). - -set_vcard(LServer, LUsername, SBDay, SCTRY, SEMail, SFN, - SFamily, SGiven, SLBDay, SLCTRY, SLEMail, SLFN, - SLFamily, SLGiven, SLLocality, SLMiddle, SLNickname, - SLOrgName, SLOrgUnit, SLocality, SMiddle, SNickname, - SOrgName, SOrgUnit, SVCARD, Username) -> - ejabberd_odbc:sql_transaction(LServer, - fun () -> - update_t(<<"vcard">>, - [<<"username">>, - <<"vcard">>], - [LUsername, SVCARD], - [<<"username='">>, LUsername, - <<"'">>]), - update_t(<<"vcard_search">>, - [<<"username">>, - <<"lusername">>, <<"fn">>, - <<"lfn">>, <<"family">>, - <<"lfamily">>, <<"given">>, - <<"lgiven">>, <<"middle">>, - <<"lmiddle">>, - <<"nickname">>, - <<"lnickname">>, <<"bday">>, - <<"lbday">>, <<"ctry">>, - <<"lctry">>, <<"locality">>, - <<"llocality">>, - <<"email">>, <<"lemail">>, - <<"orgname">>, - <<"lorgname">>, - <<"orgunit">>, - <<"lorgunit">>], - [Username, LUsername, SFN, - SLFN, SFamily, SLFamily, - SGiven, SLGiven, SMiddle, - SLMiddle, SNickname, - SLNickname, SBDay, SLBDay, - SCTRY, SLCTRY, SLocality, - SLLocality, SEMail, SLEMail, - SOrgName, SLOrgName, - SOrgUnit, SLOrgUnit], - [<<"lusername='">>, - LUsername, <<"'">>]) - end). - -get_vcard(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"select vcard from vcard where username='">>, - Username, <<"';">>]). +del_user_private_storage(LServer, LUser) -> + ejabberd_odbc:sql_query( + LServer, + ?SQL("delete from private_storage" + " where username=%(LUser)s")). + +set_vcard(LServer, LUser, BDay, CTRY, EMail, FN, + Family, Given, LBDay, LCTRY, LEMail, LFN, + LFamily, LGiven, LLocality, LMiddle, LNickname, + LOrgName, LOrgUnit, Locality, Middle, Nickname, + OrgName, OrgUnit, SVCARD, User) -> + ejabberd_odbc:sql_transaction( + LServer, + fun() -> + ?SQL_UPSERT(LServer, "vcard", + ["!username=%(LUser)s", + "vcard=%(SVCARD)s"]), + ?SQL_UPSERT(LServer, "vcard_search", + ["username=%(User)s", + "!lusername=%(LUser)s", + "fn=%(FN)s", + "lfn=%(LFN)s", + "family=%(Family)s", + "lfamily=%(LFamily)s", + "given=%(Given)s", + "lgiven=%(LGiven)s", + "middle=%(Middle)s", + "lmiddle=%(LMiddle)s", + "nickname=%(Nickname)s", + "lnickname=%(LNickname)s", + "bday=%(BDay)s", + "lbday=%(LBDay)s", + "ctry=%(CTRY)s", + "lctry=%(LCTRY)s", + "locality=%(Locality)s", + "llocality=%(LLocality)s", + "email=%(EMail)s", + "lemail=%(LEMail)s", + "orgname=%(OrgName)s", + "lorgname=%(LOrgName)s", + "orgunit=%(OrgUnit)s", + "lorgunit=%(LOrgUnit)s"]) + end). + +get_vcard(LServer, LUser) -> + ejabberd_odbc:sql_query( + LServer, + ?SQL("select @(vcard)s from vcard where username=%(LUser)s")). -get_default_privacy_list(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"select name from privacy_default_list " - "where username='">>, - Username, <<"';">>]). +get_default_privacy_list(LServer, LUser) -> + ejabberd_odbc:sql_query( + LServer, + ?SQL("select @(name)s from privacy_default_list " + "where username=%(LUser)s")). -get_default_privacy_list_t(Username) -> - ejabberd_odbc:sql_query_t([<<"select name from privacy_default_list " - "where username='">>, - Username, <<"';">>]). +get_default_privacy_list_t(LUser) -> + ejabberd_odbc:sql_query_t( + ?SQL("select @(name)s from privacy_default_list " + "where username=%(LUser)s")). -get_privacy_list_names(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"select name from privacy_list where " - "username='">>, - Username, <<"';">>]). +get_privacy_list_names(LServer, LUser) -> + ejabberd_odbc:sql_query( + LServer, + ?SQL("select @(name)s from privacy_list" + " where username=%(LUser)s")). -get_privacy_list_names_t(Username) -> - ejabberd_odbc:sql_query_t([<<"select name from privacy_list where " - "username='">>, - Username, <<"';">>]). +get_privacy_list_names_t(LUser) -> + ejabberd_odbc:sql_query_t( + ?SQL("select @(name)s from privacy_list" + " where username=%(LUser)s")). -get_privacy_list_id(LServer, Username, SName) -> - ejabberd_odbc:sql_query(LServer, - [<<"select id from privacy_list where username='">>, - Username, <<"' and name='">>, SName, <<"';">>]). +get_privacy_list_id(LServer, LUser, Name) -> + ejabberd_odbc:sql_query( + LServer, + ?SQL("select @(id)d from privacy_list" + " where username=%(LUser)s and name=%(Name)s")). -get_privacy_list_id_t(Username, SName) -> - ejabberd_odbc:sql_query_t([<<"select id from privacy_list where username='">>, - Username, <<"' and name='">>, SName, <<"';">>]). +get_privacy_list_id_t(LUser, Name) -> + ejabberd_odbc:sql_query_t( + ?SQL("select @(id)d from privacy_list" + " where username=%(LUser)s and name=%(Name)s")). -get_privacy_list_data(LServer, Username, SName) -> - ejabberd_odbc:sql_query(LServer, - [<<"select t, value, action, ord, match_all, " - "match_iq, match_message, match_presence_in, " - "match_presence_out from privacy_list_data " - "where id = (select id from privacy_list " - "where username='">>, - Username, <<"' and name='">>, SName, - <<"') order by ord;">>]). - -get_privacy_list_data_t(Username, SName) -> - ejabberd_odbc:sql_query_t([<<"select t, value, action, ord, match_all, " - "match_iq, match_message, match_presence_in, " - "match_presence_out from privacy_list_data " - "where id = (select id from privacy_list " - "where username='">>, - Username, <<"' and name='">>, SName, - <<"') order by ord;">>]). +get_privacy_list_data(LServer, LUser, Name) -> + ejabberd_odbc:sql_query( + LServer, + ?SQL("select @(t)s, @(value)s, @(action)s, @(ord)d, @(match_all)b, " + "@(match_iq)b, @(match_message)b, @(match_presence_in)b, " + "@(match_presence_out)b from privacy_list_data " + "where id =" + " (select id from privacy_list" + " where username=%(LUser)s and name=%(Name)s) " + "order by ord")). + +%% Not used? +get_privacy_list_data_t(LUser, Name) -> + ejabberd_odbc:sql_query_t( + ?SQL("select @(t)s, @(value)s, @(action)s, @(ord)d, @(match_all)b, " + "@(match_iq)b, @(match_message)b, @(match_presence_in)b, " + "@(match_presence_out)b from privacy_list_data " + "where id =" + " (select id from privacy_list" + " where username=%(LUser)s and name=%(Name)s) " + "order by ord")). get_privacy_list_data_by_id(LServer, ID) -> - ejabberd_odbc:sql_query(LServer, - [<<"select t, value, action, ord, match_all, " - "match_iq, match_message, match_presence_in, " - "match_presence_out from privacy_list_data " - "where id='">>, - ID, <<"' order by ord;">>]). + ejabberd_odbc:sql_query( + LServer, + ?SQL("select @(t)s, @(value)s, @(action)s, @(ord)d, @(match_all)b, " + "@(match_iq)b, @(match_message)b, @(match_presence_in)b, " + "@(match_presence_out)b from privacy_list_data " + "where id=%(ID)d order by ord")). get_privacy_list_data_by_id_t(ID) -> - ejabberd_odbc:sql_query_t([<<"select t, value, action, ord, match_all, " - "match_iq, match_message, match_presence_in, " - "match_presence_out from privacy_list_data " - "where id='">>, - ID, <<"' order by ord;">>]). - -set_default_privacy_list(Username, SName) -> - update_t(<<"privacy_default_list">>, - [<<"username">>, <<"name">>], [Username, SName], - [<<"username='">>, Username, <<"'">>]). - -unset_default_privacy_list(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"delete from privacy_default_list " - " where username='">>, - Username, <<"';">>]). + ejabberd_odbc:sql_query_t( + ?SQL("select @(t)s, @(value)s, @(action)s, @(ord)d, @(match_all)b, " + "@(match_iq)b, @(match_message)b, @(match_presence_in)b, " + "@(match_presence_out)b from privacy_list_data " + "where id=%(ID)d order by ord")). + +set_default_privacy_list(LUser, Name) -> + ?SQL_UPSERT_T( + "privacy_default_list", + ["!username=%(LUser)s", + "name=%(Name)s"]). + +unset_default_privacy_list(LServer, LUser) -> + ejabberd_odbc:sql_query( + LServer, + ?SQL("delete from privacy_default_list" + " where username=%(LUser)s")). -remove_privacy_list(Username, SName) -> - ejabberd_odbc:sql_query_t([<<"delete from privacy_list where username='">>, - Username, <<"' and name='">>, SName, <<"';">>]). +remove_privacy_list(LUser, Name) -> + ejabberd_odbc:sql_query_t( + ?SQL("delete from privacy_list where" + " username=%(LUser)s and name=%(Name)s")). -add_privacy_list(Username, SName) -> - ejabberd_odbc:sql_query_t([<<"insert into privacy_list(username, name) " - "values ('">>, - Username, <<"', '">>, SName, <<"');">>]). +add_privacy_list(LUser, Name) -> + ejabberd_odbc:sql_query_t( + ?SQL("insert into privacy_list(username, name) " + "values (%(LUser)s, %(Name)s)")). set_privacy_list(ID, RItems) -> - ejabberd_odbc:sql_query_t([<<"delete from privacy_list_data where " - "id='">>, - ID, <<"';">>]), - lists:foreach(fun (Items) -> - ejabberd_odbc:sql_query_t([<<"insert into privacy_list_data(id, t, " - "value, action, ord, match_all, match_iq, " - "match_message, match_presence_in, match_prese" - "nce_out ) values ('">>, - ID, <<"', '">>, - join(Items, <<"', '">>), - <<"');">>]) + ejabberd_odbc:sql_query_t( + ?SQL("delete from privacy_list_data where id=%(ID)d")), + lists:foreach( + fun({SType, SValue, SAction, Order, MatchAll, MatchIQ, + MatchMessage, MatchPresenceIn, MatchPresenceOut}) -> + ejabberd_odbc:sql_query_t( + ?SQL("insert into privacy_list_data(id, t, " + "value, action, ord, match_all, match_iq, " + "match_message, match_presence_in, match_presence_out) " + "values (%(ID)d, %(SType)s, %(SValue)s, %(SAction)s," + " %(Order)d, %(MatchAll)b, %(MatchIQ)b," + " %(MatchMessage)b, %(MatchPresenceIn)b," + " %(MatchPresenceOut)b)")) end, RItems). -del_privacy_lists(LServer, Server, Username) -> -%% Characters to escape -%% Count number of records in a table given a where clause - ejabberd_odbc:sql_query(LServer, - [<<"delete from privacy_list where username='">>, - Username, <<"';">>]), - ejabberd_odbc:sql_query(LServer, - [<<"delete from privacy_list_data where " - "value='">>, - <<Username/binary, "@", Server/binary>>, - <<"';">>]), - ejabberd_odbc:sql_query(LServer, - [<<"delete from privacy_default_list where " - "username='">>, - Username, <<"';">>]). +del_privacy_lists(LServer, LUser) -> + ejabberd_odbc:sql_query( + LServer, + ?SQL("delete from privacy_list where username=%(LUser)s")), + %US = <<LUser/binary, "@", LServer/binary>>, + %ejabberd_odbc:sql_query( + % LServer, + % ?SQL("delete from privacy_list_data where value=%(US)s")), + ejabberd_odbc:sql_query( + LServer, + ?SQL("delete from privacy_default_list where username=%(LUser)s")). +%% Characters to escape escape($\000) -> <<"\\0">>; escape($\n) -> <<"\\n">>; escape($\t) -> <<"\\t">>; @@ -649,344 +616,30 @@ escape($") -> <<"\\\"">>; escape($\\) -> <<"\\\\">>; escape(C) -> <<C>>. +%% Count number of records in a table given a where clause count_records_where(LServer, Table, WhereClause) -> ejabberd_odbc:sql_query(LServer, [<<"select count(*) from ">>, Table, <<" ">>, WhereClause, <<";">>]). get_roster_version(LServer, LUser) -> - ejabberd_odbc:sql_query(LServer, - [<<"select version from roster_version where " - "username = '">>, - LUser, <<"'">>]). + ejabberd_odbc:sql_query( + LServer, + ?SQL("select @(version)s from roster_version" + " where username = %(LUser)s")). set_roster_version(LUser, Version) -> update_t(<<"roster_version">>, [<<"username">>, <<"version">>], [LUser, Version], [<<"username = '">>, LUser, <<"'">>]). --endif. - -%% ----------------- -%% MSSQL queries --ifdef(mssql). - -%% Queries can be either a fun or a list of queries -get_db_type() -> mssql. - -sql_transaction(LServer, Queries) - when is_list(Queries) -> - F = fun () -> - lists:foreach(fun (Query) -> - ejabberd_odbc:sql_query(LServer, Query) - end, - Queries) - end, - {atomic, catch F()}; -sql_transaction(_LServer, FQueries) -> - {atomic, catch FQueries()}. - -get_last(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.get_last '">>, Username, <<"'">>]). - -set_last_t(LServer, Username, Seconds, State) -> - Result = ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.set_last '">>, Username, - <<"', '">>, Seconds, <<"', '">>, State, - <<"'">>]), - {atomic, Result}. - -del_last(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.del_last '">>, Username, <<"'">>]). - -get_password(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.get_password '">>, Username, - <<"'">>]). - -set_password_t(LServer, Username, Pass) -> - Result = ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.set_password '">>, - Username, <<"', '">>, Pass, <<"'">>]), - {atomic, Result}. - -add_user(LServer, Username, Pass) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.add_user '">>, Username, <<"', '">>, - Pass, <<"'">>]). - -del_user(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.del_user '">>, Username, <<"'">>]). - -del_user_return_password(LServer, Username, Pass) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.del_user_return_password '">>, - Username, <<"'">>]), - Pass. - -list_users(LServer) -> - ejabberd_odbc:sql_query(LServer, - <<"EXECUTE dbo.list_users">>). - -list_users(LServer, _) -> list_users(LServer). - -users_number(LServer) -> - ejabberd_odbc:sql_query(LServer, - <<"select count(*) from users with (nolock)">>). - -users_number(LServer, _) -> users_number(LServer). - -add_spool_sql(Username, XML) -> - [<<"EXECUTE dbo.add_spool '">>, Username, <<"' , '">>, - XML, <<"'">>]. - -add_spool(LServer, Queries) -> - lists:foreach(fun (Query) -> - ejabberd_odbc:sql_query(LServer, Query) - end, - Queries). - -get_and_del_spool_msg_t(LServer, Username) -> - [Result] = case ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.get_and_del_spool_msg '">>, - Username, <<"'">>]) - of - Rs when is_list(Rs) -> - lists:filter(fun ({selected, _Header, _Row}) -> true; - ({updated, _N}) -> false - end, - Rs); - Rs -> [Rs] - end, - {atomic, Result}. - -del_spool_msg(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.del_spool_msg '">>, Username, - <<"'">>]). - -get_roster(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.get_roster '">>, Username, - <<"'">>]). - -get_roster_jid_groups(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.get_roster_jid_groups '">>, - Username, <<"'">>]). - -get_roster_groups(LServer, Username, SJID) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.get_roster_groups '">>, Username, - <<"' , '">>, SJID, <<"'">>]). - -del_user_roster_t(LServer, Username) -> - Result = ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.del_user_roster '">>, - Username, <<"'">>]), - {atomic, Result}. - -get_roster_by_jid(LServer, Username, SJID) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.get_roster_by_jid '">>, Username, - <<"' , '">>, SJID, <<"'">>]). - -get_rostergroup_by_jid(LServer, Username, SJID) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.get_rostergroup_by_jid '">>, - Username, <<"' , '">>, SJID, <<"'">>]). - -del_roster(LServer, Username, SJID) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.del_roster '">>, Username, - <<"', '">>, SJID, <<"'">>]). - -del_roster_sql(Username, SJID) -> - [<<"EXECUTE dbo.del_roster '">>, Username, <<"', '">>, - SJID, <<"'">>]. - -update_roster(LServer, Username, SJID, ItemVals, - ItemGroups) -> - Query1 = [<<"EXECUTE dbo.del_roster '">>, Username, - <<"', '">>, SJID, <<"' ">>], - ejabberd_odbc:sql_query(LServer, lists:flatten(Query1)), - Query2 = [<<"EXECUTE dbo.add_roster_user ">>, ItemVals], - ejabberd_odbc:sql_query(LServer, lists:flatten(Query2)), - Query3 = [<<"EXECUTE dbo.del_roster_groups '">>, - Username, <<"', '">>, SJID, <<"' ">>], - ejabberd_odbc:sql_query(LServer, lists:flatten(Query3)), - lists:foreach(fun (ItemGroup) -> - Query = [<<"EXECUTE dbo.add_roster_group ">>, - ItemGroup], - ejabberd_odbc:sql_query(LServer, lists:flatten(Query)) - end, - ItemGroups). - -update_roster_sql(Username, SJID, ItemVals, - ItemGroups) -> - [<<"BEGIN TRANSACTION ">>, - <<"EXECUTE dbo.del_roster_groups '">>, Username, - <<"','">>, SJID, <<"' ">>, - <<"EXECUTE dbo.add_roster_user ">>, ItemVals, <<" ">>] - ++ - [lists:flatten(<<"EXECUTE dbo.add_roster_group ">>, - ItemGroup, <<" ">>) - || ItemGroup <- ItemGroups] - ++ [<<"COMMIT">>]. - -roster_subscribe(LServer, _Username, _SJID, ItemVals) -> - catch ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.add_roster_user ">>, - ItemVals]). - -get_subscription(LServer, Username, SJID) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.get_subscription '">>, Username, - <<"' , '">>, SJID, <<"'">>]). - -set_private_data(LServer, Username, LXMLNS, SData) -> - ejabberd_odbc:sql_query(LServer, - set_private_data_sql(Username, LXMLNS, SData)). - -set_private_data_sql(Username, LXMLNS, SData) -> - [<<"EXECUTE dbo.set_private_data '">>, Username, - <<"' , '">>, LXMLNS, <<"' , '">>, SData, <<"'">>]. - -get_private_data(LServer, Username, LXMLNS) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.get_private_data '">>, Username, - <<"' , '">>, LXMLNS, <<"'">>]). - -del_user_private_storage(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.del_user_storage '">>, Username, - <<"'">>]). - -set_vcard(LServer, LUsername, SBDay, SCTRY, SEMail, SFN, - SFamily, SGiven, SLBDay, SLCTRY, SLEMail, SLFN, - SLFamily, SLGiven, SLLocality, SLMiddle, SLNickname, - SLOrgName, SLOrgUnit, SLocality, SMiddle, SNickname, - SOrgName, SOrgUnit, SVCARD, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.set_vcard '">>, SVCARD, <<"' , '">>, - Username, <<"' , '">>, LUsername, <<"' , '">>, SFN, - <<"' , '">>, SLFN, <<"' , '">>, SFamily, - <<"' , '">>, SLFamily, <<"' , '">>, SGiven, - <<"' , '">>, SLGiven, <<"' , '">>, SMiddle, - <<"' , '">>, SLMiddle, <<"' , '">>, SNickname, - <<"' , '">>, SLNickname, <<"' , '">>, SBDay, - <<"' , '">>, SLBDay, <<"' , '">>, SCTRY, - <<"' , '">>, SLCTRY, <<"' , '">>, SLocality, - <<"' , '">>, SLLocality, <<"' , '">>, SEMail, - <<"' , '">>, SLEMail, <<"' , '">>, SOrgName, - <<"' , '">>, SLOrgName, <<"' , '">>, SOrgUnit, - <<"' , '">>, SLOrgUnit, <<"'">>]). - -get_vcard(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.get_vcard '">>, Username, <<"'">>]). - -get_default_privacy_list(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.get_default_privacy_list '">>, - Username, <<"'">>]). - -get_default_privacy_list_t(Username) -> - ejabberd_odbc:sql_query_t([<<"EXECUTE dbo.get_default_privacy_list '">>, - Username, <<"'">>]). - -get_privacy_list_names(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.get_privacy_list_names '">>, - Username, <<"'">>]). - -get_privacy_list_names_t(Username) -> - ejabberd_odbc:sql_query_t([<<"EXECUTE dbo.get_privacy_list_names '">>, - Username, <<"'">>]). - -get_privacy_list_id(LServer, Username, SName) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.get_privacy_list_id '">>, Username, - <<"' , '">>, SName, <<"'">>]). - -get_privacy_list_id_t(Username, SName) -> - ejabberd_odbc:sql_query_t([<<"EXECUTE dbo.get_privacy_list_id '">>, - Username, <<"' , '">>, SName, <<"'">>]). - -get_privacy_list_data(LServer, Username, SName) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.get_privacy_list_data '">>, - Username, <<"' , '">>, SName, <<"'">>]). - -get_privacy_list_data_by_id(LServer, ID) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.get_privacy_list_data_by_id '">>, - ID, <<"'">>]). - -get_privacy_list_data_by_id_t(ID) -> - ejabberd_odbc:sql_query_t([<<"EXECUTE dbo.get_privacy_list_data_by_id '">>, - ID, <<"'">>]). - -set_default_privacy_list(Username, SName) -> - ejabberd_odbc:sql_query_t([<<"EXECUTE dbo.set_default_privacy_list '">>, - Username, <<"' , '">>, SName, <<"'">>]). - -unset_default_privacy_list(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.unset_default_privacy_list '">>, - Username, <<"'">>]). - -remove_privacy_list(Username, SName) -> - ejabberd_odbc:sql_query_t([<<"EXECUTE dbo.remove_privacy_list '">>, - Username, <<"' , '">>, SName, <<"'">>]). - -add_privacy_list(Username, SName) -> - ejabberd_odbc:sql_query_t([<<"EXECUTE dbo.add_privacy_list '">>, - Username, <<"' , '">>, SName, <<"'">>]). - -set_privacy_list(ID, RItems) -> - ejabberd_odbc:sql_query_t([<<"EXECUTE dbo.del_privacy_list_by_id '">>, - ID, <<"'">>]), - lists:foreach(fun (Items) -> - ejabberd_odbc:sql_query_t([<<"EXECUTE dbo.set_privacy_list '">>, - ID, <<"', '">>, - join(Items, <<"', '">>), - <<"'">>]) - end, - RItems). - -del_privacy_lists(LServer, Server, Username) -> -%% Characters to escape -%% Count number of records in a table given a where clause - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.del_privacy_lists @Server='">>, - Server, <<"' @username='">>, Username, <<"'">>]). - -escape($\000) -> <<"\\0">>; -escape($\t) -> <<"\\t">>; -escape($\b) -> <<"\\b">>; -escape($\r) -> <<"\\r">>; -escape($') -> <<"''">>; -escape($") -> <<"\\\"">>; -escape(C) -> C. - -count_records_where(LServer, Table, WhereClause) -> - ejabberd_odbc:sql_query(LServer, - [<<"select count(*) from ">>, Table, - <<" with (nolock) ">>, WhereClause]). - -get_roster_version(LServer, LUser) -> - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.get_roster_version '">>, LUser, - <<"'">>]). - -set_roster_version(Username, Version) -> - LServer = (?MYNAME), - ejabberd_odbc:sql_query(LServer, - [<<"EXECUTE dbo.set_roster_version '">>, Username, - <<"', '">>, Version, <<"'">>]). - --endif. +opt_type(odbc_type) -> + fun (pgsql) -> pgsql; + (mysql) -> mysql; + (sqlite) -> sqlite; + (mssql) -> mssql; + (odbc) -> odbc + end; +opt_type(pgsql_users_number_estimate) -> + fun (V) when is_boolean(V) -> V end; +opt_type(_) -> [odbc_type, pgsql_users_number_estimate]. diff --git a/src/prosody2ejabberd.erl b/src/prosody2ejabberd.erl new file mode 100644 index 00000000..204cfec2 --- /dev/null +++ b/src/prosody2ejabberd.erl @@ -0,0 +1,341 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 20 Jan 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(prosody2ejabberd). + +%% API +-export([from_dir/1]). + +-include("ejabberd.hrl"). +-include("jlib.hrl"). +-include("logger.hrl"). +-include("mod_roster.hrl"). +-include("mod_offline.hrl"). +-include("mod_privacy.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +from_dir(ProsodyDir) -> + case file:list_dir(ProsodyDir) of + {ok, HostDirs} -> + lists:foreach( + fun(HostDir) -> + Host = list_to_binary(HostDir), + lists:foreach( + fun(SubDir) -> + Path = filename:join( + [ProsodyDir, HostDir, SubDir]), + convert_dir(Path, Host, SubDir) + end, ["vcard", "accounts", "roster", + "private", "config", "offline", + "privacy"]) + end, HostDirs); + {error, Why} = Err -> + ?ERROR_MSG("failed to list ~s: ~s", + [ProsodyDir, file:format_error(Why)]), + Err + end. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +convert_dir(Path, Host, Type) -> + case file:list_dir(Path) of + {ok, Files} -> + lists:foreach( + fun(File) -> + FilePath = filename:join(Path, File), + case eval_file(FilePath) of + {ok, Data} -> + Name = iolist_to_binary(filename:rootname(File)), + convert_data(Host, Type, Name, Data); + Err -> + Err + end + end, Files); + {error, enoent} -> + ok; + {error, Why} = Err -> + ?ERROR_MSG("failed to list ~s: ~s", + [Path, file:format_error(Why)]), + Err + end. + +eval_file(Path) -> + case file:read_file(Path) of + {ok, Data} -> + State0 = luerl:init(), + State1 = luerl:set_table([item], + fun([X], State) -> {[X], State} end, + State0), + NewData = case filename:extension(Path) of + ".list" -> + <<"return {", Data/binary, "};">>; + _ -> + Data + end, + case luerl:eval(NewData, State1) of + {ok, _} = Res -> + Res; + {error, Why} = Err -> + ?ERROR_MSG("failed to eval ~s: ~p", [Path, Why]), + Err + end; + {error, Why} = Err -> + ?ERROR_MSG("failed to read file ~s: ~s", + [Path, file:format_error(Why)]), + Err + end. + +convert_data(Host, "accounts", User, [Data]) -> + Password = proplists:get_value(<<"password">>, Data, <<>>), + case ejabberd_auth:try_register(User, Host, Password) of + {atomic, ok} -> + ok; + Err -> + ?ERROR_MSG("failed to register user ~s@~s: ~p", + [User, Host, Err]), + Err + end; +convert_data(Host, "roster", User, [Data]) -> + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Host), + Rosters = + lists:flatmap( + fun({<<"pending">>, L}) -> + convert_pending_item(LUser, LServer, L); + ({S, L}) when is_binary(S) -> + convert_roster_item(LUser, LServer, S, L); + (_) -> + [] + end, Data), + lists:foreach(fun mod_roster:set_roster/1, Rosters); +convert_data(Host, "private", User, [Data]) -> + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Host), + PrivData = lists:flatmap( + fun({_TagXMLNS, Raw}) -> + case deserialize(Raw) of + [El] -> + XMLNS = fxml:get_tag_attr_s(<<"xmlns">>, El), + [{XMLNS, El}]; + _ -> + [] + end + end, Data), + mod_private:set_data(LUser, LServer, PrivData); +convert_data(Host, "vcard", User, [Data]) -> + LServer = jid:nameprep(Host), + case deserialize(Data) of + [VCard] -> + mod_vcard:set_vcard(User, LServer, VCard); + _ -> + ok + end; +convert_data(_Host, "config", _User, [Data]) -> + RoomJID = jid:from_string(proplists:get_value(<<"jid">>, Data, <<"">>)), + Config = proplists:get_value(<<"_data">>, Data, []), + RoomCfg = convert_room_config(Data), + case proplists:get_bool(<<"persistent">>, Config) of + true when RoomJID /= error -> + mod_muc:store_room(?MYNAME, RoomJID#jid.lserver, + RoomJID#jid.luser, RoomCfg); + _ -> + ok + end; +convert_data(Host, "offline", User, [Data]) -> + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Host), + Msgs = lists:flatmap( + fun({_, RawXML}) -> + case deserialize(RawXML) of + [El] -> el_to_offline_msg(LUser, LServer, El); + _ -> [] + end + end, Data), + mod_offline:store_offline_msg( + LServer, {LUser, LServer}, Msgs, length(Msgs), infinity); +convert_data(Host, "privacy", User, [Data]) -> + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Host), + Lists = proplists:get_value(<<"lists">>, Data, []), + Priv = #privacy{ + us = {LUser, LServer}, + default = proplists:get_value(<<"default">>, Data, none), + lists = lists:flatmap( + fun({Name, Vals}) -> + Items = proplists:get_value(<<"items">>, Vals, []), + case lists:map(fun convert_privacy_item/1, + Items) of + [] -> []; + ListItems -> [{Name, ListItems}] + end + end, Lists)}, + mod_privacy:set_privacy_list(Priv); +convert_data(_Host, _Type, _User, _Data) -> + ok. + +convert_pending_item(LUser, LServer, LuaList) -> + lists:flatmap( + fun({S, true}) -> + case jid:from_string(S) of + #jid{} = J -> + LJID = jid:tolower(J), + [#roster{usj = {LUser, LServer, LJID}, + us = {LUser, LServer}, + jid = LJID, + ask = in}]; + error -> + [] + end; + (_) -> + [] + end, LuaList). + +convert_roster_item(LUser, LServer, JIDstring, LuaList) -> + case jid:from_string(JIDstring) of + #jid{} = JID -> + LJID = jid:tolower(JID), + InitR = #roster{usj = {LUser, LServer, LJID}, + us = {LUser, LServer}, + jid = LJID}, + Roster = + lists:foldl( + fun({<<"groups">>, Val}, R) -> + Gs = lists:flatmap( + fun({G, true}) -> [G]; + (_) -> [] + end, Val), + R#roster{groups = Gs}; + ({<<"subscription">>, Sub}, R) -> + R#roster{subscription = jlib:binary_to_atom(Sub)}; + ({<<"ask">>, <<"subscribe">>}, R) -> + R#roster{ask = out}; + ({<<"name">>, Name}, R) -> + R#roster{name = Name} + end, InitR, LuaList), + [Roster]; + error -> + [] + end. + +convert_room_affiliations(Data) -> + lists:flatmap( + fun({J, Aff}) -> + case jid:from_string(J) of + #jid{luser = U, lserver = S} -> + [{{U, S, <<>>}, jlib:binary_to_atom(Aff)}]; + error -> + [] + end + end, proplists:get_value(<<"_affiliations">>, Data, [])). + +convert_room_config(Data) -> + Config = proplists:get_value(<<"_data">>, Data, []), + Pass = case proplists:get_value(<<"password">>, Config, <<"">>) of + <<"">> -> + []; + Password -> + [{password_protected, true}, + {password, Password}] + end, + Subj = case jid:from_string( + proplists:get_value( + <<"subject_from">>, Config, <<"">>)) of + #jid{lresource = Nick} when Nick /= <<"">> -> + [{subject, proplists:get_value(<<"subject">>, Config, <<"">>)}, + {subject_author, Nick}]; + _ -> + [] + end, + Anonymous = case proplists:get_value(<<"whois">>, Config, <<"moderators">>) of + <<"moderators">> -> true; + _ -> false + end, + [{affiliations, convert_room_affiliations(Data)}, + {allow_change_subj, proplists:get_bool(<<"changesubject">>, Config)}, + {description, proplists:get_value(<<"description">>, Config, <<"">>)}, + {members_only, proplists:get_bool(<<"members_only">>, Config)}, + {moderated, proplists:get_bool(<<"moderated">>, Config)}, + {anonymous, Anonymous}] ++ Pass ++ Subj. + +convert_privacy_item({_, Item}) -> + Action = proplists:get_value(<<"action">>, Item, <<"allow">>), + Order = proplists:get_value(<<"order">>, Item, 0), + T = jlib:binary_to_atom(proplists:get_value(<<"type">>, Item, <<"none">>)), + V = proplists:get_value(<<"value">>, Item, <<"">>), + MatchIQ = proplists:get_bool(<<"iq">>, Item), + MatchMsg = proplists:get_bool(<<"message">>, Item), + MatchPresIn = proplists:get_bool(<<"presence-in">>, Item), + MatchPresOut = proplists:get_bool(<<"presence-out">>, Item), + MatchAll = if (MatchIQ == false) and (MatchMsg == false) and + (MatchPresIn == false) and (MatchPresOut == false) -> + true; + true -> + false + end, + {Type, Value} = try case T of + none -> {T, none}; + group -> {T, V}; + jid -> {T, jid:tolower(jid:from_string(V))}; + subscription -> {T, jlib:binary_to_atom(V)} + end + catch _:_ -> + {none, none} + end, + #listitem{type = Type, + value = Value, + action = jlib:binary_to_atom(Action), + order = erlang:trunc(Order), + match_all = MatchAll, + match_iq = MatchIQ, + match_message = MatchMsg, + match_presence_in = MatchPresIn, + match_presence_out = MatchPresOut}. + +el_to_offline_msg(LUser, LServer, #xmlel{attrs = Attrs} = El) -> + case jlib:datetime_string_to_timestamp( + fxml:get_attr_s(<<"stamp">>, Attrs)) of + {_, _, _} = TS -> + Attrs1 = lists:filter( + fun(<<"stamp">>) -> false; + (<<"stamp_legacy">>) -> false; + (_) -> true + end, Attrs), + Packet = El#xmlel{attrs = Attrs1}, + case {jid:from_string(fxml:get_attr_s(<<"from">>, Attrs)), + jid:from_string(fxml:get_attr_s(<<"to">>, Attrs))} of + {#jid{} = From, #jid{} = To} -> + [#offline_msg{ + us = {LUser, LServer}, + timestamp = TS, + expire = never, + from = From, + to = To, + packet = Packet}]; + _ -> + [] + end; + _ -> + [] + end. + +deserialize(L) -> + deserialize(L, #xmlel{}, []). + +deserialize([{<<"attr">>, Attrs}|T], El, Acc) -> + deserialize(T, El#xmlel{attrs = Attrs ++ El#xmlel.attrs}, Acc); +deserialize([{<<"name">>, Name}|T], El, Acc) -> + deserialize(T, El#xmlel{name = Name}, Acc); +deserialize([{_, S}|T], #xmlel{children = Els} = El, Acc) when is_binary(S) -> + deserialize(T, El#xmlel{children = [{xmlcdata, S}|Els]}, Acc); +deserialize([{_, L}|T], #xmlel{children = Els} = El, Acc) when is_list(L) -> + deserialize(T, El#xmlel{children = deserialize(L) ++ Els}, Acc); +deserialize([], El, Acc) -> + [El|Acc]. diff --git a/src/pubsub_db_odbc.erl b/src/pubsub_db_odbc.erl index 56a500eb..cfdeda1e 100644 --- a/src/pubsub_db_odbc.erl +++ b/src/pubsub_db_odbc.erl @@ -1,24 +1,28 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. +%%%---------------------------------------------------------------------- +%%% File : pubsub_db_odbc.erl +%%% Author : Pablo Polvorin <pablo.polvorin@process-one.net> +%%% Purpose : Provide helpers for PubSub ODBC backend +%%% Created : 7 Aug 2009 by Pablo Polvorin <pablo.polvorin@process-one.net> %%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. %%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% -%%% @author Pablo Polvorin <pablo.polvorin@process-one.net> -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== +%%% 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(pubsub_db_odbc). -author("pablo.polvorin@process-one.net"). diff --git a/src/pubsub_index.erl b/src/pubsub_index.erl index c9ec94a6..983356a1 100644 --- a/src/pubsub_index.erl +++ b/src/pubsub_index.erl @@ -1,29 +1,27 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. -%%% +%%%---------------------------------------------------------------------- +%%% File : pubsub_index.erl +%%% Author : Christophe Romain <christophe.romain@process-one.net> +%%% Purpose : Provide uniq integer index for pubsub node +%%% Created : 30 Apr 2009 by Christophe Romain <christophe.romain@process-one.net> %%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. -%%% %%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. +%%% ejabberd, Copyright (C) 2002-2016 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. %%% -%%% @copyright 2006-2015 ProcessOne -%%% @author Christophe Romain <christophe.romain@process-one.net> -%%% [http://www.process-one.net/] -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== +%%% 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. +%%% +%%%---------------------------------------------------------------------- %% important note: %% new/1 and free/2 MUST be called inside a transaction bloc diff --git a/src/pubsub_migrate.erl b/src/pubsub_migrate.erl index e48efcd4..c493b58f 100644 --- a/src/pubsub_migrate.erl +++ b/src/pubsub_migrate.erl @@ -5,7 +5,7 @@ %%% Created : 26 Jul 2014 by Christophe Romain <christophe.romain@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -17,10 +17,9 @@ %%% 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 +%%% 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. %%% %%%---------------------------------------------------------------------- @@ -29,7 +28,8 @@ -include("pubsub.hrl"). -include("logger.hrl"). --export([update_node_database/2, update_state_database/2, update_lastitem_database/2]). +-export([update_node_database/2, update_state_database/2]). +-export([update_item_database/2, update_lastitem_database/2]). update_item_database_binary() -> F = fun () -> @@ -57,6 +57,27 @@ update_item_database_binary() -> ?INFO_MSG("Pubsub items table has been binarized: ~p", [Result]) end. +update_item_database(_Host, _ServerHost) -> + F = fun() -> + ?INFO_MSG("Migration of old pubsub items...", []), + lists:foreach(fun (Key) -> + [Item] = mnesia:read({pubsub_item, Key}), + Payload = [xmlelement_to_xmlel(El) || El <- Item#pubsub_item.payload], + mnesia:write(Item#pubsub_item{payload=Payload}) + end, + mnesia:all_keys(pubsub_item)) + end, + case mnesia:transaction(F) of + {aborted, Reason} -> + ?ERROR_MSG("Failed to migrate old pubsub items to xmlel: ~p", [Reason]); + {atomic, Result} -> + ?INFO_MSG("Pubsub items has been migrated: ~p", [Result]) + end. + +xmlelement_to_xmlel({xmlelement, A, B, C}) when is_list(C) -> + {xmlel, A, B, [xmlelement_to_xmlel(El) || El <- C]}; +xmlelement_to_xmlel(El) -> + El. update_node_database_binary() -> F = fun () -> @@ -136,7 +157,7 @@ update_node_database(Host, ServerHost) -> {unknown, Publisher}, M = - {now(), + {p1_time_compat:timestamp(), Publisher}, mnesia:write(#pubsub_item{itemid = diff --git a/src/pubsub_subscription.erl b/src/pubsub_subscription.erl index df156000..22c90414 100644 --- a/src/pubsub_subscription.erl +++ b/src/pubsub_subscription.erl @@ -1,24 +1,27 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. +%%%---------------------------------------------------------------------- +%%% File : pubsub_subscription.erl +%%% Author : Brian Cully <bjc@kublai.com> +%%% Purpose : Handle pubsub subscriptions options +%%% Created : 29 May 2009 by Brian Cully <bjc@kublai.com> %%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. %%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% -%%% @author Brian Cully <bjc@kublai.com> -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== +%%% 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(pubsub_subscription). @@ -27,6 +30,7 @@ %% API -export([init/0, subscribe_node/3, unsubscribe_node/3, get_subscription/3, set_subscription/4, + make_subid/0, get_options_xform/2, parse_options_xform/1]). % Internal function also exported for use in transactional bloc from pubsub plugins @@ -122,7 +126,7 @@ get_options_xform(Lang, Options) -> ++ XFields}}. parse_options_xform(XFields) -> - case xml:remove_cdata(XFields) of + case fxml:remove_cdata(XFields) of [#xmlel{name = <<"x">>} = XEl] -> case jlib:parse_xdata_submit(XEl) of XData when is_list(XData) -> @@ -202,7 +206,7 @@ write_subscription(_JID, _NodeId, SubID, Options) -> -spec(make_subid/0 :: () -> SubId::mod_pubsub:subId()). make_subid() -> - {T1, T2, T3} = now(), + {T1, T2, T3} = p1_time_compat:timestamp(), iolist_to_binary(io_lib:fwrite("~.16B~.16B~.16B", [T1, T2, T3])). %% diff --git a/src/pubsub_subscription_odbc.erl b/src/pubsub_subscription_odbc.erl index 6c99b155..149308ad 100644 --- a/src/pubsub_subscription_odbc.erl +++ b/src/pubsub_subscription_odbc.erl @@ -1,25 +1,28 @@ -%%% ==================================================================== -%%% ``The contents of this file are subject to the Erlang Public License, -%%% Version 1.1, (the "License"); you may not use this file except in -%%% compliance with the License. You should have received a copy of the -%%% Erlang Public License along with this software. If not, it can be -%%% retrieved via the world wide web at http://www.erlang.org/. +%%%---------------------------------------------------------------------- +%%% File : pubsub_subscription_odbc.erl +%%% Author : Pablo Polvorin <pablo.polvorin@process-one.net> +%%% Purpose : Handle pubsub subscriptions options with ODBC backend +%%% based on pubsub_subscription.erl by Brian Cully <bjc@kublai.com> +%%% Created : 7 Aug 2009 by Pablo Polvorin <pablo.polvorin@process-one.net> %%% -%%% Software distributed under the License is distributed on an "AS IS" -%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See -%%% the License for the specific language governing rights and limitations -%%% under the License. %%% -%%% The Initial Developer of the Original Code is ProcessOne. -%%% Portions created by ProcessOne are Copyright 2006-2015, ProcessOne -%%% All Rights Reserved.'' -%%% This software is copyright 2006-2015, ProcessOne. +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% -%%% @author Pablo Polvorin <pablo.polvorin@process-one.net> -%%% @author based on pubsub_subscription.erl by Brian Cully <bjc@kublai.com> -%%% @version {@vsn}, {@date} {@time} -%%% @end -%%% ==================================================================== +%%% 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(pubsub_subscription_odbc). -author("pablo.polvorin@process-one.net"). @@ -27,6 +30,7 @@ %% API -export([init/0, subscribe_node/3, unsubscribe_node/3, get_subscription/3, set_subscription/4, + make_subid/0, get_options_xform/2, parse_options_xform/1]). -include("pubsub.hrl"). @@ -147,7 +151,7 @@ get_options_xform(Lang, Options) -> ++ XFields}}. parse_options_xform(XFields) -> - case xml:remove_cdata(XFields) of + case fxml:remove_cdata(XFields) of [#xmlel{name = <<"x">>} = XEl] -> case jlib:parse_xdata_submit(XEl) of XData when is_list(XData) -> @@ -165,7 +169,7 @@ create_table() -> ok. -spec(make_subid/0 :: () -> mod_pubsub:subId()). make_subid() -> - {T1, T2, T3} = now(), + {T1, T2, T3} = p1_time_compat:timestamp(), iolist_to_binary(io_lib:fwrite("~.16B~.16B~.16B", [T1, T2, T3])). %% diff --git a/src/randoms.erl b/src/randoms.erl index 950f29fc..52fceef4 100644 --- a/src/randoms.erl +++ b/src/randoms.erl @@ -5,7 +5,7 @@ %%% Created : 13 Dec 2002 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -29,23 +29,12 @@ -export([get_string/0]). --export([start/0, init/0]). +-export([start/0]). start() -> - register(random_generator, spawn(randoms, init, [])). - -init() -> - {A1, A2, A3} = now(), random:seed(A1, A2, A3), loop(). - -loop() -> - receive - {From, get_random, N} -> - From ! {random, random:uniform(N)}, loop(); - _ -> loop() - end. + ok. get_string() -> - random_generator ! {self(), get_random, 65536 * 65536}, - receive - {random, R} -> jlib:integer_to_binary(R) - end. + R = crypto:rand_uniform(0, 16#10000000000000000), + jlib:integer_to_binary(R). + diff --git a/src/scram.erl b/src/scram.erl index 8c793521..2c4d265b 100644 --- a/src/scram.erl +++ b/src/scram.erl @@ -5,7 +5,7 @@ %%% Created : 7 Aug 2011 by Stephen Röttger <stephen.roettger@googlemail.com> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -36,7 +36,7 @@ -spec salted_password(binary(), binary(), non_neg_integer()) -> binary(). salted_password(Password, Salt, IterationCount) -> - hi(jlib:resourceprep(Password), Salt, IterationCount). + hi(jid:resourceprep(Password), Salt, IterationCount). -spec client_key(binary()) -> binary(). @@ -86,6 +86,4 @@ hi_round(Password, UPrev, IterationCount) -> IterationCount - 1)))). sha_mac(Key, Data) -> - Context1 = crypto:hmac_init(sha, Key), - Context2 = crypto:hmac_update(Context1, Data), - crypto:hmac_final(Context2). + crypto:hmac(sha, Key, Data). diff --git a/src/shaper.erl b/src/shaper.erl index a85c4f11..a136c213 100644 --- a/src/shaper.erl +++ b/src/shaper.erl @@ -5,7 +5,7 @@ %%% Created : 9 Feb 2003 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,10 +25,13 @@ -module(shaper). +-behaviour(ejabberd_config). + -author('alexey@process-one.net'). --export([start/0, new/1, new1/1, update/2, get_max_rate/1, - transform_options/1, load_from_config/0]). +-export([start/0, new/1, new1/1, update/2, + get_max_rate/1, transform_options/1, load_from_config/0, + opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -104,7 +107,7 @@ new(Name) -> new1(none) -> none; new1(MaxRate) -> #maxrate{maxrate = MaxRate, lastrate = 0.0, - lasttime = now_to_usec(now())}. + lasttime = p1_time_compat:system_time(micro_seconds)}. -spec update(shaper(), integer()) -> {shaper(), integer()}. @@ -112,7 +115,7 @@ update(none, _Size) -> {none, 0}; update(#maxrate{} = State, Size) -> MinInterv = 1000 * Size / (2 * State#maxrate.maxrate - State#maxrate.lastrate), - Interv = (now_to_usec(now()) - State#maxrate.lasttime) / + Interv = (p1_time_compat:system_time(micro_seconds) - State#maxrate.lasttime) / 1000, ?DEBUG("State: ~p, Size=~p~nM=~p, I=~p~n", [State, Size, MinInterv, Interv]), @@ -120,7 +123,7 @@ update(#maxrate{} = State, Size) -> 1 + trunc(MinInterv - Interv); true -> 0 end, - NextNow = now_to_usec(now()) + Pause * 1000, + NextNow = p1_time_compat:system_time(micro_seconds) + Pause * 1000, {State#maxrate{lastrate = (State#maxrate.lastrate + 1000000 * Size / (NextNow - State#maxrate.lasttime)) @@ -138,5 +141,5 @@ transform_options({OptName, Name, none}, Opts) when OptName == shaper -> transform_options(Opt, Opts) -> [Opt|Opts]. -now_to_usec({MSec, Sec, USec}) -> - (MSec * 1000000 + Sec) * 1000000 + USec. +opt_type(shaper) -> fun (V) -> V end; +opt_type(_) -> [shaper]. diff --git a/src/str.erl b/src/str.erl index 80d7b05b..27d21075 100644 --- a/src/str.erl +++ b/src/str.erl @@ -5,7 +5,7 @@ %%% Created : 23 Feb 2012 by Evgeniy Khramtsov <ekhramtsov@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/translate.erl b/src/translate.erl index 277dfa44..e9f61ab8 100644 --- a/src/translate.erl +++ b/src/translate.erl @@ -5,7 +5,7 @@ %%% Created : 6 Jan 2003 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as diff --git a/src/win32_dns.erl b/src/win32_dns.erl index f0e4b5f2..5dda22b9 100644 --- a/src/win32_dns.erl +++ b/src/win32_dns.erl @@ -5,7 +5,7 @@ %%% Created : 5 Mar 2009 by Geoff Cant %%% %%% -%%% ejabberd, Copyright (C) 2002-2015 ProcessOne +%%% ejabberd, Copyright (C) 2002-2016 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as |