diff options
Diffstat (limited to 'src')
171 files changed, 13207 insertions, 8650 deletions
diff --git a/src/acl.erl b/src/acl.erl index fdf397d88..06202c67e 100644 --- a/src/acl.erl +++ b/src/acl.erl @@ -31,9 +31,11 @@ -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, + match_rule/3, match_access/4, match_acl/3, transform_options/1, opt_type/1]). +-export([add_access/3, clear/0]). + -include("ejabberd.hrl"). -include("logger.hrl"). -include("jlib.hrl"). @@ -43,6 +45,7 @@ rules = [] :: [access_rule()]}). -type regexp() :: binary(). +-type iprange() :: {inet:ip_address(), integer()} | binary(). -type glob() :: binary(). -type access_name() :: atom(). -type access_rule() :: {atom(), any()}. @@ -61,7 +64,7 @@ {user_glob, {glob(), host()} | glob()} | {server_glob, glob()} | {resource_glob, glob()} | - {ip, {inet:ip_address(), integer()}} | + {ip, iprange()} | {node_glob, {glob(), glob()}}. -type acl() :: #acl{aclname :: aclname(), @@ -204,6 +207,12 @@ load_from_config() -> end, AccessRules) end, Hosts). +%% Delete all previous set ACLs and Access rules +clear() -> + mnesia:clear_table(acl), + mnesia:clear_table(access), + ok. + b(S) -> iolist_to_binary(S). @@ -246,6 +255,19 @@ normalize_spec(Spec) -> end end. +-spec match_access(global | binary(), access_name(), + jid() | ljid() | inet:ip_address(), + atom()) -> any(). + +match_access(_Host, all, _JID, _Default) -> + allow; +match_access(_Host, none, _JID, _Default) -> + deny; +match_access(_Host, {user, UserPattern}, JID, Default) -> + match_user_spec({user, UserPattern}, JID, Default); +match_access(Host, AccessRule, JID, _Default) -> + match_rule(Host, AccessRule, JID). + -spec match_rule(global | binary(), access_name(), jid() | ljid() | inet:ip_address()) -> any(). @@ -348,6 +370,16 @@ match_acl(ACL, JID, Host) -> get_aclspecs(ACL, Host) -> ets:lookup(acl, {ACL, Host}) ++ ets:lookup(acl, {ACL, global}). + +match_user_spec(Spec, JID, Default) -> + case do_match_user_spec(Spec, jid:tolower(JID)) of + true -> Default; + false -> deny + end. + +do_match_user_spec({user, {U, S}}, {User, Server, _Resource}) -> + U == User andalso S == Server. + is_regexp_match(String, RegExp) -> case ejabberd_regexp:run(String, RegExp) of nomatch -> false; diff --git a/src/adhoc.erl b/src/adhoc.erl index d252a6cbb..83a113a0c 100644 --- a/src/adhoc.erl +++ b/src/adhoc.erl @@ -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 @@ -68,7 +68,9 @@ parse_request(#iq{type = set, lang = Lang, sub_el = SubEl, xmlns = ?NS_COMMANDS} xdata = XData, others = Others }; -parse_request(_) -> {error, ?ERR_BAD_REQUEST}. +parse_request(#iq{lang = Lang}) -> + Text = <<"Failed to parse ad-hoc command request">>, + {error, ?ERRT_BAD_REQUEST(Lang, Text)}. %% Borrowed from mod_vcard.erl find_xdata_el(#xmlel{children = SubEls}) -> @@ -76,7 +78,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; diff --git a/src/cyrsasl.erl b/src/cyrsasl.erl index 5d2d89d67..21fbc9660 100644 --- a/src/cyrsasl.erl +++ b/src/cyrsasl.erl @@ -111,12 +111,12 @@ register_mechanism(Mechanism, Module, PasswordType) -> %%-include("ejabberd.hrl"). %%-include("jlib.hrl"). %%check_authzid(_State, Props) -> -%% AuthzId = xml:get_attr_s(authzid, Props), +%% AuthzId = fxml:get_attr_s(authzid, Props), %% case jid:from_string(AuthzId) of %% error -> %% {error, "invalid-authzid"}; %% JID -> -%% LUser = jid:nodeprep(xml:get_attr_s(username, Props)), +%% LUser = jid:nodeprep(fxml:get_attr_s(username, Props)), %% {U, S, R} = jid:tolower(JID), %% case R of %% "" -> @@ -132,7 +132,7 @@ register_mechanism(Mechanism, Module, PasswordType) -> %% end. check_credentials(_State, Props) -> - User = proplists:get_value(username, Props, <<>>), + User = proplists:get_value(authzid, Props, <<>>), case jid:nodeprep(User) of error -> {error, <<"not-authorized">>}; <<"">> -> {error, <<"not-authorized">>}; diff --git a/src/cyrsasl_anonymous.erl b/src/cyrsasl_anonymous.erl index 2b2a9f63c..802e1cd7b 100644 --- a/src/cyrsasl_anonymous.erl +++ b/src/cyrsasl_anonymous.erl @@ -45,9 +45,8 @@ mech_new(Host, _GetPassword, _CheckPassword, _CheckPasswordDigest) -> mech_step(#state{server = Server} = S, ClientIn) -> User = iolist_to_binary([randoms:get_string(), - randoms:get_string(), - randoms:get_string()]), + jlib:integer_to_binary(p1_time_compat:unique_integer([positive]))]), case ejabberd_auth:is_user_exists(User, Server) of true -> mech_step(S, ClientIn); - false -> {ok, [{username, User}, {auth_module, ejabberd_auth_anonymous}]} + false -> {ok, [{username, User}, {authzid, User}, {auth_module, ejabberd_auth_anonymous}]} end. diff --git a/src/cyrsasl_digest.erl b/src/cyrsasl_digest.erl index 8ccd90952..e58cb3035 100644 --- a/src/cyrsasl_digest.erl +++ b/src/cyrsasl_digest.erl @@ -50,7 +50,7 @@ username = <<"">> :: binary(), authzid = <<"">> :: binary(), get_password = fun(_) -> {false, <<>>} end :: get_password_fun(), - check_password = fun(_, _, _, _) -> false end :: check_password_fun(), + check_password = fun(_, _, _, _, _) -> false end :: check_password_fun(), auth_module :: atom(), host = <<"">> :: binary(), hostfqdn = <<"">> :: binary()}). @@ -83,9 +83,7 @@ mech_step(#state{step = 3, nonce = Nonce} = State, bad -> {error, <<"bad-protocol">>}; KeyVals -> DigestURI = proplists:get_value(<<"digest-uri">>, KeyVals, <<>>), - %DigestURI = xml:get_attr_s(<<"digest-uri">>, KeyVals), UserName = proplists:get_value(<<"username">>, KeyVals, <<>>), - %UserName = xml:get_attr_s(<<"username">>, KeyVals), case is_digesturi_valid(DigestURI, State#state.host, State#state.hostfqdn) of @@ -97,13 +95,11 @@ mech_step(#state{step = 3, nonce = Nonce} = State, {error, <<"not-authorized">>, UserName}; true -> AuthzId = proplists:get_value(<<"authzid">>, KeyVals, <<>>), - %AuthzId = xml:get_attr_s(<<"authzid">>, KeyVals), case (State#state.get_password)(UserName) of {false, _} -> {error, <<"not-authorized">>, UserName}; {Passwd, AuthModule} -> - case (State#state.check_password)(UserName, <<"">>, + case (State#state.check_password)(UserName, UserName, <<"">>, proplists:get_value(<<"response">>, KeyVals, <<>>), - %xml:get_attr_s(<<"response">>, KeyVals), fun (PW) -> response(KeyVals, UserName, @@ -130,7 +126,11 @@ mech_step(#state{step = 5, auth_module = AuthModule, username = UserName, authzid = AuthzId}, <<"">>) -> {ok, - [{username, UserName}, {authzid, AuthzId}, + [{username, UserName}, {authzid, case AuthzId of + <<"">> -> UserName; + _ -> AuthzId + end + }, {auth_module, AuthModule}]}; mech_step(A, B) -> ?DEBUG("SASL DIGEST: A ~p B ~p", [A, B]), diff --git a/src/cyrsasl_plain.erl b/src/cyrsasl_plain.erl index a9c1de056..82d68f87f 100644 --- a/src/cyrsasl_plain.erl +++ b/src/cyrsasl_plain.erl @@ -45,7 +45,7 @@ mech_new(_Host, _GetPassword, CheckPassword, _CheckPasswordDigest) -> mech_step(State, ClientIn) -> case prepare(ClientIn) of [AuthzId, User, Password] -> - case (State#state.check_password)(User, Password) of + case (State#state.check_password)(User, AuthzId, Password) of {true, AuthModule} -> {ok, [{username, User}, {authzid, AuthzId}, @@ -60,12 +60,17 @@ prepare(ClientIn) -> [<<"">>, UserMaybeDomain, Password] -> case parse_domain(UserMaybeDomain) of %% <NUL>login@domain<NUL>pwd - [User, _Domain] -> [UserMaybeDomain, User, Password]; + [User, _Domain] -> [User, User, Password]; %% <NUL>login<NUL>pwd - [User] -> [<<"">>, User, Password] + [User] -> [User, User, Password] end; + [AuthzId, User, Password] -> + case parse_domain(AuthzId) of %% login@domain<NUL>login<NUL>pwd - [AuthzId, User, Password] -> [AuthzId, User, Password]; + [AuthzUser, _Domain] -> [AuthzUser, User, Password]; + %% login<NUL>login<NUL>pwd + [AuthzUser] -> [AuthzUser, User, Password] + end; _ -> error end. diff --git a/src/cyrsasl_scram.erl b/src/cyrsasl_scram.erl index 059938f5e..18f52b48f 100644 --- a/src/cyrsasl_scram.erl +++ b/src/cyrsasl_scram.erl @@ -159,7 +159,8 @@ mech_step(#state{step = 4} = State, ClientIn) -> ServerSignature = scram:server_signature(State#state.server_key, AuthMessage), - {ok, [{username, State#state.username}], + {ok, [{username, State#state.username}, + {authzid, State#state.username}], <<"v=", (jlib:encode_base64(ServerSignature))/binary>>}; true -> {error, <<"bad-auth">>, State#state.username} diff --git a/src/ejabberd.erl b/src/ejabberd.erl index e1b92c26b..6bd2422ae 100644 --- a/src/ejabberd.erl +++ b/src/ejabberd.erl @@ -79,7 +79,7 @@ is_loaded() -> start_app(App, Type, StartFlag) when not is_list(App) -> start_app([App], Type, StartFlag); start_app([App|Apps], Type, StartFlag) -> - case application:start(App) of + case application:start(App,Type) of ok -> spawn(fun() -> check_app_modules(App, StartFlag) end), start_app(Apps, Type, StartFlag); diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl index 4aa5faedb..a22f8f3d3 100644 --- a/src/ejabberd_admin.erl +++ b/src/ejabberd_admin.erl @@ -192,15 +192,24 @@ get_commands_spec() -> module = ejabberd_piefxis, function = export_host, args = [{dir, string}, {host, string}], result = {res, rescode}}, - #ejabberd_commands{name = export_odbc, tags = [mnesia, odbc], + #ejabberd_commands{name = export_sql, tags = [mnesia, sql], desc = "Export all tables as SQL queries to a file", - module = ejd2odbc, function = export, + module = ejd2sql, function = export, args = [{host, string}, {file, string}], result = {res, rescode}}, - #ejabberd_commands{name = convert_to_scram, tags = [odbc], + #ejabberd_commands{name = delete_mnesia, tags = [mnesia, sql], + desc = "Export all tables as SQL queries to a file", + module = ejd2sql, function = delete, + args = [{host, string}], result = {res, rescode}}, + #ejabberd_commands{name = convert_to_scram, tags = [sql], desc = "Convert the passwords in 'users' ODBC table to SCRAM", - module = ejabberd_auth_odbc, function = convert_to_scram, + module = ejabberd_auth_sql, function = convert_to_scram, args = [{host, binary}], result = {res, rescode}}, + #ejabberd_commands{name = import_prosody, tags = [mnesia, sql, 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", module = ejabberd_config, function = convert_to_yaml, @@ -216,9 +225,9 @@ get_commands_spec() -> module = ?MODULE, function = delete_old_messages, args = [{days, integer}], result = {res, rescode}}, - #ejabberd_commands{name = export2odbc, tags = [mnesia], + #ejabberd_commands{name = export2sql, tags = [mnesia], desc = "Export virtual host information from Mnesia tables to SQL files", - module = ejd2odbc, function = export, + module = ejd2sql, function = export, args = [{host, string}, {directory, string}], result = {res, rescode}}, #ejabberd_commands{name = set_master, tags = [mnesia], diff --git a/src/ejabberd_app.erl b/src/ejabberd_app.erl index 628b4a8ad..b25bf231b 100644 --- a/src/ejabberd_app.erl +++ b/src/ejabberd_app.erl @@ -30,7 +30,7 @@ -behaviour(application). --export([start_modules/0, start/2, prep_stop/1, stop/1, +-export([start/2, prep_stop/1, stop/1, init/0, opt_type/1]). -include("ejabberd.hrl"). @@ -71,7 +71,7 @@ start(normal, _Args) -> maybe_add_nameservers(), ejabberd_auth:start(), ejabberd_oauth:start(), - start_modules(), + gen_mod:start_modules(), ejabberd_listener:start_listeners(), ?INFO_MSG("ejabberd ~s is started in the node ~p", [?VERSION, node()]), Sup; @@ -83,7 +83,7 @@ start(_, _) -> %% before shutting down the processes of the application. prep_stop(State) -> ejabberd_listener:stop_listeners(), - stop_modules(), + gen_mod:stop_modules(), ejabberd_admin:stop(), broadcast_c2s_shutdown(), timer:sleep(5000), @@ -137,42 +137,6 @@ db_init() -> ejabberd:start_app(mnesia, permanent), mnesia:wait_for_tables(mnesia:system_info(local_tables), infinity). -%% Start all the modules in all the hosts -start_modules() -> - lists:foreach( - fun(Host) -> - Modules = ejabberd_config:get_option( - {modules, Host}, - fun(Mods) -> - lists:map( - fun({M, A}) when is_atom(M), is_list(A) -> - {M, A} - end, Mods) - end, []), - lists:foreach( - fun({Module, Args}) -> - gen_mod:start_module(Host, Module, Args) - end, Modules) - end, ?MYHOSTS). - -%% Stop all the modules in all the hosts -stop_modules() -> - lists:foreach( - fun(Host) -> - Modules = ejabberd_config:get_option( - {modules, Host}, - fun(Mods) -> - lists:map( - fun({M, A}) when is_atom(M), is_list(A) -> - {M, A} - end, Mods) - end, []), - lists:foreach( - fun({Module, _Args}) -> - gen_mod:stop_module_keep_config(Host, Module) - end, Modules) - end, ?MYHOSTS). - connect_nodes() -> Nodes = ejabberd_config:get_option( cluster_nodes, @@ -252,11 +216,10 @@ 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(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) -> diff --git a/src/ejabberd_auth.erl b/src/ejabberd_auth.erl index 2cc37c6e0..0267a2192 100644 --- a/src/ejabberd_auth.erl +++ b/src/ejabberd_auth.erl @@ -32,9 +32,9 @@ -author('alexey@process-one.net'). %% External exports --export([start/0, set_password/3, check_password/3, - check_password/5, check_password_with_authmodule/3, - check_password_with_authmodule/5, try_register/3, +-export([start/0, set_password/3, check_password/4, + check_password/6, check_password_with_authmodule/4, + check_password_with_authmodule/6, try_register/3, dirty_get_registered_users/0, get_vh_registered_users/1, get_vh_registered_users/2, export/1, import/1, get_vh_registered_users_number/1, import/3, @@ -63,8 +63,8 @@ -callback remove_user(binary(), binary()) -> any(). -callback remove_user(binary(), binary(), binary()) -> any(). -callback is_user_exists(binary(), binary()) -> boolean() | {error, atom()}. --callback check_password(binary(), binary(), binary()) -> boolean(). --callback check_password(binary(), binary(), binary(), binary(), +-callback check_password(binary(), binary(), binary(), binary()) -> boolean(). +-callback check_password(binary(), binary(), binary(), binary(), binary(), fun((binary()) -> binary())) -> boolean(). -callback try_register(binary(), binary(), binary()) -> {atomic, atom()} | {error, atom()}. @@ -102,10 +102,10 @@ store_type(Server) -> end, plain, auth_modules(Server)). --spec check_password(binary(), binary(), binary()) -> boolean(). +-spec check_password(binary(), binary(), binary(), binary()) -> boolean(). -check_password(User, Server, Password) -> - case check_password_with_authmodule(User, Server, +check_password(User, AuthzId, Server, Password) -> + case check_password_with_authmodule(User, AuthzId, Server, Password) of {true, _AuthModule} -> true; @@ -113,15 +113,15 @@ check_password(User, Server, Password) -> end. %% @doc Check if the user and password can login in server. -%% @spec (User::string(), Server::string(), Password::string(), +%% @spec (User::string(), AuthzId::string(), Server::string(), Password::string(), %% Digest::string(), DigestGen::function()) -> %% true | false --spec check_password(binary(), binary(), binary(), binary(), +-spec check_password(binary(), binary(), binary(), binary(), binary(), fun((binary()) -> binary())) -> boolean(). -check_password(User, Server, Password, Digest, +check_password(User, AuthzId, Server, Password, Digest, DigestGen) -> - case check_password_with_authmodule(User, Server, + case check_password_with_authmodule(User, AuthzId, Server, Password, Digest, DigestGen) of {true, _AuthModule} -> true; @@ -132,28 +132,28 @@ check_password(User, Server, Password, Digest, %% The user can login if at least an authentication method accepts the user %% and the password. %% The first authentication method that accepts the credentials is returned. -%% @spec (User::string(), Server::string(), Password::string()) -> +%% @spec (User::string(), AuthzId::string(), Server::string(), Password::string()) -> %% {true, AuthModule} | false %% where %% AuthModule = ejabberd_auth_anonymous | ejabberd_auth_external %% | ejabberd_auth_internal | ejabberd_auth_ldap -%% | ejabberd_auth_odbc | ejabberd_auth_pam --spec check_password_with_authmodule(binary(), binary(), binary()) -> false | +%% | ejabberd_auth_sql | ejabberd_auth_pam | ejabberd_auth_riak +-spec check_password_with_authmodule(binary(), binary(), binary(), binary()) -> false | {true, atom()}. -check_password_with_authmodule(User, Server, +check_password_with_authmodule(User, AuthzId, Server, Password) -> check_password_loop(auth_modules(Server), - [User, Server, Password]). + [User, AuthzId, Server, Password]). --spec check_password_with_authmodule(binary(), binary(), binary(), binary(), +-spec check_password_with_authmodule(binary(), binary(), binary(), binary(), binary(), fun((binary()) -> binary())) -> false | {true, atom()}. -check_password_with_authmodule(User, Server, Password, +check_password_with_authmodule(User, AuthzId, Server, Password, Digest, DigestGen) -> check_password_loop(auth_modules(Server), - [User, Server, Password, Digest, DigestGen]). + [User, AuthzId, Server, Password, Digest, DigestGen]). check_password_loop([], _Args) -> false; check_password_loop([AuthModule | AuthModules], Args) -> diff --git a/src/ejabberd_auth_anonymous.erl b/src/ejabberd_auth_anonymous.erl index 7b21752a2..5a5b395bf 100644 --- a/src/ejabberd_auth_anonymous.erl +++ b/src/ejabberd_auth_anonymous.erl @@ -38,8 +38,8 @@ unregister_connection/3 ]). --export([login/2, set_password/3, check_password/3, - check_password/5, try_register/3, +-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, @@ -56,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 = {p1_time_compat:timestamp(), self()} :: ejabberd_sm:sid()}). + sid = ejabberd_sm:make_sid() :: ejabberd_sm:sid()}). start(Host) -> %% TODO: Check cluster mode @@ -175,11 +175,11 @@ purge_hook(true, LUser, LServer) -> %% When anonymous login is enabled, check the password for permenant users %% before allowing access -check_password(User, Server, Password) -> - check_password(User, Server, Password, undefined, +check_password(User, AuthzId, Server, Password) -> + check_password(User, AuthzId, Server, Password, undefined, undefined). -check_password(User, Server, _Password, _Digest, +check_password(User, _AuthzId, Server, _Password, _Digest, _DigestGen) -> case ejabberd_auth:is_user_exists_in_other_modules(?MODULE, diff --git a/src/ejabberd_auth_external.erl b/src/ejabberd_auth_external.erl index 2a1cbf085..5897fba5b 100644 --- a/src/ejabberd_auth_external.erl +++ b/src/ejabberd_auth_external.erl @@ -31,8 +31,8 @@ -behaviour(ejabberd_auth). --export([start/1, set_password/3, check_password/3, - check_password/5, try_register/3, +-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, @@ -76,16 +76,20 @@ plain_password_required() -> true. store_type() -> external. -check_password(User, Server, Password) -> +check_password(User, AuthzId, Server, Password) -> + if AuthzId /= <<>> andalso AuthzId /= User -> + false; + true -> case get_cache_option(Server) of - false -> check_password_extauth(User, Server, Password); + false -> check_password_extauth(User, AuthzId, Server, Password); {true, CacheTime} -> - check_password_cache(User, Server, Password, CacheTime) + check_password_cache(User, AuthzId, Server, Password, CacheTime) + end end. -check_password(User, Server, Password, _Digest, +check_password(User, AuthzId, Server, Password, _Digest, _DigestGen) -> - check_password(User, Server, Password). + check_password(User, AuthzId, Server, Password). set_password(User, Server, Password) -> case extauth:set_password(User, Server, Password) of @@ -178,8 +182,8 @@ get_cache_option(Host) -> CacheTime -> {true, CacheTime} end. -%% @spec (User, Server, Password) -> true | false -check_password_extauth(User, Server, Password) -> +%% @spec (User, AuthzId, Server, Password) -> true | false +check_password_extauth(User, _AuthzId, Server, Password) -> extauth:check_password(User, Server, Password) andalso Password /= <<"">>. @@ -187,42 +191,42 @@ check_password_extauth(User, Server, Password) -> try_register_extauth(User, Server, Password) -> extauth:try_register(User, Server, Password). -check_password_cache(User, Server, Password, 0) -> - check_password_external_cache(User, Server, Password); -check_password_cache(User, Server, Password, +check_password_cache(User, AuthzId, Server, Password, 0) -> + check_password_external_cache(User, AuthzId, Server, Password); +check_password_cache(User, AuthzId, Server, Password, CacheTime) -> case get_last_access(User, Server) of online -> - check_password_internal(User, Server, Password); + check_password_internal(User, AuthzId, Server, Password); never -> - check_password_external_cache(User, Server, Password); + check_password_external_cache(User, AuthzId, Server, Password); mod_last_required -> ?ERROR_MSG("extauth is used, extauth_cache is enabled " "but mod_last is not enabled in that " "host", []), - check_password_external_cache(User, Server, Password); + check_password_external_cache(User, AuthzId, Server, Password); TimeStamp -> case is_fresh_enough(TimeStamp, CacheTime) of %% If no need to refresh, check password against Mnesia true -> - case check_password_internal(User, Server, Password) of + case check_password_internal(User, AuthzId, Server, Password) of %% If password valid in Mnesia, accept it true -> true; %% Else (password nonvalid in Mnesia), check in extauth and cache result false -> - check_password_external_cache(User, Server, Password) + check_password_external_cache(User, AuthzId, Server, Password) end; %% Else (need to refresh), check in extauth and cache result false -> - check_password_external_cache(User, Server, Password) + check_password_external_cache(User, AuthzId, Server, Password) end end. 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); @@ -241,8 +245,8 @@ get_password_cache(User, Server, CacheTime) -> end. %% Check the password using extauth; if success then cache it -check_password_external_cache(User, Server, Password) -> - case check_password_extauth(User, Server, Password) of +check_password_external_cache(User, AuthzId, Server, Password) -> + case check_password_extauth(User, AuthzId, Server, Password) of true -> set_password_internal(User, Server, Password), true; false -> false @@ -256,9 +260,9 @@ try_register_external_cache(User, Server, Password) -> _ -> {error, not_allowed} end. -%% @spec (User, Server, Password) -> true | false -check_password_internal(User, Server, Password) -> - ejabberd_auth_internal:check_password(User, Server, +%% @spec (User, AuthzId, Server, Password) -> true | false +check_password_internal(User, AuthzId, Server, Password) -> + ejabberd_auth_internal:check_password(User, AuthzId, Server, Password). %% @spec (User, Server, Password) -> ok | {error, invalid_jid} @@ -273,10 +277,10 @@ is_fresh_enough(TimeStampLast, CacheTime) -> 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 [] -> diff --git a/src/ejabberd_auth_internal.erl b/src/ejabberd_auth_internal.erl index d60e0fc5f..acbbfe506 100644 --- a/src/ejabberd_auth_internal.erl +++ b/src/ejabberd_auth_internal.erl @@ -31,8 +31,8 @@ -behaviour(ejabberd_auth). --export([start/1, set_password/3, check_password/3, - check_password/5, try_register/3, +-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, @@ -86,9 +86,12 @@ store_type() -> true -> scram %% allows: PLAIN SCRAM end. -check_password(User, Server, Password) -> - LUser = jid:nodeprep(User), - LServer = jid:nameprep(Server), +check_password(User, AuthzId, Server, Password) -> + if AuthzId /= <<>> andalso AuthzId /= User -> + false; + true -> + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), US = {LUser, LServer}, case catch mnesia:dirty_read({passwd, US}) of [#passwd{password = Password}] @@ -98,12 +101,16 @@ check_password(User, Server, Password) -> when is_record(Scram, scram) -> is_password_scram_valid(Password, Scram); _ -> false + end end. -check_password(User, Server, Password, Digest, +check_password(User, AuthzId, Server, Password, Digest, DigestGen) -> - LUser = jid:nodeprep(User), - LServer = jid:nameprep(Server), + if AuthzId /= <<>> andalso AuthzId /= User -> + false; + true -> + 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) -> @@ -125,6 +132,7 @@ check_password(User, Server, Password, Digest, true -> (Passwd == Password) and (Password /= <<"">>) end; _ -> false + end end. %% @spec (User::string(), Server::string(), Password::string()) -> @@ -466,8 +474,8 @@ export(_Server) -> [{passwd, fun(Host, #passwd{us = {LUser, LServer}, password = Password}) when LServer == Host -> - Username = ejabberd_odbc:escape(LUser), - Pass = ejabberd_odbc:escape(Password), + Username = ejabberd_sql:escape(LUser), + Pass = ejabberd_sql:escape(Password), [[<<"delete from users where username='">>, Username, <<"';">>], [<<"insert into users(username, password) " "values ('">>, Username, <<"', '">>, Pass, <<"');">>]]; diff --git a/src/ejabberd_auth_ldap.erl b/src/ejabberd_auth_ldap.erl index dd5d54a74..51b466ef4 100644 --- a/src/ejabberd_auth_ldap.erl +++ b/src/ejabberd_auth_ldap.erl @@ -37,7 +37,7 @@ handle_cast/2, terminate/2, code_change/3]). -export([start/1, stop/1, start_link/1, set_password/3, - check_password/3, check_password/5, try_register/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, @@ -116,7 +116,10 @@ plain_password_required() -> true. store_type() -> external. -check_password(User, Server, Password) -> +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) @@ -124,11 +127,12 @@ check_password(User, Server, Password) -> {'EXIT', _} -> false; Result -> Result end + end end. -check_password(User, Server, Password, _Digest, +check_password(User, AuthzId, Server, Password, _Digest, _DigestGen) -> - check_password(User, Server, Password). + check_password(User, AuthzId, Server, Password). set_password(User, Server, Password) -> {ok, State} = eldap_utils:get_state(Server, ?MODULE), diff --git a/src/ejabberd_auth_pam.erl b/src/ejabberd_auth_pam.erl index ee5123c5e..fa4b9f078 100644 --- a/src/ejabberd_auth_pam.erl +++ b/src/ejabberd_auth_pam.erl @@ -30,8 +30,8 @@ -behaviour(ejabberd_auth). --export([start/1, set_password/3, check_password/3, - check_password/5, try_register/3, +-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, @@ -46,11 +46,14 @@ start(_Host) -> set_password(_User, _Server, _Password) -> {error, not_allowed}. -check_password(User, Server, Password, _Digest, +check_password(User, AuthzId, Server, Password, _Digest, _DigestGen) -> - check_password(User, Server, Password). + check_password(User, AuthzId, Server, Password). -check_password(User, Host, Password) -> +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; @@ -61,6 +64,7 @@ check_password(User, Host, Password) -> of true -> true; _ -> false + end end. try_register(_User, _Server, _Password) -> diff --git a/src/ejabberd_auth_riak.erl b/src/ejabberd_auth_riak.erl index 64bf12c6a..c48b94410 100644 --- a/src/ejabberd_auth_riak.erl +++ b/src/ejabberd_auth_riak.erl @@ -30,8 +30,8 @@ -behaviour(ejabberd_auth). %% External exports --export([start/1, set_password/3, check_password/3, - check_password/5, try_register/3, +-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, @@ -66,9 +66,12 @@ store_type() -> passwd_schema() -> {record_info(fields, passwd), #passwd{}}. -check_password(User, Server, Password) -> - LUser = jid:nodeprep(User), - LServer = jid:nameprep(Server), +check_password(User, AuthzId, Server, Password) -> + if AuthzId /= <<>> andalso AuthzId /= User -> + false; + true -> + 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 /= <<"">>; @@ -76,12 +79,16 @@ check_password(User, Server, Password) -> is_password_scram_valid(Password, Scram); _ -> false + end end. -check_password(User, Server, Password, Digest, +check_password(User, AuthzId, Server, Password, Digest, DigestGen) -> - LUser = jid:nodeprep(User), - LServer = jid:nameprep(Server), + if AuthzId /= <<>> andalso AuthzId /= User -> + false; + true -> + 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 /= <<"">> -> @@ -102,6 +109,7 @@ check_password(User, Server, Password, Digest, true -> (Passwd == Password) and (Password /= <<"">>) end; _ -> false + end end. set_password(User, Server, Password) -> @@ -283,8 +291,8 @@ export(_Server) -> [{passwd, fun(Host, #passwd{us = {LUser, LServer}, password = Password}) when LServer == Host -> - Username = ejabberd_odbc:escape(LUser), - Pass = ejabberd_odbc:escape(Password), + Username = ejabberd_sql:escape(LUser), + Pass = ejabberd_sql:escape(Password), [[<<"delete from users where username='">>, Username, <<"';">>], [<<"insert into users(username, password) " "values ('">>, Username, <<"', '">>, Pass, <<"');">>]]; diff --git a/src/ejabberd_auth_odbc.erl b/src/ejabberd_auth_sql.erl index b8b4594b6..000b4a4f4 100644 --- a/src/ejabberd_auth_odbc.erl +++ b/src/ejabberd_auth_sql.erl @@ -1,5 +1,5 @@ %%%---------------------------------------------------------------------- -%%% File : ejabberd_auth_odbc.erl +%%% File : ejabberd_auth_sql.erl %%% Author : Alexey Shchepin <alexey@process-one.net> %%% Purpose : Authentification via ODBC %%% Created : 12 Dec 2004 by Alexey Shchepin <alexey@process-one.net> @@ -23,7 +23,7 @@ %%% %%%---------------------------------------------------------------------- --module(ejabberd_auth_odbc). +-module(ejabberd_auth_sql). -behaviour(ejabberd_config). @@ -31,8 +31,8 @@ -behaviour(ejabberd_auth). --export([start/1, set_password/3, check_password/3, - check_password/5, try_register/3, +-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, @@ -63,31 +63,30 @@ store_type() -> true -> scram %% allows: PLAIN SCRAM end. -%% @spec (User, Server, Password) -> true | false | {error, Error} -check_password(User, Server, Password) -> - LServer = jid:nameprep(Server), - LUser = jid:nodeprep(User), +%% @spec (User, AuthzId, Server, Password) -> true | false | {error, Error} +check_password(User, AuthzId, Server, Password) -> + if AuthzId /= <<>> andalso AuthzId /= User -> + false; + true -> + 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 -> - try odbc_queries:get_password_scram(LServer, Username) of - {selected, [<<"password">>, <<"serverkey">>, - <<"salt">>, <<"iterationcount">>], - [[StoredKey, ServerKey, Salt, IterationCount]]} -> + try sql_queries:get_password_scram(LServer, LUser) of + {selected, + [{StoredKey, ServerKey, Salt, IterationCount}]} -> Scram = #scram{storedkey = StoredKey, serverkey = ServerKey, salt = Salt, - iterationcount = binary_to_integer( - IterationCount)}, + iterationcount = IterationCount}, is_password_scram_valid(Password, Scram); - {selected, [<<"password">>, <<"serverkey">>, - <<"salt">>, <<"iterationcount">>], []} -> + {selected, []} -> false; %% Account does not exist {error, _Error} -> false %% Typical error is that table doesn't exist @@ -96,12 +95,12 @@ check_password(User, Server, Password) -> false %% Typical error is database not accessible end; false -> - try odbc_queries:get_password(LServer, Username) of - {selected, [<<"password">>], [[Password]]} -> + try sql_queries:get_password(LServer, LUser) of + {selected, [{Password}]} -> Password /= <<"">>; - {selected, [<<"password">>], [[_Password2]]} -> + {selected, [{_Password2}]} -> false; %% Password is not correct - {selected, [<<"password">>], []} -> + {selected, []} -> false; %% Account does not exist {error, _Error} -> false %% Typical error is that table doesn't exist @@ -110,13 +109,17 @@ check_password(User, Server, Password) -> false %% Typical error is database not accessible end end + end end. -%% @spec (User, Server, Password, Digest, DigestGen) -> true | false | {error, Error} -check_password(User, Server, Password, Digest, +%% @spec (User, AuthzId, Server, Password, Digest, DigestGen) -> true | false | {error, Error} +check_password(User, AuthzId, Server, Password, Digest, DigestGen) -> - LServer = jid:nameprep(Server), - LUser = jid:nodeprep(User), + if AuthzId /= <<>> andalso AuthzId /= User -> + false; + true -> + LServer = jid:nameprep(Server), + LUser = jid:nodeprep(User), if (LUser == error) or (LServer == error) -> false; (LUser == <<>>) or (LServer == <<>>) -> @@ -124,10 +127,9 @@ check_password(User, Server, Password, Digest, true -> case is_scrammed() of false -> - Username = ejabberd_odbc:escape(LUser), - try odbc_queries:get_password(LServer, Username) of + try sql_queries:get_password(LServer, LUser) of %% Account exists, check if password is valid - {selected, [<<"password">>], [[Passwd]]} -> + {selected, [{Passwd}]} -> DigRes = if Digest /= <<"">> -> Digest == DigestGen(Passwd); true -> false @@ -135,7 +137,7 @@ check_password(User, Server, Password, Digest, if DigRes -> true; true -> (Passwd == Password) and (Password /= <<"">>) end; - {selected, [<<"password">>], []} -> + {selected, []} -> false; %% Account does not exist {error, _Error} -> false %% Typical error is that table doesn't exist @@ -146,6 +148,7 @@ check_password(User, Server, Password, Digest, true -> false end + end end. %% @spec (User::string(), Server::string(), Password::string()) -> @@ -158,26 +161,24 @@ set_password(User, Server, Password) -> (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( + case catch sql_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), - 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) + case catch sql_queries:set_password_t(LServer, + LUser, Password) of {atomic, ok} -> ok; Other -> {error, Other} @@ -194,26 +195,23 @@ try_register(User, Server, Password) -> (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( + case catch sql_queries:add_user_scram( LServer, - Username, - ejabberd_odbc:escape(Scram#scram.storedkey), - ejabberd_odbc:escape(Scram#scram.serverkey), - ejabberd_odbc:escape(Scram#scram.salt), - 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 sql_queries:add_user(LServer, LUser, + Password) of {updated, 1} -> {atomic, ok}; _ -> {atomic, exists} end @@ -221,42 +219,58 @@ try_register(User, Server, Password) -> end. dirty_get_registered_users() -> - Servers = ejabberd_config:get_vh_by_auth_method(odbc), + Servers = ejabberd_config:get_vh_by_auth_method(sql), lists:flatmap(fun (Server) -> get_vh_registered_users(Server) end, Servers). get_vh_registered_users(Server) -> - LServer = jid: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 sql_queries:list_users(LServer) of + {selected, Res} -> + [{U, LServer} || {U} <- Res]; + _ -> [] + end end. get_vh_registered_users(Server, Opts) -> - LServer = jid: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 sql_queries:list_users(LServer, Opts) of + {selected, Res} -> + [{U, LServer} || {U} <- Res]; + _ -> [] + end end. get_vh_registered_users_number(Server) -> - LServer = jid: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 sql_queries:users_number(LServer) of + {selected, [{Res}]} -> + Res; + _ -> 0 + end end. get_vh_registered_users_number(Server, Opts) -> - LServer = jid: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 sql_queries:users_number(LServer, Opts) of + {selected, [{Res}]} -> + Res; + _Other -> 0 + end end. get_password(User, Server) -> @@ -267,24 +281,22 @@ get_password(User, Server) -> (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]]} -> + case catch sql_queries:get_password_scram( + LServer, LUser) of + {selected, + [{StoredKey, ServerKey, Salt, IterationCount}]} -> {jlib:decode_base64(StoredKey), jlib:decode_base64(ServerKey), jlib:decode_base64(Salt), - binary_to_integer(IterationCount)}; + IterationCount}; _ -> false end; false -> - case catch odbc_queries:get_password(LServer, Username) + case catch sql_queries:get_password(LServer, LUser) of - {selected, [<<"password">>], [[Password]]} -> Password; + {selected, [{Password}]} -> Password; _ -> false end end @@ -300,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 sql_queries:get_password(LServer, LUser) of + {selected, [{Password}]} -> Password; _ -> <<"">> end; true -> <<"">> @@ -311,15 +322,17 @@ get_password_s(User, Server) -> %% @spec (User, Server) -> true | false | {error, Error} is_user_exists(User, Server) -> - case jid:nodeprep(User) of - error -> false; - LUser -> - Username = ejabberd_odbc:escape(LUser), - LServer = jid: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 sql_queries:get_password(LServer, LUser) of + {selected, [{_Password}]} -> true; %% Account exists - {selected, [<<"password">>], []} -> + {selected, []} -> false; %% Account does not exist {error, Error} -> {error, Error} catch @@ -331,12 +344,14 @@ 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 jid:nodeprep(User) of - error -> error; - LUser -> - Username = ejabberd_odbc:escape(LUser), - LServer = jid: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 sql_queries:del_user(LServer, LUser), ok end. @@ -352,27 +367,23 @@ remove_user(User, Server, Password) -> true -> case is_scrammed() of true -> - case check_password(User, Server, Password) of + case check_password(User, <<"">>, Server, Password) of true -> remove_user(User, Server), ok; 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), + Result = sql_queries:del_user_return_password( + LServer, LUser, Password), case Result of - {selected, [<<"password">>], - [[Password]]} -> ok; - {selected, [<<"password">>], - []} -> not_exists; + {selected, [{Password}]} -> ok; + {selected, []} -> not_exists; _ -> not_allowed end end, - {atomic, Result} = odbc_queries:sql_transaction( + {atomic, Result} = sql_queries:sql_transaction( LServer, F), Result end @@ -416,7 +427,7 @@ is_password_scram_valid(Password, Scram) -> set_password_scram_t(Username, StoredKey, ServerKey, Salt, IterationCount) -> - odbc_queries:update_t(<<"users">>, + sql_queries:update_t(<<"users">>, [<<"username">>, <<"password">>, <<"serverkey">>, @@ -436,7 +447,7 @@ convert_to_scram(Server) -> {error, {incorrect_server_name, Server}}; true -> F = fun () -> - case ejabberd_odbc:sql_query_t( + case ejabberd_sql:sql_query_t( [<<"select username, password from users where " "iterationcount=0 limit ">>, integer_to_binary(?BATCH_SIZE), @@ -446,13 +457,13 @@ convert_to_scram(Server) -> {selected, [<<"username">>, <<"password">>], Rs} -> lists:foreach( fun([LUser, Password]) -> - Username = ejabberd_odbc:escape(LUser), + Username = ejabberd_sql:escape(LUser), Scram = password_to_scram(Password), set_password_scram_t( Username, - ejabberd_odbc:escape(Scram#scram.storedkey), - ejabberd_odbc:escape(Scram#scram.serverkey), - ejabberd_odbc:escape(Scram#scram.salt), + ejabberd_sql:escape(Scram#scram.storedkey), + ejabberd_sql:escape(Scram#scram.serverkey), + ejabberd_sql:escape(Scram#scram.salt), integer_to_binary(Scram#scram.iterationcount) ) end, Rs), @@ -460,7 +471,7 @@ convert_to_scram(Server) -> Err -> {bad_reply, Err} end end, - case odbc_queries:sql_transaction(LServer, F) of + case sql_queries:sql_transaction(LServer, F) of {atomic, ok} -> ok; {atomic, continue} -> convert_to_scram(Server); {atomic, Error} -> {error, Error}; diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl index 04e41f468..948e2027b 100644 --- a/src/ejabberd_c2s.erl +++ b/src/ejabberd_c2s.erl @@ -115,6 +115,7 @@ mgmt_resend, mgmt_stanzas_in = 0, mgmt_stanzas_out = 0, + ask_offline = true, lang = <<"">>}). %-define(DBGFSM, true). @@ -133,13 +134,13 @@ %% 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>">>). @@ -326,7 +327,7 @@ init([{SockMod, Socket}, Opts]) -> xml_socket = XMLSocket, zlib = Zlib, tls = TLS, tls_required = StartTLSRequired, tls_enabled = TLSEnabled, tls_options = TLSOpts, - sid = {p1_time_compat:timestamp(), self()}, streamid = new_id(), + sid = ejabberd_sm:make_sid(), streamid = new_id(), access = Access, shaper = Shaper, ip = IP, mgmt_state = StreamMgmtState, mgmt_max_queue = MaxAckQueue, @@ -342,15 +343,15 @@ get_subscribed(FsmRef) -> 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 <<"">> -> - jid:nameprep(xml:get_attr_s(<<"to">>, Attrs)); + jid:nameprep(fxml:get_attr_s(<<"to">>, Attrs)); S -> S end, - Lang = case xml:get_attr_s(<<"xml:lang">>, Attrs) of + 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 @@ -367,7 +368,7 @@ wait_for_stream({xmlstreamstart, _Name, Attrs}, StateData) -> case lists:member(Server, ?MYHOSTS) of true when IsBlacklistedIP == false -> change_shaper(StateData, jid:make(<<"">>, Server, <<"">>)), - case xml:get_attr_s(<<"version">>, Attrs) of + case fxml:get_attr_s(<<"version">>, Attrs) of <<"1.0">> -> send_header(StateData, Server, <<"1.0">>, DefaultLang), case StateData#state.authenticated of @@ -381,13 +382,13 @@ wait_for_stream({xmlstreamstart, _Name, Attrs}, StateData) -> ejabberd_auth:get_password_with_authmodule( U, Server) end, - fun (U, P) -> + fun(U, AuthzId, P) -> ejabberd_auth:check_password_with_authmodule( - U, Server, P) + U, AuthzId, Server, P) end, - fun (U, P, D, DG) -> + fun(U, AuthzId, P, D, DG) -> ejabberd_auth:check_password_with_authmodule( - U, Server, P, D, DG) + U, AuthzId, Server, P, D, DG) end), Mechs = case TLSEnabled or not TLSRequired of @@ -408,7 +409,7 @@ wait_for_stream({xmlstreamstart, _Name, Attrs}, StateData) -> (StateData#state.sockmod):get_sockmod(StateData#state.socket), Zlib = StateData#state.zlib, CompressFeature = case Zlib andalso - ((SockMod == gen_tcp) orelse (SockMod == p1_tls)) of + ((SockMod == gen_tcp) orelse (SockMod == fast_tls)) of true -> [#xmlel{name = <<"compression">>, attrs = [{<<"xmlns">>, ?NS_FEATURE_COMPRESS}], @@ -468,6 +469,22 @@ wait_for_stream({xmlstreamstart, _Name, Attrs}, StateData) -> false -> [] end, + 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 = []}, @@ -478,6 +495,7 @@ wait_for_stream({xmlstreamstart, _Name, Attrs}, StateData) -> ++ RosterVersioningFeature ++ StreamManagementFeature ++ + CompressFeature ++ ejabberd_hooks:run_fold(c2s_post_auth_features, Server, [], [Server]), StreamFeatures = ejabberd_hooks:run_fold(c2s_stream_features, @@ -504,7 +522,6 @@ wait_for_stream({xmlstreamstart, _Name, Attrs}, StateData) -> send_element(StateData, ?POLICY_VIOLATION_ERR(Lang, <<"Use of STARTTLS required">>)), - send_trailer(StateData), {stop, normal, StateData}; true -> fsm_next_state(wait_for_auth, @@ -519,34 +536,28 @@ wait_for_stream({xmlstreamstart, _Name, Attrs}, StateData) -> [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; _ -> send_header(StateData, ?MYNAME, <<"">>, DefaultLang), send_element(StateData, ?INVALID_NS_ERR), - send_trailer(StateData), {stop, normal, StateData} end; wait_for_stream(timeout, StateData) -> {stop, normal, StateData}; wait_for_stream({xmlstreamelement, _}, StateData) -> send_element(StateData, ?INVALID_XML_ERR), - send_trailer(StateData), {stop, normal, StateData}; wait_for_stream({xmlstreamend, _}, StateData) -> send_element(StateData, ?INVALID_XML_ERR), - send_trailer(StateData), {stop, normal, StateData}; wait_for_stream({xmlstreamerror, _}, StateData) -> send_header(StateData, ?MYNAME, <<"1.0">>, <<"">>), send_element(StateData, ?INVALID_XML_ERR), - send_trailer(StateData), {stop, normal, StateData}; wait_for_stream(closed, StateData) -> {stop, normal, StateData}; @@ -601,8 +612,9 @@ wait_for_auth({xmlstreamelement, El}, StateData) -> 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))), + Lang = StateData#state.lang, + Txt = <<"No resource provided">>, + Err = jlib:make_error_reply(El, ?ERRT_NOT_ACCEPTABLE(Lang, Txt)), send_element(StateData, Err), fsm_next_state(wait_for_auth, StateData); {auth, _ID, set, {U, P, D, R}} -> @@ -616,7 +628,7 @@ wait_for_auth({xmlstreamelement, El}, StateData) -> DGen = fun (PW) -> p1_sha:sha(<<(StateData#state.streamid)/binary, PW/binary>>) end, - case ejabberd_auth:check_password_with_authmodule(U, + case ejabberd_auth:check_password_with_authmodule(U, U, StateData#state.server, P, D, DGen) of @@ -667,7 +679,10 @@ wait_for_auth({xmlstreamelement, El}, StateData) -> 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), + Lang = StateData#state.lang, + Txt = <<"Legacy authentication failed">>, + Err = jlib:make_error_reply( + El, ?ERRT_NOT_AUTHORIZED(Lang, Txt)), send_element(StateData, Err), fsm_next_state(wait_for_auth, StateData) end; @@ -688,7 +703,9 @@ wait_for_auth({xmlstreamelement, El}, StateData) -> 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), + Lang = StateData#state.lang, + Txt = <<"Legacy authentication forbidden">>, + Err = jlib:make_error_reply(El, ?ERRT_NOT_ALLOWED(Lang, Txt)), send_element(StateData, Err), fsm_next_state(wait_for_auth, StateData) end @@ -700,10 +717,9 @@ wait_for_auth({xmlstreamelement, El}, StateData) -> wait_for_auth(timeout, StateData) -> {stop, normal, StateData}; wait_for_auth({xmlstreamend, _Name}, StateData) -> - send_trailer(StateData), {stop, normal, StateData}; + {stop, normal, StateData}; wait_for_auth({xmlstreamerror, _}, StateData) -> send_element(StateData, ?INVALID_XML_ERR), - send_trailer(StateData), {stop, normal, StateData}; wait_for_auth(closed, StateData) -> {stop, normal, StateData}; @@ -724,19 +740,17 @@ 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 {ok, Props} -> (StateData#state.sockmod):reset_stream(StateData#state.socket), - %U = xml:get_attr_s(username, Props), - U = proplists:get_value(username, Props, <<>>), - %AuthModule = xml:get_attr_s(auth_module, Props), + U = identity(Props), AuthModule = proplists:get_value(auth_module, Props, undefined), ?INFO_MSG("(~w) Accepted authentication for ~s " "by ~p from ~s", @@ -802,7 +816,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, @@ -813,46 +827,14 @@ 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, send_element(StateData, ?POLICY_VIOLATION_ERR(Lang, <<"Use of STARTTLS required">>)), - send_trailer(StateData), {stop, normal, StateData}; true -> process_unauthenticated_stanza(StateData, El), @@ -863,11 +845,10 @@ wait_for_feature_request(timeout, StateData) -> {stop, normal, StateData}; wait_for_feature_request({xmlstreamend, _Name}, StateData) -> - send_trailer(StateData), {stop, normal, StateData}; + {stop, normal, StateData}; wait_for_feature_request({xmlstreamerror, _}, StateData) -> send_element(StateData, ?INVALID_XML_ERR), - send_trailer(StateData), {stop, normal, StateData}; wait_for_feature_request(closed, StateData) -> {stop, normal, StateData}; @@ -880,18 +861,16 @@ 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 {ok, Props} -> catch (StateData#state.sockmod):reset_stream(StateData#state.socket), -% U = xml:get_attr_s(username, Props), - U = proplists:get_value(username, Props, <<>>), -% AuthModule = xml:get_attr_s(auth_module, Props), + U = identity(Props), AuthModule = proplists:get_value(auth_module, Props, <<>>), ?INFO_MSG("(~w) Accepted authentication for ~s " "by ~p from ~s", @@ -912,9 +891,7 @@ wait_for_sasl_response({xmlstreamelement, El}, user = U}); {ok, Props, ServerOut} -> (StateData#state.sockmod):reset_stream(StateData#state.socket), -% U = xml:get_attr_s(username, Props), - U = proplists:get_value(username, Props, <<>>), -% AuthModule = xml:get_attr_s(auth_module, Props), + U = identity(Props), AuthModule = proplists:get_value(auth_module, Props, undefined), ?INFO_MSG("(~w) Accepted authentication for ~s " "by ~p from ~s", @@ -976,11 +953,10 @@ wait_for_sasl_response(timeout, StateData) -> {stop, normal, StateData}; wait_for_sasl_response({xmlstreamend, _Name}, StateData) -> - send_trailer(StateData), {stop, normal, StateData}; + {stop, normal, StateData}; wait_for_sasl_response({xmlstreamerror, _}, StateData) -> send_element(StateData, ?INVALID_XML_ERR), - send_trailer(StateData), {stop, normal, StateData}; wait_for_sasl_response(closed, StateData) -> {stop, normal, StateData}; @@ -1012,7 +988,7 @@ resource_conflict_action(U, S, R) -> acceptnew -> {accept_resource, R}; closenew -> closenew; setresource -> - Rnew = iolist_to_binary([randoms:get_string(),randoms:get_string(),randoms:get_string()]), + Rnew = new_uniq_id(), {accept_resource, Rnew} end. @@ -1032,20 +1008,20 @@ wait_for_bind({xmlstreamelement, #xmlel{name = Name, attrs = Attrs} = El}, end; wait_for_bind({xmlstreamelement, El}, StateData) -> case jlib:iq_query_info(El) of - #iq{type = set, xmlns = ?NS_BIND, sub_el = SubEl} = + #iq{type = set, lang = Lang, 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 jid:resourceprep(R1) of error -> error; - <<"">> -> - iolist_to_binary([randoms:get_string(),randoms:get_string(),randoms:get_string()]); + <<"">> -> new_uniq_id(); Resource -> Resource end, case R of error -> - Err = jlib:make_error_reply(El, ?ERR_BAD_REQUEST), + Txt = <<"Malformed resource">>, + Err = jlib:make_error_reply(El, ?ERRT_BAD_REQUEST(Lang, Txt)), send_element(StateData, Err), fsm_next_state(wait_for_bind, StateData); _ -> @@ -1088,15 +1064,26 @@ wait_for_bind({xmlstreamelement, El}, 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}; wait_for_bind({xmlstreamend, _Name}, StateData) -> - send_trailer(StateData), {stop, normal, StateData}; + {stop, normal, StateData}; wait_for_bind({xmlstreamerror, _}, StateData) -> send_element(StateData, ?INVALID_XML_ERR), - send_trailer(StateData), {stop, normal, StateData}; wait_for_bind(closed, StateData) -> {stop, normal, StateData}; @@ -1107,6 +1094,7 @@ open_session(StateData) -> U = StateData#state.user, R = StateData#state.resource, JID = StateData#state.jid, + Lang = StateData#state.lang, case acl:match_rule(StateData#state.server, StateData#state.access, JID) of allow -> @@ -1144,7 +1132,8 @@ open_session(StateData) -> StateData#state.server, [JID]), ?INFO_MSG("(~w) Forbidden session for ~s", [StateData#state.socket, jid:to_string(JID)]), - {error, ?ERR_NOT_ALLOWED} + Txt = <<"Denied by ACL">>, + {error, ?ERRT_NOT_ALLOWED(Lang, Txt)} end. session_established({xmlstreamelement, #xmlel{name = Name} = El}, StateData) @@ -1167,7 +1156,6 @@ session_established({xmlstreamelement, El}, case check_from(El, FromJID) of 'invalid-from' -> send_element(StateData, ?INVALID_FROM), - send_trailer(StateData), {stop, normal, StateData}; _NewEl -> session_established2(El, StateData) @@ -1180,17 +1168,15 @@ session_established(timeout, StateData) -> [?MODULE, Options, session_established, StateData]), fsm_next_state(session_established, StateData); session_established({xmlstreamend, _Name}, StateData) -> - send_trailer(StateData), {stop, normal, StateData}; + {stop, normal, StateData}; session_established({xmlstreamerror, <<"XML stanza is too big">> = E}, StateData) -> send_element(StateData, ?POLICY_VIOLATION_ERR((StateData#state.lang), E)), - send_trailer(StateData), {stop, normal, StateData}; session_established({xmlstreamerror, _}, StateData) -> send_element(StateData, ?INVALID_XML_ERR), - send_trailer(StateData), {stop, normal, StateData}; session_established(closed, #state{mgmt_state = active} = StateData) -> catch (StateData#state.sockmod):close(StateData#state.socket), @@ -1207,24 +1193,24 @@ session_established2(El, StateData) -> 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 <<"">> -> 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; _ -> @@ -1345,7 +1331,6 @@ handle_info(kick, StateName, StateData) -> handle_info({kick, kicked_by_admin, Xmlelement}, StateName, StateData); handle_info({kick, Reason, Xmlelement}, _StateName, StateData) -> send_element(StateData, Xmlelement), - send_trailer(StateData), {stop, normal, StateData#state{authenticated = Reason}}; handle_info({route, _From, _To, {broadcast, Data}}, @@ -1358,7 +1343,6 @@ handle_info({route, _From, _To, {broadcast, Data}}, {exit, Reason} -> Lang = StateData#state.lang, send_element(StateData, ?SERRT_CONFLICT(Lang, Reason)), - catch send_trailer(StateData), {stop, normal, StateData}; {privacy_list, PrivList, PrivListName} -> case ejabberd_hooks:run_fold(privacy_updated_list, @@ -1408,7 +1392,7 @@ 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 = jid:tolower(From), LBFrom = @@ -1625,7 +1609,7 @@ handle_info({route, From, To, 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; @@ -1672,11 +1656,9 @@ handle_info(system_shutdown, StateName, StateData) -> wait_for_stream -> send_header(StateData, ?MYNAME, <<"1.0">>, <<"en">>), send_element(StateData, ?SERR_SYSTEM_SHUTDOWN), - send_trailer(StateData), ok; _ -> send_element(StateData, ?SERR_SYSTEM_SHUTDOWN), - send_trailer(StateData), ok end, {stop, normal, StateData}; @@ -1737,6 +1719,8 @@ handle_info({broadcast, Type, From, Packet}, StateName, StateData) -> 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). @@ -1806,6 +1790,7 @@ terminate(_Reason, StateName, StateData) -> ok end end, + catch send_trailer(StateData), (StateData#state.sockmod):close(StateData#state.socket), ok. @@ -1844,7 +1829,7 @@ 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); @@ -1912,6 +1897,10 @@ 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} -> @@ -1924,7 +1913,7 @@ is_auth_packet(El) -> 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; @@ -1936,7 +1925,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); @@ -1955,11 +1944,11 @@ 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; @@ -2003,11 +1992,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}, @@ -2068,7 +2057,7 @@ presence_track(From, To, Packet, StateData) -> 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); @@ -2265,11 +2254,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 @@ -2277,30 +2266,32 @@ get_priority_from_presence(PresencePacket) -> end. process_privacy_iq(From, To, - #iq{type = Type, sub_el = SubEl} = IQ, StateData) -> - {Res, NewStateData} = case Type of - get -> - R = ejabberd_hooks:run_fold(privacy_iq_get, - StateData#state.server, - {error, - ?ERR_FEATURE_NOT_IMPLEMENTED}, - [From, To, IQ, - StateData#state.privacy_list]), - {R, StateData}; - set -> - case ejabberd_hooks:run_fold(privacy_iq_set, - StateData#state.server, - {error, - ?ERR_FEATURE_NOT_IMPLEMENTED}, - [From, To, IQ]) - of - {result, R, NewPrivList} -> - {{result, R}, - StateData#state{privacy_list = - NewPrivList}}; - R -> {R, StateData} - end - end, + #iq{type = Type, lang = Lang, sub_el = SubEl} = IQ, StateData) -> + Txt = <<"No module is handling this query">>, + {Res, NewStateData} = + case Type of + get -> + R = ejabberd_hooks:run_fold( + privacy_iq_get, + StateData#state.server, + {error, ?ERRT_FEATURE_NOT_IMPLEMENTED(Lang, Txt)}, + [From, To, IQ, + StateData#state.privacy_list]), + {R, StateData}; + set -> + case ejabberd_hooks:run_fold( + privacy_iq_set, + StateData#state.server, + {error, ?ERRT_FEATURE_NOT_IMPLEMENTED(Lang, Txt)}, + [From, To, IQ]) + of + {result, R, NewPrivList} -> + {{result, R}, + StateData#state{privacy_list = + NewPrivList}}; + R -> {R, StateData} + end + end, IQRes = case Res of {result, Result} -> IQ#iq{type = result, sub_el = Result}; @@ -2310,7 +2301,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]) @@ -2331,7 +2322,9 @@ resend_offline_messages(StateData) -> end end, Rs) - end. + end; +resend_offline_messages(_StateData) -> + ok. resend_subscription_requests(#state{user = User, server = Server} = StateData) -> @@ -2346,34 +2339,35 @@ 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, case jlib:iq_query_info(NewEl) of - #iq{} = IQ -> + #iq{lang = L} = IQ -> Res = ejabberd_hooks:run_fold(c2s_unauthenticated_iq, StateData#state.server, empty, [StateData#state.server, IQ, StateData#state.ip]), case Res of empty -> + Txt = <<"Authentication required">>, ResIQ = IQ#iq{type = error, - sub_el = [?ERR_SERVICE_UNAVAILABLE]}, + sub_el = [?ERRT_SERVICE_UNAVAILABLE(L, Txt)]}, Res1 = jlib:replace_from_to(jid:make(<<"">>, StateData#state.server, <<"">>), @@ -2419,7 +2413,6 @@ fsm_next_state(session_established, #state{mgmt_max_queue = exceeded} = Err = ?SERRT_POLICY_VIOLATION(StateData#state.lang, <<"Too many unacked stanzas">>), send_element(StateData, Err), - send_trailer(StateData), {stop, normal, StateData#state{mgmt_resend = false}}; fsm_next_state(session_established, #state{mgmt_state = pending} = StateData) -> fsm_next_state(wait_for_resume, StateData); @@ -2462,7 +2455,7 @@ 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} -> @@ -2505,6 +2498,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 %%%---------------------------------------------------------------------- @@ -2573,7 +2601,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 -> @@ -2601,7 +2629,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">> -> @@ -2626,10 +2654,10 @@ perform_stream_mgmt(#xmlel{name = Name, attrs = Attrs}, StateData) -> handle_enable(#state{mgmt_timeout = DefaultTimeout, mgmt_max_timeout = MaxTimeout} = StateData, Attrs) -> - Timeout = case xml:get_attr_s(<<"resume">>, Attrs) of + 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 =< MaxTimeout -> Max; @@ -2669,7 +2697,7 @@ 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); _ -> @@ -2679,12 +2707,12 @@ handle_a(StateData, Attrs) -> 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 @@ -2815,9 +2843,9 @@ handle_unacked_stanzas(StateData, F) [N, jid:to_string(StateData#state.jid)]), lists:foreach( fun({_, Time, #xmlel{attrs = Attrs} = El}) -> - From_s = xml:get_attr_s(<<"from">>, Attrs), + From_s = fxml:get_attr_s(<<"from">>, Attrs), From = jid:from_string(From_s), - To_s = xml:get_attr_s(<<"to">>, Attrs), + To_s = fxml:get_attr_s(<<"to">>, Attrs), To = jid:from_string(To_s), F(From, To, El, Time) end, queue:to_list(Queue)) @@ -2833,9 +2861,18 @@ 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, + Lang = StateData#state.lang, ReRoute = case ResendOnTimeout of true -> fun(From, To, El, Time) -> @@ -2844,9 +2881,11 @@ handle_unacked_stanzas(StateData) end; false -> fun(From, To, El, _Time) -> + Txt = <<"User session not found">>, Err = - jlib:make_error_reply(El, - ?ERR_SERVICE_UNAVAILABLE), + jlib:make_error_reply( + El, + ?ERRT_SERVICE_UNAVAILABLE(Lang, Txt)), ejabberd_router:route(To, From, Err) end end, @@ -2854,7 +2893,9 @@ handle_unacked_stanzas(StateData) ?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), + Txt = <<"User session not found">>, + Err = jlib:make_error_reply( + El, ?ERRT_SERVICE_UNAVAILABLE(Lang, Txt)), ejabberd_router:route(To, From, Err); (From, To, El, Time) -> %% We'll drop the stanza if it was <forwarded/> by some @@ -2867,7 +2908,7 @@ handle_unacked_stanzas(StateData) case is_encapsulated_forward(El) of true -> ?DEBUG("Dropping forwarded message stanza from ~s", - [xml:get_attr_s(<<"from">>, El#xmlel.attrs)]); + [fxml:get_attr_s(<<"from">>, El#xmlel.attrs)]); false -> case ejabberd_hooks:run_fold(message_is_archived, StateData#state.server, @@ -2886,9 +2927,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} -> @@ -2901,7 +2942,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; _ -> @@ -2989,7 +3030,7 @@ csi_filter_stanza(#state{csi_state = CsiState, jid = JID} = StateData, 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), @@ -3000,7 +3041,7 @@ 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), + 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. @@ -3080,6 +3121,12 @@ pack_string(String, Pack) -> transform_listen_option(Opt, Opts) -> [Opt|Opts]. +identity(Props) -> + 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; diff --git a/src/ejabberd_captcha.erl b/src/ejabberd_captcha.erl index cc3e2e9f4..157700c47 100644 --- a/src/ejabberd_captcha.erl +++ b/src/ejabberd_captcha.erl @@ -320,7 +320,7 @@ build_captcha_html(Id, Lang) -> -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), diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl index 265d7141f..55ecba5d1 100644 --- a/src/ejabberd_commands.erl +++ b/src/ejabberd_commands.erl @@ -90,7 +90,8 @@ %%% PowFloat = math:pow(Base, Exponent), %%% round(PowFloat).</pre> %%% -%%% Since this function will be called by ejabberd_commands, it must be exported. +%%% Since this function will be called by ejabberd_commands, it must +%%% be exported. %%% Add to your module: %%% <pre>-export([calc_power/2]).</pre> %%% @@ -201,24 +202,34 @@ %%% TODO: consider this feature: %%% All commands are catched. If an error happens, return the restuple: %%% {error, flattened error string} -%%% This means that ecomm call APIs (ejabberd_ctl, ejabberd_xmlrpc) need to allows this. -%%% And ejabberd_xmlrpc must be prepared to handle such an unexpected response. +%%% This means that ecomm call APIs (ejabberd_ctl, ejabberd_xmlrpc) +%%% need to allows this. And ejabberd_xmlrpc must be prepared to +%%% handle such an unexpected response. -module(ejabberd_commands). -author('badlop@process-one.net'). +-define(DEFAULT_VERSION, 1000000). + -export([init/0, list_commands/0, + list_commands/1, get_command_format/1, - get_command_format/2, + get_command_format/2, + get_command_format/3, + get_command_policy/1, get_command_definition/1, + get_command_definition/2, get_tags_commands/0, + get_tags_commands/1, get_commands/0, register_commands/1, unregister_commands/1, execute_command/2, - execute_command/4, + execute_command/3, + execute_command/4, + execute_command/5, opt_type/1, get_commands_spec/0 ]). @@ -226,6 +237,7 @@ -include("ejabberd_commands.hrl"). -include("ejabberd.hrl"). -include("logger.hrl"). +-include_lib("stdlib/include/ms_transform.hrl"). -define(POLICY_ACCESS, '$policy'). @@ -260,23 +272,26 @@ get_commands_spec() -> 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}]), + mnesia:delete_table(ejabberd_commands), + mnesia:create_table(ejabberd_commands, + [{ram_copies, [node()]}, + {local_content, true}, + {attributes, record_info(fields, ejabberd_commands)}, + {type, bag}]), + mnesia:add_table_copy(ejabberd_commands, node(), ram_copies), register_commands(get_commands_spec()). -spec register_commands([ejabberd_commands()]) -> ok. %% @doc Register ejabberd commands. -%% If a command is already registered, a warning is printed and the old command is preserved. +%% If a command is already registered, a warning is printed and the +%% old command is preserved. register_commands(Commands) -> lists:foreach( fun(Command) -> - case ets:insert_new(ejabberd_commands, Command) of - true -> - ok; - false -> - ?DEBUG("This command is already defined:~n~p", [Command]) - end + % XXX check if command exists + mnesia:dirty_write(Command) + % ?DEBUG("This command is already defined:~n~p", [Command]) end, Commands). @@ -286,7 +301,7 @@ register_commands(Commands) -> unregister_commands(Commands) -> lists:foreach( fun(Command) -> - ets:delete_object(ejabberd_commands, Command) + mnesia:dirty_delete_object(Command) end, Commands). @@ -294,94 +309,194 @@ unregister_commands(Commands) -> %% @doc Get a list of all the available commands, arguments and description. list_commands() -> - Commands = ets:match(ejabberd_commands, - #ejabberd_commands{name = '$1', - args = '$2', - desc = '$3', - _ = '_'}), - [{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}. + list_commands(?DEFAULT_VERSION). + +-spec list_commands(integer()) -> [{atom(), [aterm()], string()}]. + +%% @doc Get a list of all the available commands, arguments and +%% description in a given API verion. +list_commands(Version) -> + Commands = get_commands_definition(Version), + [{Name, Args, Desc} || #ejabberd_commands{name = Name, + args = Args, + desc = Desc} <- Commands]. + + +-spec list_commands_policy(integer()) -> + [{atom(), [aterm()], string(), atom()}]. + +%% @doc Get a list of all the available commands, arguments, +%% description, and policy in a given API version. +list_commands_policy(Version) -> + Commands = get_commands_definition(Version), + [{Name, Args, Desc, Policy} || + #ejabberd_commands{name = Name, + args = Args, + desc = Desc, + policy = Policy} <- Commands]. + +-spec get_command_format(atom()) -> {[aterm()], rterm()}. %% @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) -> + get_command_format(Name, noauth, ?DEFAULT_VERSION). +get_command_format(Name, Version) when is_integer(Version) -> + get_command_format(Name, noauth, Version); +get_command_format(Name, Auth) -> + get_command_format(Name, Auth, ?DEFAULT_VERSION). + +-spec get_command_format(atom(), + {binary(), binary(), binary(), boolean()} | + noauth | admin, + integer()) -> + {[aterm()], rterm()}. + +get_command_format(Name, Auth, Version) -> 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, user]] when Admin; - Auth == noauth -> + #ejabberd_commands{args = Args, + result = Result, + policy = Policy} = + get_command_definition(Name, Version), + case Policy of + user when Admin; + Auth == noauth -> {[{user, binary}, {server, binary} | Args], Result}; - [[Args, Result, _]] -> + _ -> {Args, Result} end. --spec get_command_definition(atom()) -> ejabberd_commands() | command_not_found. +-spec get_command_policy(atom()) -> {ok, open|user|admin|restricted} | {error, command_not_found}. + +%% @doc return command policy. +get_command_policy(Name) -> + case get_command_definition(Name) of + #ejabberd_commands{policy = Policy} -> + {ok, Policy}; + command_not_found -> + {error, command_not_found} + end. + +-spec get_command_definition(atom()) -> ejabberd_commands(). %% @doc Get the definition record of a command. get_command_definition(Name) -> - case ets:lookup(ejabberd_commands, Name) of - [E] -> E; - [] -> command_not_found + get_command_definition(Name, ?DEFAULT_VERSION). + +-spec get_command_definition(atom(), integer()) -> ejabberd_commands(). + +%% @doc Get the definition record of a command in a given API version. +get_command_definition(Name, Version) -> + case lists:reverse( + lists:sort( + mnesia:dirty_select( + ejabberd_commands, + ets:fun2ms( + fun(#ejabberd_commands{name = N, version = V} = C) + when N == Name, V =< Version -> + {V, C} + end)))) of + [{_, Command} | _ ] -> Command; + _E -> throw(unknown_command) end. -%% @spec (Name::atom(), Arguments) -> ResultTerm | {error, command_unknown} +-spec get_commands_definition(integer()) -> [ejabberd_commands()]. + +% @doc Returns all commands for a given API version +get_commands_definition(Version) -> + L = lists:reverse( + lists:sort( + mnesia:dirty_select( + ejabberd_commands, + ets:fun2ms( + fun(#ejabberd_commands{name = Name, version = V} = C) + when V =< Version -> + {Name, V, C} + end)))), + F = fun({_Name, _V, Command}, []) -> + [Command]; + ({Name, _V, _Command}, [#ejabberd_commands{name=Name}|_T] = Acc) -> + Acc; + ({_Name, _V, Command}, Acc) -> [Command | Acc] + end, + lists:foldl(F, [], L). + +%% @spec (Name::atom(), Arguments) -> ResultTerm +%% where +%% Arguments = [any()] %% @doc Execute a command. +%% Can return the following exceptions: +%% command_unknown | account_unprivileged | invalid_account_data | +%% no_auth_provided execute_command(Name, Arguments) -> - execute_command([], noauth, Name, Arguments). + execute_command(Name, Arguments, ?DEFAULT_VERSION). --spec execute_command([{atom(), [atom()], [any()]}], +-spec execute_command(atom(), + [any()], + integer() | + {binary(), binary(), binary(), boolean()} | + noauth | admin + ) -> any(). + +%% @spec (Name::atom(), Arguments, integer() | Auth) -> ResultTerm +%% where +%% Auth = {User::string(), Server::string(), Password::string(), +%% Admin::boolean()} +%% | noauth +%% | admin +%% Arguments = [any()] +%% +%% @doc Execute a command in a given API version +%% Can return the following exceptions: +%% command_unknown | account_unprivileged | invalid_account_data | +%% no_auth_provided +execute_command(Name, Arguments, Version) when is_integer(Version) -> + execute_command([], noauth, Name, Arguments, Version); +execute_command(Name, Arguments, Auth) -> + execute_command([], Auth, Name, Arguments, ?DEFAULT_VERSION). + +%% @spec (AccessCommands, Auth, Name::atom(), Arguments) -> +%% ResultTerm | {error, Error} +%% where +%% AccessCommands = [{Access, CommandNames, Arguments}] | undefined +%% Auth = {User::string(), Server::string(), Password::string(), Admin::boolean()} +%% | noauth +%% | admin +%% Arguments = [any()] +%% +%% @doc Execute a command +%% Can return the following exceptions: +%% command_unknown | account_unprivileged | invalid_account_data | no_auth_provided +execute_command(AccessCommands, Auth, Name, Arguments) -> + execute_command(AccessCommands, Auth, Name, Arguments, ?DEFAULT_VERSION). + +-spec execute_command([{atom(), [atom()], [any()]}] | undefined, {binary(), binary(), binary(), boolean()} | noauth | admin, atom(), - [any()] + [any()], + integer() ) -> any(). -%% @spec (AccessCommands, Auth, Name::atom(), Arguments) -> ResultTerm | {error, Error} +%% @spec (AccessCommands, Auth, Name::atom(), Arguments, integer()) -> ResultTerm %% where -%% AccessCommands = [{Access, CommandNames, Arguments}] +%% AccessCommands = [{Access, CommandNames, Arguments}] | undefined %% 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(AccessCommands1, Auth1, Name, Arguments) -> +%% +%% @doc Execute a command in a given API version +%% Can return the following exceptions: +%% command_unknown | account_unprivileged | invalid_account_data | no_auth_provided +execute_command(AccessCommands1, Auth1, Name, Arguments, Version) -> 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(Auth, Command, Arguments) - catch - {error, Error} -> {error, Error} - end; - [] -> {error, command_unknown} + Command = get_command_definition(Name, Version), + AccessCommands = get_access_commands(AccessCommands1, Version), + case check_access_commands(AccessCommands, Auth, Name, Command, Arguments) of + ok -> execute_command2(Auth, Command, Arguments) end. execute_command2( @@ -407,26 +522,25 @@ execute_command2(Command, Arguments) -> Module = Command#ejabberd_commands.module, Function = Command#ejabberd_commands.function, ?DEBUG("Executing command ~p:~p with Args=~p", [Module, Function, Arguments]), - try apply(Module, Function, Arguments) of - Response -> - Response - catch - Problem -> - {error, Problem} - end. + apply(Module, Function, Arguments). -spec get_tags_commands() -> [{string(), [string()]}]. %% @spec () -> [{Tag::string(), [CommandName::string()]}] %% @doc Get all the tags and associated commands. get_tags_commands() -> - CommandTags = ets:match(ejabberd_commands, - #ejabberd_commands{ - name = '$1', - tags = '$2', - _ = '_'}), + get_tags_commands(?DEFAULT_VERSION). + +-spec get_tags_commands(integer()) -> [{string(), [string()]}]. + +%% @spec (integer) -> [{Tag::string(), [CommandName::string()]}] +%% @doc Get all the tags and associated commands in a given API version +get_tags_commands(Version) -> + CommandTags = [{Name, Tags} || + #ejabberd_commands{name = Name, tags = Tags} + <- get_commands_definition(Version)], Dict = lists:foldl( - fun([CommandNameAtom, CTags], D) -> + fun({CommandNameAtom, CTags}, D) -> CommandName = atom_to_list(CommandNameAtom), case CTags of [] -> @@ -445,7 +559,6 @@ get_tags_commands() -> CommandTags), orddict:to_list(Dict). - %% ----------------------------- %% Access verification %% ----------------------------- @@ -465,7 +578,7 @@ check_access_commands([], _Auth, _Method, _Command, _Arguments) -> check_access_commands(AccessCommands, Auth, Method, Command1, Arguments) -> Command = case {Command1#ejabberd_commands.policy, Auth} of - {user, {_, _, _}} -> + {user, {_, _, _, _}} -> Command1; {user, _} -> Command1#ejabberd_commands{ @@ -479,7 +592,8 @@ check_access_commands(AccessCommands, Auth, Method, Command1, Arguments) -> fun({Access, Commands, ArgumentRestrictions}) -> case check_access(Command, Access, Auth) of true -> - check_access_command(Commands, Command, ArgumentRestrictions, + check_access_command(Commands, Command, + ArgumentRestrictions, Method, Arguments); false -> false @@ -488,7 +602,8 @@ check_access_commands(AccessCommands, Auth, Method, Command1, Arguments) -> ArgumentRestrictions = [], case check_access(Command, Access, Auth) of true -> - check_access_command(Commands, Command, ArgumentRestrictions, + check_access_command(Commands, Command, + ArgumentRestrictions, Method, Arguments); false -> false @@ -517,7 +632,7 @@ check_auth(Command, {User, Server, {oauth, Token}, _}) -> 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 + case ejabberd_auth:check_password(User, <<"">>, Server, Password) of true -> {ok, User, Server}; _ -> throw({error, invalid_account_data}) end. @@ -551,9 +666,11 @@ check_access2(Access, User, Server) -> deny -> false end. -check_access_command(Commands, Command, ArgumentRestrictions, Method, Arguments) -> +check_access_command(Commands, Command, ArgumentRestrictions, + Method, Arguments) -> case Commands==all orelse lists:member(Method, Commands) of - true -> check_access_arguments(Command, ArgumentRestrictions, Arguments); + true -> check_access_arguments(Command, ArgumentRestrictions, + Arguments); false -> false end. @@ -577,25 +694,28 @@ tag_arguments(ArgsDefs, Args) -> Args). -get_access_commands(undefined) -> - Cmds = get_commands(), +get_access_commands(undefined, Version) -> + Cmds = get_commands(Version), [{?POLICY_ACCESS, Cmds, []}]; -get_access_commands(AccessCommands) -> +get_access_commands(AccessCommands, _Version) -> AccessCommands. get_commands() -> - Opts = ejabberd_config:get_option( + get_commands(?DEFAULT_VERSION). +get_commands(Version) -> + Opts0 = ejabberd_config:get_option( commands, fun(V) when is_list(V) -> V end, []), - CommandsList = list_commands_policy(), + Opts = lists:map(fun(V) when is_tuple(V) -> [V]; (V) -> V end, Opts0), + CommandsList = list_commands_policy(Version), 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) -> + fun([{add_commands, L}], Acc) -> Cmds = case L of open -> OpenCmds; restricted -> RestrictedCmds; @@ -604,7 +724,7 @@ get_commands() -> _ when is_list(L) -> L end, lists:usort(Cmds ++ Acc); - ({remove_commands, L}, Acc) -> + ([{remove_commands, L}], Acc) -> Cmds = case L of open -> OpenCmds; restricted -> RestrictedCmds; diff --git a/src/ejabberd_commands_doc.erl b/src/ejabberd_commands_doc.erl index 277ed0c78..dc00c5d2a 100644 --- a/src/ejabberd_commands_doc.erl +++ b/src/ejabberd_commands_doc.erl @@ -32,7 +32,7 @@ -include("ejabberd_commands.hrl"). -include("ejabberd.hrl"). --define(RAW(V), if HTMLOutput -> xml:crypt(iolist_to_binary(V)); true -> iolist_to_binary(V) end). +-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). @@ -79,7 +79,7 @@ md_tag(pre, V) -> md_tag(p, V) -> [<<"\n\n">>, V, <<"\n">>]; md_tag(h1, V) -> - [<<"\\nn## ">>, V, <<"\n">>]; + [<<"\n\n## ">>, V, <<"\n">>]; md_tag(h2, V) -> [<<"\n\n### ">>, V, <<"\n">>]; md_tag(strong, V) -> @@ -158,7 +158,7 @@ java_call(Name, ArgsDesc, Values, HTMLOutput) -> 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(");"), ?BR]. + ?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(">")). @@ -360,8 +360,9 @@ gen_doc(#ejabberd_commands{name=Name, tags=_Tags, desc=Desc, longdesc=LongDesc, none -> [?RAW(io_lib:format("~p", [Result]))]; _ -> - [?RAW(io_lib:format("~p", [Result])), - ?TAG_R(p, ResultDesc)] + [?TAG(dl, [ + ?TAG(dt, io_lib:format("~p", [Result])), + ?TAG_R(dd, ResultDesc)])] end, [?TAG(h1, [?TAG(strong, atom_to_list(Name)), <<" - ">>, ?RAW(Desc)]), diff --git a/src/ejabberd_config.erl b/src/ejabberd_config.erl index 8d2d19975..f73474fe7 100644 --- a/src/ejabberd_config.erl +++ b/src/ejabberd_config.erl @@ -38,6 +38,8 @@ convert_to_yaml/1, convert_to_yaml/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"). @@ -52,22 +54,12 @@ %% @type macro_value() = term(). - start() -> - case catch mnesia:table_info(local_config, storage_type) of - disc_copies -> - mnesia:delete_table(local_config); - _ -> - ok - end, - mnesia:create_table(local_config, - [{ram_copies, [node()]}, - {local_content, true}, - {attributes, record_info(fields, local_config)}]), - mnesia:add_table_copy(local_config, node(), ram_copies), + mnesia_init(), Config = get_ejabberd_config_path(), State0 = read_file(Config), - State = validate_opts(State0), + 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 @@ -76,9 +68,45 @@ start() -> 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). + 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(set_hosts_in_options(Hosts, #state{opts = Opts})). + +mnesia_init() -> + case catch mnesia:table_info(local_config, storage_type) of + disc_copies -> + mnesia:delete_table(local_config); + _ -> + ok + end, + mnesia:create_table(local_config, + [{ram_copies, [node()]}, + {local_content, true}, + {attributes, record_info(fields, local_config)}]), + 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". @@ -112,7 +140,7 @@ 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}, @@ -162,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]); @@ -226,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; _ -> @@ -277,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", []), @@ -286,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", []), @@ -296,9 +324,12 @@ 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,[]). @@ -408,7 +439,7 @@ maps_to_lists(IMap) -> end, [], IMap). merge_configs(Terms, ResMap) -> - lists:foldl(fun({Name, Val}, Map) when is_list(Val) -> + 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 -> @@ -620,14 +651,27 @@ process_host_term(Term, Host, State, Action) -> {hosts, _} -> State; {Opt, Val} when Action == set -> - set_option({Opt, Host}, Val, State); + set_option({rename_option(Opt), Host}, Val, State); {Opt, Val} when Action == append -> - append_option({Opt, Host}, Val, State); + append_option({rename_option(Opt), Host}, Val, State); Opt -> ?WARNING_MSG("Ignore invalid (outdated?) option ~p", [Opt]), State end. +rename_option(Option) when is_atom(Option) -> + case atom_to_list(Option) of + "odbc_" ++ T -> + NewOption = list_to_atom("sql_" ++ T), + ?WARNING_MSG("Option '~s' is obsoleted, use '~s' instead", + [Option, NewOption]), + NewOption; + _ -> + Option + end; +rename_option(Option) -> + Option. + set_option(Opt, Val, State) -> State#state{opts = [#local_config{key = Opt, value = Val} | State#state.opts]}. @@ -836,20 +880,20 @@ get_mylang() -> fun iolist_to_binary/1, <<"en">>). -replace_module(mod_announce_odbc) -> {mod_announce, odbc}; -replace_module(mod_blocking_odbc) -> {mod_blocking, odbc}; -replace_module(mod_caps_odbc) -> {mod_caps, odbc}; -replace_module(mod_irc_odbc) -> {mod_irc, odbc}; -replace_module(mod_last_odbc) -> {mod_last, odbc}; -replace_module(mod_muc_odbc) -> {mod_muc, odbc}; -replace_module(mod_offline_odbc) -> {mod_offline, odbc}; -replace_module(mod_privacy_odbc) -> {mod_privacy, odbc}; -replace_module(mod_private_odbc) -> {mod_private, odbc}; -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(mod_announce_odbc) -> {mod_announce, sql}; +replace_module(mod_blocking_odbc) -> {mod_blocking, sql}; +replace_module(mod_caps_odbc) -> {mod_caps, sql}; +replace_module(mod_irc_odbc) -> {mod_irc, sql}; +replace_module(mod_last_odbc) -> {mod_last, sql}; +replace_module(mod_muc_odbc) -> {mod_muc, sql}; +replace_module(mod_offline_odbc) -> {mod_offline, sql}; +replace_module(mod_privacy_odbc) -> {mod_privacy, sql}; +replace_module(mod_private_odbc) -> {mod_private, sql}; +replace_module(mod_roster_odbc) -> {mod_roster, sql}; +replace_module(mod_shared_roster_odbc) -> {mod_shared_roster, sql}; +replace_module(mod_vcard_odbc) -> {mod_vcard, sql}; +replace_module(mod_vcard_xupdate_odbc) -> {mod_vcard_xupdate, sql}; +replace_module(mod_pubsub_odbc) -> {mod_pubsub, sql}; replace_module(Module) -> case is_elixir_module(Module) of true -> expand_elixir_module(Module); @@ -954,7 +998,7 @@ transform_terms(Terms) -> mod_last, ejabberd_s2s, ejabberd_listener, - ejabberd_odbc_sup, + ejabberd_sql_sup, shaper, ejabberd_s2s_out, acl, diff --git a/src/ejabberd_ctl.erl b/src/ejabberd_ctl.erl index 2bf20c2e7..bb16e91e8 100644 --- a/src/ejabberd_ctl.erl +++ b/src/ejabberd_ctl.erl @@ -57,6 +57,8 @@ -include("ejabberd.hrl"). -include("logger.hrl"). +-define(DEFAULT_VERSION, 1000000). + %%----------------------------- %% Module @@ -69,7 +71,7 @@ start() -> [SNode3 | Args3] -> [SNode3, 60000, Args3]; _ -> - print_usage(), + print_usage(?DEFAULT_VERSION), halt(?STATUS_USAGE) end, SNode1 = case string:tokens(SNode, "@") of @@ -93,6 +95,9 @@ start() -> [Node, Reason]), %% TODO: show minimal start help ?STATUS_BADRPC; + {invalid_version, V} -> + print("Invalid API version number: ~p~n", [V]), + ?STATUS_ERROR; S -> S end, @@ -126,11 +131,17 @@ unregister_commands(CmdDescs, Module, Function) -> %% Process %%----------------------------- + -spec process([string()]) -> non_neg_integer(). +process(Args) -> + process(Args, ?DEFAULT_VERSION). + + +-spec process([string()], non_neg_integer()) -> non_neg_integer(). %% The commands status, stop and restart are defined here to ensure %% they are usable even if ejabberd is completely stopped. -process(["status"]) -> +process(["status"], _Version) -> {InternalStatus, ProvidedStatus} = init:get_status(), print("The node ~p is ~p with status: ~p~n", [node(), InternalStatus, ProvidedStatus]), @@ -146,24 +157,24 @@ process(["status"]) -> ?STATUS_SUCCESS end; -process(["stop"]) -> +process(["stop"], _Version) -> %%ejabberd_cover:stop(), init:stop(), ?STATUS_SUCCESS; -process(["restart"]) -> +process(["restart"], _Version) -> init:restart(), ?STATUS_SUCCESS; -process(["mnesia"]) -> +process(["mnesia"], _Version) -> print("~p~n", [mnesia:system_info(all)]), ?STATUS_SUCCESS; -process(["mnesia", "info"]) -> +process(["mnesia", "info"], _Version) -> mnesia:info(), ?STATUS_SUCCESS; -process(["mnesia", Arg]) -> +process(["mnesia", Arg], _Version) -> case catch mnesia:system_info(list_to_atom(Arg)) of {'EXIT', Error} -> print("Error: ~p~n", [Error]); Return -> print("~p~n", [Return]) @@ -172,23 +183,23 @@ process(["mnesia", Arg]) -> %% The arguments --long and --dual are not documented because they are %% automatically selected depending in the number of columns of the shell -process(["help" | Mode]) -> +process(["help" | Mode], Version) -> {MaxC, ShCode} = get_shell_info(), case Mode of [] -> - print_usage(dual, MaxC, ShCode), + print_usage(dual, MaxC, ShCode, Version), ?STATUS_USAGE; ["--dual"] -> - print_usage(dual, MaxC, ShCode), + print_usage(dual, MaxC, ShCode, Version), ?STATUS_USAGE; ["--long"] -> - print_usage(long, MaxC, ShCode), + print_usage(long, MaxC, ShCode, Version), ?STATUS_USAGE; ["--tags"] -> - print_usage_tags(MaxC, ShCode), + print_usage_tags(MaxC, ShCode, Version), ?STATUS_SUCCESS; ["--tags", Tag] -> - print_usage_tags(Tag, MaxC, ShCode), + print_usage_tags(Tag, MaxC, ShCode, Version), ?STATUS_SUCCESS; ["help"] -> print_usage_help(MaxC, ShCode), @@ -196,13 +207,22 @@ process(["help" | Mode]) -> [CmdString | _] -> CmdStringU = ejabberd_regexp:greplace( list_to_binary(CmdString), <<"-">>, <<"_">>), - print_usage_commands(binary_to_list(CmdStringU), MaxC, ShCode), + print_usage_commands2(binary_to_list(CmdStringU), MaxC, ShCode, Version), ?STATUS_SUCCESS end; -process(Args) -> +process(["--version", Arg | Args], _) -> + Version = + try + list_to_integer(Arg) + catch _:_ -> + throw({invalid_version, Arg}) + end, + process(Args, Version); + +process(Args, Version) -> AccessCommands = get_accesscommands(), - {String, Code} = process2(Args, AccessCommands), + {String, Code} = process2(Args, AccessCommands, Version), case String of [] -> ok; _ -> @@ -211,18 +231,25 @@ process(Args) -> Code. %% @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), true}, AccessCommands); process2(Args, AccessCommands) -> - process2(Args, admin, AccessCommands). + process2(Args, AccessCommands, ?DEFAULT_VERSION). + +%% @spec (Args::[string()], AccessCommands, Version) -> {String::string(), Code::integer()} +process2(["--auth", User, Server, Pass | Args], AccessCommands, Version) -> + process2(Args, AccessCommands, {list_to_binary(User), list_to_binary(Server), + list_to_binary(Pass), true}, Version); +process2(Args, AccessCommands, Version) -> + process2(Args, AccessCommands, admin, Version). + + -process2(Args, Auth, AccessCommands) -> - case try_run_ctp(Args, Auth, AccessCommands) of +process2(Args, AccessCommands, Auth, Version) -> + case try_run_ctp(Args, Auth, AccessCommands, Version) of {String, wrong_command_arguments} when is_list(String) -> io:format(lists:flatten(["\n" | String]++["\n"])), [CommandString | _] = Args, - process(["help" | [CommandString]]), + process(["help" | [CommandString]], Version), {lists:flatten(String), ?STATUS_ERROR}; {String, Code} when is_list(String) and is_integer(Code) -> @@ -246,29 +273,29 @@ get_accesscommands() -> %%----------------------------- %% @spec (Args::[string()], Auth, AccessCommands) -> string() | integer() | {string(), integer()} -try_run_ctp(Args, Auth, AccessCommands) -> +try_run_ctp(Args, Auth, AccessCommands, Version) -> try ejabberd_hooks:run_fold(ejabberd_ctl_process, false, [Args]) of false when Args /= [] -> - try_call_command(Args, Auth, AccessCommands); + try_call_command(Args, Auth, AccessCommands, Version); false -> - print_usage(), + print_usage(Version), {"", ?STATUS_USAGE}; Status -> {"", Status} catch exit:Why -> - print_usage(), + print_usage(Version), {io_lib:format("Error in ejabberd ctl process: ~p", [Why]), ?STATUS_USAGE}; Error:Why -> %% In this case probably ejabberd is not started, so let's show Status - process(["status"]), + process(["status"], Version), print("~n", []), {io_lib:format("Error in ejabberd ctl process: '~p' ~p", [Error, Why]), ?STATUS_USAGE} end. %% @spec (Args::[string()], Auth, AccessCommands) -> string() | integer() | {string(), integer()} -try_call_command(Args, Auth, AccessCommands) -> - try call_command(Args, Auth, AccessCommands) of +try_call_command(Args, Auth, AccessCommands, Version) -> + try call_command(Args, Auth, AccessCommands, Version) of {error, command_unknown} -> {io_lib:format("Error: command ~p not known.", [hd(Args)]), ?STATUS_ERROR}; {error, wrong_command_arguments} -> @@ -276,24 +303,28 @@ try_call_command(Args, Auth, AccessCommands) -> Res -> Res catch + throw:Error -> + {io_lib:format("~p", [Error]), ?STATUS_ERROR}; A:Why -> Stack = erlang:get_stacktrace(), {io_lib:format("Problem '~p ~p' occurred executing the command.~nStacktrace: ~p", [A, Why, Stack]), ?STATUS_ERROR} end. %% @spec (Args::[string()], Auth, AccessCommands) -> string() | integer() | {string(), integer()} | {error, ErrorType} -call_command([CmdString | Args], Auth, AccessCommands) -> +call_command([CmdString | Args], Auth, AccessCommands, Version) -> CmdStringU = ejabberd_regexp:greplace( list_to_binary(CmdString), <<"-">>, <<"_">>), Command = list_to_atom(binary_to_list(CmdStringU)), - case ejabberd_commands:get_command_format(Command, Auth) of + case ejabberd_commands:get_command_format(Command, Auth, Version) of {error, command_unknown} -> {error, command_unknown}; {ArgsFormat, ResultFormat} -> case (catch format_args(Args, ArgsFormat)) of ArgsFormatted when is_list(ArgsFormatted) -> - Result = ejabberd_commands:execute_command(AccessCommands, Auth, Command, - ArgsFormatted), + Result = ejabberd_commands:execute_command(AccessCommands, + Auth, Command, + ArgsFormatted, + Version), format_result(Result, ResultFormat); {'EXIT', {function_clause,[{lists,zip,[A1, A2], _} | _]}} -> {NumCompa, TextCompa} = @@ -324,7 +355,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) -> @@ -349,7 +380,7 @@ 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) -> @@ -404,8 +435,8 @@ make_status(ok) -> ?STATUS_SUCCESS; make_status(true) -> ?STATUS_SUCCESS; make_status(_Error) -> ?STATUS_ERROR. -get_list_commands() -> - try ejabberd_commands:list_commands() of +get_list_commands(Version) -> + try ejabberd_commands:list_commands(Version) of Commands -> [tuple_command_help(Command) || {N,_,_}=Command <- Commands, @@ -458,10 +489,10 @@ get_list_ctls() -> -define(U2, "\e[24m"). -define(U(S), case ShCode of true -> [?U1, S, ?U2]; false -> S end). -print_usage() -> +print_usage(Version) -> {MaxC, ShCode} = get_shell_info(), - print_usage(dual, MaxC, ShCode). -print_usage(HelpMode, MaxC, ShCode) -> + print_usage(dual, MaxC, ShCode, Version). +print_usage(HelpMode, MaxC, ShCode, Version) -> AllCommands = [ {"status", [], "Get ejabberd status"}, @@ -469,12 +500,11 @@ print_usage(HelpMode, MaxC, ShCode) -> {"restart", [], "Restart ejabberd"}, {"help", ["[--tags [tag] | com?*]"], "Show help (try: ejabberdctl help help)"}, {"mnesia", ["[info]"], "show information of Mnesia system"}] ++ - get_list_commands() ++ + get_list_commands(Version) ++ get_list_ctls(), print( - ["Usage: ", ?B("ejabberdctl"), " [--no-timeout] [--node ", ?U("nodename"), "] [--auth ", - ?U("user"), " ", ?U("host"), " ", ?U("password"), "] ", + ["Usage: ", ?B("ejabberdctl"), " [--no-timeout] [--node ", ?U("nodename"), "] [--version ", ?U("api_version"), "] ", ?U("command"), " [", ?U("options"), "]\n" "\n" "Available commands in this ejabberd node:\n"], []), @@ -598,9 +628,9 @@ format_command_lines(CALD, _MaxCmdLen, MaxC, ShCode, long) -> %% Print Tags %%----------------------------- -print_usage_tags(MaxC, ShCode) -> +print_usage_tags(MaxC, ShCode, Version) -> print("Available tags and commands:", []), - TagsCommands = ejabberd_commands:get_tags_commands(), + TagsCommands = ejabberd_commands:get_tags_commands(Version), lists:foreach( fun({Tag, Commands} = _TagCommands) -> print(["\n\n ", ?B(Tag), "\n "], []), @@ -611,10 +641,10 @@ print_usage_tags(MaxC, ShCode) -> TagsCommands), print("\n\n", []). -print_usage_tags(Tag, MaxC, ShCode) -> +print_usage_tags(Tag, MaxC, ShCode, Version) -> print(["Available commands with tag ", ?B(Tag), ":", "\n"], []), HelpMode = long, - TagsCommands = ejabberd_commands:get_tags_commands(), + TagsCommands = ejabberd_commands:get_tags_commands(Version), CommandsNames = case lists:keysearch(Tag, 1, TagsCommands) of {value, {Tag, CNs}} -> CNs; false -> [] @@ -622,7 +652,7 @@ print_usage_tags(Tag, MaxC, ShCode) -> CommandsList = lists:map( fun(NameString) -> C = ejabberd_commands:get_command_definition( - list_to_atom(NameString)), + list_to_atom(NameString), Version), #ejabberd_commands{name = Name, args = Args, desc = Desc} = C, @@ -673,20 +703,20 @@ print_usage_help(MaxC, ShCode) -> %%----------------------------- %% @spec (CmdSubString::string(), MaxC::integer(), ShCode::boolean()) -> ok -print_usage_commands(CmdSubString, MaxC, ShCode) -> +print_usage_commands2(CmdSubString, MaxC, ShCode, Version) -> %% Get which command names match this substring - AllCommandsNames = [atom_to_list(Name) || {Name, _, _} <- ejabberd_commands:list_commands()], + AllCommandsNames = [atom_to_list(Name) || {Name, _, _} <- ejabberd_commands:list_commands(Version)], Cmds = filter_commands(AllCommandsNames, CmdSubString), case Cmds of - [] -> io:format("Error: not command found that match: ~p~n", [CmdSubString]); - _ -> print_usage_commands2(lists:sort(Cmds), MaxC, ShCode) + [] -> io:format("Error: no command found that match: ~p~n", [CmdSubString]); + _ -> print_usage_commands3(lists:sort(Cmds), MaxC, ShCode, Version) end. -print_usage_commands2(Cmds, MaxC, ShCode) -> +print_usage_commands3(Cmds, MaxC, ShCode, Version) -> %% Then for each one print it lists:mapfoldl( fun(Cmd, Remaining) -> - print_usage_command(Cmd, MaxC, ShCode), + print_usage_command(Cmd, MaxC, ShCode, Version), case Remaining > 1 of true -> print([" ", lists:duplicate(MaxC, 126), " \n"], []); false -> ok @@ -716,16 +746,16 @@ filter_commands_regexp(All, Glob) -> All). %% @spec (Cmd::string(), MaxC::integer(), ShCode::boolean()) -> ok -print_usage_command(Cmd, MaxC, ShCode) -> +print_usage_command(Cmd, MaxC, ShCode, Version) -> Name = list_to_atom(Cmd), - case ejabberd_commands:get_command_definition(Name) of + case ejabberd_commands:get_command_definition(Name, Version) of command_not_found -> io:format("Error: command ~p not known.~n", [Cmd]); C -> - print_usage_command(Cmd, C, MaxC, ShCode) + print_usage_command2(Cmd, C, MaxC, ShCode) end. -print_usage_command(Cmd, C, MaxC, ShCode) -> +print_usage_command2(Cmd, C, MaxC, ShCode) -> #ejabberd_commands{ tags = TagsAtoms, desc = Desc, diff --git a/src/ejabberd_frontend_socket.erl b/src/ejabberd_frontend_socket.erl index 09eeded9c..b8e706f23 100644 --- a/src/ejabberd_frontend_socket.erl +++ b/src/ejabberd_frontend_socket.erl @@ -148,20 +148,20 @@ init([Module, SockMod, Socket, Opts, Receiver]) -> receiver = Receiver}}. 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} = @@ -187,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), @@ -236,9 +236,9 @@ check_starttls(SockMod, Socket, Receiver, Opts) -> (_) -> false end, Opts), if TLSEnabled -> - {ok, TLSSocket} = p1_tls:tcp_to_tls(Socket, TLSOpts), + {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_http.erl b/src/ejabberd_http.erl index b95f98207..6b53f46c6 100644 --- a/src/ejabberd_http.erl +++ b/src/ejabberd_http.erl @@ -117,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 @@ -328,8 +328,8 @@ get_transfer_protocol(SockMod, HostPort) -> {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. @@ -532,10 +532,10 @@ 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)]); + fxml:element_to_binary(XHTML)]); _ -> iolist_to_binary([?XHTML_DOCTYPE, - xml:element_to_binary(XHTML)]) + fxml:element_to_binary(XHTML)]) end, Headers1 = case lists:keysearch(<<"Content-Type">>, 1, Headers) @@ -753,6 +753,7 @@ code_to_phrase(503) -> <<"Service Unavailable">>; code_to_phrase(504) -> <<"Gateway Timeout">>; code_to_phrase(505) -> <<"HTTP Version Not Supported">>. +-spec parse_auth(binary()) -> {binary(), binary()} | {oauth, binary(), []} | undefined. parse_auth(<<"Basic ", Auth64/binary>>) -> Auth = jlib:decode_base64(Auth64), %% Auth should be a string with the format: user@server:password diff --git a/src/ejabberd_http_bind.erl b/src/ejabberd_http_bind.erl index c4cfdddd9..ea8cd792f 100644 --- a/src/ejabberd_http_bind.erl +++ b/src/ejabberd_http_bind.erl @@ -224,7 +224,7 @@ process_request(Data, IP, HOpts) -> 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, @@ -248,13 +248,13 @@ process_request(Data, IP, HOpts) -> 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 @@ -280,7 +280,7 @@ process_request(Data, IP, HOpts) -> 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; @@ -289,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; @@ -299,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; @@ -312,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, @@ -589,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; @@ -801,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">>}, @@ -810,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">>}, @@ -819,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">>}, @@ -855,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}; @@ -929,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, @@ -968,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}, @@ -1032,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, @@ -1067,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}], @@ -1114,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}; @@ -1133,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} @@ -1165,7 +1165,7 @@ set_inactivity_timer(_Pause, MaxInactivity) -> 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()} @@ -1188,7 +1188,7 @@ get_max_pause(_) -> ?MAX_PAUSE. 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 3b50e44a3..e66cf33a5 100644 --- a/src/ejabberd_http_ws.erl +++ b/src/ejabberd_http_ws.erl @@ -171,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 @@ -186,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} -> @@ -210,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}. @@ -316,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">>}; @@ -330,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">>} | diff --git a/src/ejabberd_local.erl b/src/ejabberd_local.erl index 66f530c8f..5566073e9 100644 --- a/src/ejabberd_local.erl +++ b/src/ejabberd_local.erl @@ -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, @@ -74,7 +74,7 @@ start_link() -> process_iq(From, To, Packet) -> IQ = jlib:iq_query_info(Packet), case IQ of - #iq{xmlns = XMLNS} -> + #iq{xmlns = XMLNS, lang = Lang} -> Host = To#jid.lserver, case ets:lookup(?IQTABLE, {XMLNS, Host}) of [{_, Module, Function}] -> @@ -87,8 +87,10 @@ process_iq(From, To, Packet) -> gen_iq_handler:handle(Host, Module, Function, Opts, From, To, IQ); [] -> - Err = jlib:make_error_reply(Packet, - ?ERR_FEATURE_NOT_IMPLEMENTED), + Txt = <<"No module is handling this query">>, + Err = jlib:make_error_reply( + Packet, + ?ERRT_FEATURE_NOT_IMPLEMENTED(Lang, Txt)), ejabberd_router:route(To, From, Err) end; reply -> @@ -166,8 +168,10 @@ refresh_iq_handlers() -> ejabberd_local ! refresh_iq_handlers. bounce_resource_packet(From, To, Packet) -> + Lang = fxml:get_tag_attr_s(<<"xml:lang">>, Packet), + Txt = <<"No available resource found">>, Err = jlib:make_error_reply(Packet, - ?ERR_ITEM_NOT_FOUND), + ?ERRT_ITEM_NOT_FOUND(Lang, Txt)), ejabberd_router:route(To, From, Err), stop. @@ -178,6 +182,7 @@ bounce_resource_packet(From, To, Packet) -> init([]) -> lists:foreach(fun (Host) -> ejabberd_router:register_route(Host, + Host, {apply, ?MODULE, route}), ejabberd_hooks:add(local_send_to_resource_hook, Host, @@ -272,7 +277,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 05499b45c..795d4f390 100644 --- a/src/ejabberd_logger.erl +++ b/src/ejabberd_logger.erl @@ -50,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} -> @@ -99,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), @@ -126,10 +153,12 @@ 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( @@ -139,8 +168,9 @@ rotate_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"}; @@ -152,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; @@ -162,7 +193,7 @@ set(LogLevel) when is_integer(LogLevel) -> 5 -> debug; E -> throw({wrong_loglevel, E}) end, - case lager:get_loglevel(lager_console_backend) of + case get_lager_loglevel() of LagerLogLevel -> ok; _ -> @@ -172,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)) @@ -180,3 +213,22 @@ set(LogLevel) when is_integer(LogLevel) -> set({_LogLevel, _}) -> error_logger:error_msg("custom loglevels are not supported for 'lager'"), {module, lager}. + +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. diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl index a688bb39e..d8e0a435d 100644 --- a/src/ejabberd_oauth.erl +++ b/src/ejabberd_oauth.erl @@ -134,7 +134,7 @@ authenticate_user({User, Server}, {password, Password} = Ctx) -> none), case acl:match_rule(JID#jid.lserver, Access, JID) of allow -> - case ejabberd_auth:check_password(User, Server, Password) of + case ejabberd_auth:check_password(User, <<"">>, Server, Password) of true -> {ok, {Ctx, {user, User, Server}}}; false -> @@ -149,7 +149,7 @@ authenticate_user({User, Server}, {password, Password} = Ctx) -> authenticate_client(Client, Ctx) -> {ok, {Ctx, {client, Client}}}. -verify_resowner_scope({user, User, Server}, Scope, Ctx) -> +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], @@ -164,7 +164,7 @@ verify_resowner_scope(_, _, _) -> {error, badscope}. -associate_access_code(AccessCode, Context, AppContext) -> +associate_access_code(_AccessCode, _Context, AppContext) -> %put(?ACCESS_CODE_TABLE, AccessCode, Context), {ok, AppContext}. @@ -184,7 +184,7 @@ associate_access_token(AccessToken, Context, AppContext) -> mnesia:dirty_write(R), {ok, AppContext}. -associate_refresh_token(RefreshToken, Context, AppContext) -> +associate_refresh_token(_RefreshToken, _Context, AppContext) -> %put(?REFRESH_TOKEN_TABLE, RefreshToken, Context), {ok, AppContext}. @@ -303,7 +303,7 @@ process(_Handlers, process(_Handlers, #request{method = 'POST', q = Q, lang = _Lang, path = [_, <<"authorization_token">>]}) -> - ResponseType = proplists:get_value(<<"response_type">>, Q, <<"">>), + _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, <<"">>), diff --git a/src/ejabberd_piefxis.erl b/src/ejabberd_piefxis.erl index 50c8e323c..758001239 100644 --- a/src/ejabberd_piefxis.erl +++ b/src/ejabberd_piefxis.erl @@ -28,8 +28,8 @@ %%% Not implemented: %%% - write mod_piefxis with ejabberdctl commands -%%% - Export from mod_offline_odbc.erl -%%% - Export from mod_private_odbc.erl +%%% - Export from mod_offline_sql.erl +%%% - Export from mod_private_sql.erl %%% - XEP-227: 6. Security Considerations %%% - Other schemas of XInclude are not tested, and may not be imported correctly. %%% - If a host has many users, split that host in XML files with 50 users each. @@ -64,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(), @@ -80,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}), @@ -97,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 %%%=================================================================== @@ -194,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 @@ -223,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 -> @@ -238,8 +169,6 @@ 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 = jid:nameprep(Server), @@ -257,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}], @@ -289,7 +218,6 @@ get_vcard(User, Server) -> [] end. -%%%================================== get_offline(User, Server) -> case mod_offline:get_offline_els(User, Server) of [] -> @@ -306,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, @@ -333,7 +260,6 @@ get_privacy(User, Server) -> [] end. -%% @spec (Dir::string(), Hosts::[string()]) -> ok get_roster(User, Server) -> JID = jid:make(User, Server, <<>>), case mod_roster:get_roster(User, Server) of @@ -387,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. @@ -415,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 -> @@ -430,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}; @@ -443,7 +369,7 @@ process_el({xmlstreamstart, <<"host">>, Attrs}, State) -> process_el({xmlstreamelement, #xmlel{name = <<"host">>, attrs = Attrs, children = Els}}, State) -> - JIDS = xml:get_attr_s(<<"jid">>, Attrs), + JIDS = fxml:get_attr_s(<<"jid">>, Attrs), case jid:from_string(JIDS) of #jid{lserver = S} -> case lists:member(S, ?MYHOSTS) of @@ -486,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 -> @@ -525,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} -> @@ -576,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 = 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, @@ -603,7 +527,6 @@ 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 = jid:make(U, S, <<"">>), case mod_private:process_sm_iq( @@ -614,8 +537,6 @@ 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 = jid:make(U, S, <<"">>), case mod_vcard:process_sm_iq( @@ -626,9 +547,8 @@ 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), + FromS = fxml:get_attr_s(<<"from">>, El#xmlel.attrs), case jid:from_string(FromS) of #jid{} = From -> To = jid:make(U, S, <<>>), @@ -643,9 +563,8 @@ 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), + FromS = fxml:get_attr_s(<<"from">>, El#xmlel.attrs), case jid:from_string(FromS) of #jid{} = From -> To = jid:make(U, S, <<>>), diff --git a/src/ejabberd_rdbms.erl b/src/ejabberd_rdbms.erl index ae405db0a..5224b035b 100644 --- a/src/ejabberd_rdbms.erl +++ b/src/ejabberd_rdbms.erl @@ -35,10 +35,10 @@ -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, + file:delete(ejabberd_sql:freetds_config()), + file:delete(ejabberd_sql:odbc_config()), + file:delete(ejabberd_sql:odbcinst_config()), + case lists:any(fun(H) -> needs_sql(H) /= false end, ?MYHOSTS) of true -> start_hosts(); @@ -49,34 +49,34 @@ start() -> %% Start relationnal DB module on the nodes where it is needed start_hosts() -> lists:foreach(fun (Host) -> - case needs_odbc(Host) of - {true, App} -> start_odbc(Host, App); + case needs_sql(Host) of + {true, App} -> start_sql(Host, App); false -> ok end end, ?MYHOSTS). -%% Start the ODBC module on the given host -start_odbc(Host, App) -> +%% Start the SQL module on the given host +start_sql(Host, App) -> ejabberd:start_app(App), Supervisor_name = gen_mod:get_module_proc(Host, - ejabberd_odbc_sup), + ejabberd_sql_sup), ChildSpec = {Supervisor_name, - {ejabberd_odbc_sup, start_link, [Host]}, transient, - infinity, supervisor, [ejabberd_odbc_sup]}, + {ejabberd_sql_sup, start_link, [Host]}, transient, + infinity, supervisor, [ejabberd_sql_sup]}, case supervisor:start_child(ejabberd_sup, ChildSpec) of {ok, _PID} -> ok; _Error -> ?ERROR_MSG("Start of supervisor ~p failed:~n~p~nRetrying." "..~n", [Supervisor_name, _Error]), - start_odbc(Host, App) + start_sql(Host, App) end. -%% Returns {true, App} if we have configured odbc for the given host -needs_odbc(Host) -> +%% Returns {true, App} if we have configured sql for the given host +needs_sql(Host) -> LHost = jid:nameprep(Host), - case ejabberd_config:get_option({odbc_type, LHost}, + case ejabberd_config:get_option({sql_type, LHost}, fun(mysql) -> mysql; (pgsql) -> pgsql; (sqlite) -> sqlite; @@ -91,11 +91,11 @@ needs_odbc(Host) -> undefined -> false end. -opt_type(odbc_type) -> +opt_type(sql_type) -> fun (mysql) -> mysql; (pgsql) -> pgsql; (sqlite) -> sqlite; (mssql) -> mssql; (odbc) -> odbc end; -opt_type(_) -> [odbc_type]. +opt_type(_) -> [sql_type]. diff --git a/src/ejabberd_receiver.erl b/src/ejabberd_receiver.erl index 785855294..0a33e30ec 100644 --- a/src/ejabberd_receiver.erl +++ b/src/ejabberd_receiver.erl @@ -48,15 +48,16 @@ -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)). + -spec start_link(inet:socket(), atom(), shaper:shaper(), non_neg_integer() | infinity) -> ignore | @@ -89,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}). @@ -129,8 +130,8 @@ init([Socket, SockMod, Shaper, MaxStanzaSize]) -> handle_call({starttls, TLSSocket}, _From, State) -> State1 = reset_parser(State), NewState = State1#state{socket = TLSSocket, - sock_mod = p1_tls}, - case p1_tls:recv_data(TLSSocket, <<"">>) of + sock_mod = fast_tls}, + case fast_tls:recv_data(TLSSocket, <<"">>) of {ok, TLSData} -> {reply, ok, process_data(TLSData, NewState), ?HIBERNATE_TIMEOUT}; @@ -140,6 +141,7 @@ handle_call({starttls, TLSSocket}, _From, State) -> handle_call({compress, Data}, _From, #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); @@ -160,7 +162,7 @@ handle_call(reset_stream, _From, State) -> Reply = ok, {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), @@ -182,8 +184,8 @@ handle_info({Tag, _TCPSocket, Data}, 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}; @@ -284,7 +286,7 @@ process_data(Data, undefined -> XMLStreamState; _ -> - xml_stream:parse(XMLStreamState, Data) + fxml_stream:parse(XMLStreamState, Data) end, {NewShaperState, Pause} = shaper:update(ShaperState, byte_size(Data)), if @@ -309,7 +311,7 @@ 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; @@ -317,14 +319,14 @@ reset_parser(#state{c2s_pid = C2SPid, max_stanza_size = MaxStanzaSize, xml_stream_state = XMLStreamState} = State) -> - NewStreamState = try xml_stream:reset(XMLStreamState) + NewStreamState = try fxml_stream:reset(XMLStreamState) catch error:_ -> close_stream(XMLStreamState), case C2SPid of undefined -> undefined; _ -> - xml_stream:new(C2SPid, MaxStanzaSize) + fxml_stream:new(C2SPid, MaxStanzaSize) end end, State#state{xml_stream_state = NewStreamState}. diff --git a/src/ejabberd_riak.erl b/src/ejabberd_riak.erl index 22da9d2e9..575810acc 100644 --- a/src/ejabberd_riak.erl +++ b/src/ejabberd_riak.erl @@ -28,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, @@ -68,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) -> @@ -429,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}}; @@ -517,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 319d1707d..af811441b 100644 --- a/src/ejabberd_riak_sup.erl +++ b/src/ejabberd_riak_sup.erl @@ -103,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))}}. @@ -131,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, @@ -162,6 +198,9 @@ 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_start_interval, riak_cacertfile, riak_username, riak_password]. diff --git a/src/ejabberd_router.erl b/src/ejabberd_router.erl index 5ca1262e6..e29d6acfb 100644 --- a/src/ejabberd_router.erl +++ b/src/ejabberd_router.erl @@ -36,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, @@ -55,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, {}). @@ -86,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. @@ -94,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 jid: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); @@ -115,46 +127,42 @@ 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). @@ -183,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); @@ -211,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 @@ -283,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 = @@ -394,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)) diff --git a/src/ejabberd_s2s.erl b/src/ejabberd_s2s.erl index 2f026b32a..be1ee4659 100644 --- a/src/ejabberd_s2s.erl +++ b/src/ejabberd_s2s.erl @@ -214,7 +214,7 @@ 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">>}; @@ -308,12 +308,14 @@ 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; _ -> - Err = jlib:make_error_reply(Packet, - ?ERR_SERVICE_UNAVAILABLE), + Lang = fxml:get_tag_attr_s(<<"xml:lang">>, Packet), + Txt = <<"No s2s connection found">>, + Err = jlib:make_error_reply( + Packet, ?ERRT_SERVICE_UNAVAILABLE(Lang, Txt)), ejabberd_router:route(To, From, Err) end, false diff --git a/src/ejabberd_s2s_in.erl b/src/ejabberd_s2s_in.erl index 29dd5e9df..d8d0a400a 100644 --- a/src/ejabberd_s2s_in.erl +++ b/src/ejabberd_s2s_in.erl @@ -85,16 +85,16 @@ -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)). start(SockData, Opts) -> supervisor:start_child(ejabberd_s2s_in_sup, @@ -188,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 @@ -199,7 +199,7 @@ wait_for_stream({xmlstreamstart, _Name, Attrs}, send_text(StateData, ?STREAM_HEADER(<<" version='1.0'">>)), Auth = if StateData#state.tls_enabled -> - case jid: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, @@ -234,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}; @@ -306,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 -> @@ -325,13 +325,13 @@ wait_for_feature_request({xmlstreamelement, El}, {s2s_tls_compression, StateData#state.server}, fun(true) -> true; (false) -> false - end, true) of + end, false) of true -> lists:delete(compression_none, TLSOpts1); false -> [compression_none | TLSOpts1] end, TLSSocket = (StateData#state.sockmod):starttls(Socket, TLSOpts, - xml:element_to_binary(#xmlel{name + fxml:element_to_binary(#xmlel{name = <<"proceed">>, attrs @@ -345,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, @@ -447,9 +447,9 @@ 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_s = fxml:get_attr_s(<<"from">>, Attrs), From = jid:from_string(From_s), - To_s = xml:get_attr_s(<<"to">>, Attrs), + To_s = fxml:get_attr_s(<<"to">>, Attrs), To = jid:from_string(To_s), if (To /= error) and (From /= error) -> LFrom = From#jid.lserver, @@ -626,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, @@ -643,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) -> diff --git a/src/ejabberd_s2s_out.erl b/src/ejabberd_s2s_out.erl index 696185739..a30f2f438 100644 --- a/src/ejabberd_s2s_out.erl +++ b/src/ejabberd_s2s_out.erl @@ -105,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}). @@ -192,7 +192,7 @@ init([From, Server, Type]) -> {s2s_tls_compression, From}, fun(true) -> true; (false) -> false - end, true) of + end, false) of false -> [compression_none | TLSOpts4]; true -> TLSOpts4 end, @@ -323,15 +323,15 @@ wait_for_stream({xmlstreamstart, _Name, Attrs}, true -> {no_verify, <<"Not verified">>, StateData} end, - RemoteStreamID = xml:get_attr_s(<<"id">>, Attrs), + RemoteStreamID = fxml:get_attr_s(<<"id">>, Attrs), NewStateData = StateData0#state{remote_streamid = RemoteStreamID}, - case {xml:get_attr_s(<<"xmlns">>, Attrs), - xml:get_attr_s(<<"xmlns:db">>, 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(<<"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)", @@ -495,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 -> @@ -508,7 +508,7 @@ wait_for_features({xmlstreamelement, El}, StateData) -> = Els2}) -> case - xml:get_cdata(Els2) + fxml:get_cdata(Els2) of <<"EXTERNAL">> -> true; @@ -533,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{} -> @@ -610,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)", @@ -637,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}]), @@ -653,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)", @@ -661,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}]), @@ -670,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)", @@ -679,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)", @@ -707,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}]), @@ -737,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)", @@ -985,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 @@ -997,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 = jid:from_string(xml:get_tag_attr_s(<<"from">>, + From = jid:from_string(fxml:get_tag_attr_s(<<"from">>, El)), - To = jid:from_string(xml:get_tag_attr_s(<<"to">>, + To = jid:from_string(fxml:get_tag_attr_s(<<"to">>, El)), ejabberd_router:route(To, From, Err) end. @@ -1070,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. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff --git a/src/ejabberd_service.erl b/src/ejabberd_service.erl index a9b771b89..465fb587a 100644 --- a/src/ejabberd_service.erl +++ b/src/ejabberd_service.erl @@ -91,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 @@ -166,9 +166,9 @@ 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), + To = fxml:get_attr_s(<<"to">>, Attrs), Host = jid:nameprep(To), if Host == error -> Header = io_lib:format(?STREAM_HEADER, @@ -180,7 +180,7 @@ wait_for_stream({xmlstreamstart, _Name, Attrs}, {stop, normal, StateData}; true -> Header = io_lib:format(?STREAM_HEADER, - [StateData#state.streamid, xml:crypt(To)]), + [StateData#state.streamid, fxml:crypt(To)]), send_text(StateData, Header), HostOpts = case dict:is_key(Host, StateData#state.host_opts) of true -> @@ -212,7 +212,7 @@ 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 dict:find(StateData#state.host, StateData#state.host_opts) of {ok, Password} -> @@ -222,7 +222,7 @@ wait_for_handshake({xmlstreamelement, El}, StateData) -> send_text(StateData, <<"<handshake/>">>), lists:foreach( fun (H) -> - ejabberd_router:register_route(H), + ejabberd_router:register_route(H, ?MYNAME), ?INFO_MSG("Route registered for service ~p~n", [H]) end, dict:fetch_keys(StateData#state.host_opts)), @@ -250,7 +250,7 @@ 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. @@ -269,7 +269,7 @@ stream_established({xmlstreamelement, El}, StateData) -> _ -> error end end, - To = xml:get_attr_s(<<"to">>, Attrs), + To = fxml:get_attr_s(<<"to">>, Attrs), ToJID = case To of <<"">> -> error; _ -> jid:from_string(To) @@ -280,7 +280,9 @@ stream_established({xmlstreamelement, El}, StateData) -> and (FromJID /= error) -> ejabberd_router:route(FromJID, ToJID, NewEl); true -> - Err = jlib:make_error_reply(NewEl, ?ERR_BAD_REQUEST), + Lang = fxml:get_tag_attr_s(<<"xml:lang">>, El), + Txt = <<"Incorrect stanza name or from/to JID">>, + Err = jlib:make_error_reply(NewEl, ?ERRT_BAD_REQUEST(Lang, Txt)), send_element(StateData, Err), error end, @@ -356,11 +358,13 @@ handle_info({route, From, To, Packet}, StateName, Attrs2 = jlib:replace_from_to_attrs(jid:to_string(From), jid:to_string(To), Attrs), - Text = xml:element_to_binary(#xmlel{name = Name, + Text = fxml:element_to_binary(#xmlel{name = Name, attrs = Attrs2, children = Els}), send_text(StateData, Text); deny -> - Err = jlib:make_error_reply(Packet, ?ERR_NOT_ALLOWED), + Lang = fxml:get_tag_attr_s(<<"xml:lang">>, Packet), + Txt = <<"Denied by ACL">>, + Err = jlib:make_error_reply(Packet, ?ERRT_NOT_ALLOWED(Lang, Txt)), ejabberd_router:route_error(To, From, Err, Packet) end, {next_state, StateName, StateData}; @@ -402,7 +406,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(). diff --git a/src/ejabberd_sm.erl b/src/ejabberd_sm.erl index 3045a417b..b7fc39502 100644 --- a/src/ejabberd_sm.erl +++ b/src/ejabberd_sm.erl @@ -35,6 +35,7 @@ -export([start/0, start_link/0, route/3, + process_iq/3, open_session/5, open_session/6, close_session/4, @@ -50,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, @@ -64,7 +66,8 @@ get_max_user_sessions/2, get_all_pids/0, is_existing_resource/3, - get_commands_spec/0 + get_commands_spec/0, + make_sid/0 ]). -export([init/1, handle_call/3, handle_cast/2, @@ -133,10 +136,10 @@ 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 = 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} -> [] @@ -157,8 +160,10 @@ check_in_subscription(Acc, User, Server, _JID, _Type, _Reason) -> -spec bounce_offline_message(jid(), jid(), xmlel()) -> stop. bounce_offline_message(From, To, Packet) -> - Err = jlib:make_error_reply(Packet, - ?ERR_SERVICE_UNAVAILABLE), + Lang = fxml:get_tag_attr_s(<<"xml:lang">>, Packet), + Txt = <<"User session not found">>, + Err = jlib:make_error_reply( + Packet, ?ERRT_SERVICE_UNAVAILABLE(Lang, Txt)), ejabberd_router:route(To, From, Err), stop. @@ -172,14 +177,14 @@ disconnect_removed_user(User, Server) -> get_user_resources(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), - Mod = get_sm_backend(), + 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)]. @@ -190,7 +195,7 @@ get_user_ip(User, Server, Resource) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), LResource = jid:resourceprep(Resource), - Mod = get_sm_backend(), + Mod = get_sm_backend(LServer), case Mod:get_sessions(LUser, LServer, LResource) of [] -> undefined; @@ -205,7 +210,7 @@ get_user_info(User, Server, Resource) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), LResource = jid:resourceprep(Resource), - Mod = get_sm_backend(), + Mod = get_sm_backend(LServer), case Mod:get_sessions(LUser, LServer, LResource) of [] -> offline; @@ -255,7 +260,7 @@ get_session_pid(User, Server, Resource) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), LResource = jid:resourceprep(Resource), - Mod = get_sm_backend(), + Mod = get_sm_backend(LServer), case Mod:get_sessions(LUser, LServer, LResource) of [#session{sid = {_, Pid}}] -> Pid; _ -> none @@ -264,33 +269,40 @@ 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 = jid:nameprep(Server), - Mod = get_sm_backend(), + 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 = jid:nameprep(Server), - Mod = get_sm_backend(), + Mod = get_sm_backend(LServer), length(Mod:get_sessions(LServer)). register_iq_handler(Host, XMLNS, Module, Fun) -> @@ -312,8 +324,7 @@ unregister_iq_handler(Host, XMLNS) -> %%==================================================================== 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) -> @@ -380,7 +391,7 @@ set_session(SID, User, Server, Resource, Priority, Info) -> 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}). @@ -397,7 +408,7 @@ do_route(From, To, {broadcast, _} = Packet) -> get_user_resources(To#jid.user, To#jid.server)); _ -> {U, S, R} = jid:tolower(To), - Mod = get_sm_backend(), + Mod = get_sm_backend(S), case Mod:get_sessions(U, S, R) of [] -> ?DEBUG("packet dropped~n", []); @@ -415,14 +426,15 @@ do_route(From, To, #xmlel{} = Packet) -> #jid{user = User, server = Server, luser = LUser, lserver = LServer, lresource = LResource} = To, #xmlel{name = Name, attrs = Attrs} = Packet, + Lang = fxml:get_attr_s(<<"xml:lang">>, Attrs), case LResource of <<"">> -> 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]), @@ -483,13 +495,14 @@ 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; <<"groupchat">> -> - Err = jlib:make_error_reply(Packet, - ?ERR_SERVICE_UNAVAILABLE), + ErrTxt = <<"Incorrect message type">>, + Err = jlib:make_error_reply( + Packet, ?ERRT_SERVICE_UNAVAILABLE(Lang, ErrTxt)), ejabberd_router:route(To, From, Err); _ -> route_message(From, To, Packet, normal) @@ -498,28 +511,32 @@ 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, - ?ERR_SERVICE_UNAVAILABLE), + ErrTxt = <<"Incorrect message type">>, + Err = jlib:make_error_reply( + Packet, + ?ERRT_SERVICE_UNAVAILABLE(Lang, ErrTxt)), 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; _ -> - Err = jlib:make_error_reply(Packet, - ?ERR_SERVICE_UNAVAILABLE), + ErrTxt = <<"User session not found">>, + Err = jlib:make_error_reply( + Packet, + ?ERRT_SERVICE_UNAVAILABLE(Lang, ErrTxt)), ejabberd_router:route(To, From, Err) end; _ -> ?DEBUG("packet dropped~n", []) @@ -565,7 +582,7 @@ route_message(From, To, Packet, Type) -> lists:foreach(fun ({P, R}) when P == Priority; (P >= 0) and (Type == headline) -> LResource = jid:resourceprep(R), - Mod = get_sm_backend(), + Mod = get_sm_backend(LServer), case Mod:get_sessions(LUser, LServer, LResource) of [] -> @@ -647,11 +664,11 @@ get_resource_sessions(User, Server, Resource) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), LResource = jid:resourceprep(Resource), - Mod = get_sm_backend(), + 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; @@ -676,7 +693,7 @@ get_max_user_sessions(LUser, Host) -> process_iq(From, To, Packet) -> IQ = jlib:iq_query_info(Packet), case IQ of - #iq{xmlns = XMLNS} -> + #iq{xmlns = XMLNS, lang = Lang} -> Host = To#jid.lserver, case ets:lookup(sm_iqtable, {XMLNS, Host}) of [{_, Module, Function}] -> @@ -689,8 +706,10 @@ process_iq(From, To, Packet) -> gen_iq_handler:handle(Host, Module, Function, Opts, From, To, IQ); [] -> - Err = jlib:make_error_reply(Packet, - ?ERR_SERVICE_UNAVAILABLE), + Txt = <<"No module is handling this query">>, + Err = jlib:make_error_reply( + Packet, + ?ERRT_SERVICE_UNAVAILABLE(Lang, Txt)), ejabberd_router:route(To, From, Err) end; reply -> ok; @@ -703,24 +722,38 @@ 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, 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; + (odbc) -> sql; + (sql) -> sql; (redis) -> redis 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 @@ -775,10 +808,14 @@ kick_user(User, Server) -> end, Resources), length(Resources). +make_sid() -> + {p1_time_compat:unique_timestamp(), self()}. + opt_type(sm_db_type) -> fun (mnesia) -> mnesia; (internal) -> mnesia; - (odbc) -> odbc; + (sql) -> sql; + (odbc) -> sql; (redis) -> redis end; opt_type(_) -> [sm_db_type]. diff --git a/src/ejabberd_sm_redis.erl b/src/ejabberd_sm_redis.erl index 637e13647..bf9e0eff5 100644 --- a/src/ejabberd_sm_redis.erl +++ b/src/ejabberd_sm_redis.erl @@ -107,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) -> @@ -204,7 +204,7 @@ 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; diff --git a/src/ejabberd_sm_odbc.erl b/src/ejabberd_sm_sql.erl index debeafe99..3d4e224d4 100644 --- a/src/ejabberd_sm_odbc.erl +++ b/src/ejabberd_sm_sql.erl @@ -6,7 +6,7 @@ %%% @end %%% Created : 9 Mar 2015 by Evgeny Khramtsov <ekhramtsov@process-one.net> %%%------------------------------------------------------------------- --module(ejabberd_sm_odbc). +-module(ejabberd_sm_sql). -behaviour(ejabberd_sm). @@ -29,11 +29,11 @@ %%%=================================================================== -spec init() -> ok | {error, any()}. init() -> - Node = ejabberd_odbc:escape(jlib:atom_to_binary(node())), + Node = ejabberd_sql:escape(jlib:atom_to_binary(node())), ?INFO_MSG("Cleaning SQL SM table...", []), lists:foldl( fun(Host, ok) -> - case ejabberd_odbc:sql_query( + case ejabberd_sql:sql_query( Host, [<<"delete from sm where node='">>, Node, <<"'">>]) of {updated, _} -> ok; @@ -43,18 +43,18 @@ 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}) -> - Username = ejabberd_odbc:escape(U), - Resource = ejabberd_odbc:escape(R), - InfoS = ejabberd_odbc:encode_term(Info), + Username = ejabberd_sql:escape(U), + Resource = ejabberd_sql:escape(R), + InfoS = ejabberd_sql:encode_term(Info), PrioS = enc_priority(Priority), TS = now_to_timestamp(Now), PidS = list_to_binary(erlang:pid_to_list(Pid)), - Node = ejabberd_odbc:escape(jlib:atom_to_binary(node(Pid))), - case odbc_queries:update( + Node = ejabberd_sql:escape(jlib:atom_to_binary(node(Pid))), + case sql_queries:update( LServer, <<"sm">>, [<<"usec">>, <<"pid">>, <<"node">>, <<"username">>, @@ -70,12 +70,12 @@ set_session(#session{sid = {Now, Pid}, usr = {U, LServer, R}, delete_session(_LUser, LServer, _LResource, {Now, Pid}) -> TS = now_to_timestamp(Now), PidS = list_to_binary(erlang:pid_to_list(Pid)), - case ejabberd_odbc:sql_query( + case ejabberd_sql:sql_query( LServer, [<<"select usec, pid, username, resource, priority, info ">>, <<"from sm where usec='">>, TS, <<"' and pid='">>,PidS, <<"'">>]) of {selected, _, [Row]} -> - ejabberd_odbc:sql_query( + ejabberd_sql:sql_query( LServer, [<<"delete from sm where usec='">>, TS, <<"' and pid='">>, PidS, <<"'">>]), {ok, row_to_session(LServer, Row)}; @@ -90,10 +90,10 @@ 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( + case ejabberd_sql:sql_query( LServer, [<<"select usec, pid, username, ">>, <<"resource, priority, info from sm">>]) of {selected, _, Rows} -> @@ -104,8 +104,8 @@ get_sessions(LServer) -> end. get_sessions(LUser, LServer) -> - Username = ejabberd_odbc:escape(LUser), - case ejabberd_odbc:sql_query( + Username = ejabberd_sql:escape(LUser), + case ejabberd_sql:sql_query( LServer, [<<"select usec, pid, username, ">>, <<"resource, priority, info from sm where ">>, <<"username='">>, Username, <<"'">>]) of @@ -117,9 +117,9 @@ get_sessions(LUser, LServer) -> end. get_sessions(LUser, LServer, LResource) -> - Username = ejabberd_odbc:escape(LUser), - Resource = ejabberd_odbc:escape(LResource), - case ejabberd_odbc:sql_query( + Username = ejabberd_sql:escape(LUser), + Resource = ejabberd_sql:escape(LResource), + case ejabberd_sql:sql_query( LServer, [<<"select usec, pid, username, ">>, <<"resource, priority, info from sm where ">>, <<"username='">>, Username, <<"' and resource='">>, @@ -162,7 +162,7 @@ row_to_session(LServer, [USec, PidS, User, Resource, PrioS, InfoS]) -> Now = timestamp_to_now(USec), Pid = erlang:list_to_pid(binary_to_list(PidS)), Priority = dec_priority(PrioS), - Info = ejabberd_odbc:decode_term(InfoS), + Info = ejabberd_sql:decode_term(InfoS), #session{sid = {Now, Pid}, us = {User, LServer}, usr = {User, LServer, Resource}, priority = Priority, diff --git a/src/ejabberd_socket.erl b/src/ejabberd_socket.erl index 16ffd192e..887b4a0f3 100644 --- a/src/ejabberd_socket.erl +++ b/src/ejabberd_socket.erl @@ -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(). @@ -145,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). @@ -216,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). diff --git a/src/ejabberd_odbc.erl b/src/ejabberd_sql.erl index a15c66b5d..4bee08f7e 100644 --- a/src/ejabberd_odbc.erl +++ b/src/ejabberd_sql.erl @@ -23,7 +23,7 @@ %%% %%%---------------------------------------------------------------------- --module(ejabberd_odbc). +-module(ejabberd_sql). -behaviour(ejabberd_config). @@ -41,6 +41,7 @@ sql_bloc/2, escape/1, escape_like/1, + escape_like_arg/1, to_bool/1, sqlite_db/1, sqlite_file/1, @@ -63,18 +64,20 @@ -include("ejabberd.hrl"). -include("logger.hrl"). +-include("ejabberd_sql_pt.hrl"). -record(state, {db_ref = self() :: pid(), 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}}). --define(STATE_KEY, ejabberd_odbc_state). +-define(STATE_KEY, ejabberd_sql_state). --define(NESTING_KEY, ejabberd_odbc_nesting_level). +-define(NESTING_KEY, ejabberd_sql_nesting_level). -define(TOP_LEVEL_TXN, 0). @@ -92,6 +95,8 @@ -define(KEEPALIVE_QUERY, [<<"SELECT 1;">>]). +-define(PREPARE_KEY, ejabberd_sql_prepare). + %%-define(DBGFSM, true). -ifdef(DBGFSM). @@ -108,19 +113,21 @@ %%% API %%%---------------------------------------------------------------------- start(Host) -> - (?GEN_FSM):start(ejabberd_odbc, [Host], + (?GEN_FSM):start(ejabberd_sql, [Host], fsm_limit_opts() ++ (?FSMOPTS)). start_link(Host, StartInterval) -> - (?GEN_FSM):start_link(ejabberd_odbc, + (?GEN_FSM):start_link(ejabberd_sql, [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(). @@ -150,7 +157,7 @@ sql_bloc(Host, F) -> sql_call(Host, {sql_bloc, F}). sql_call(Host, Msg) -> case get(?STATE_KEY) of undefined -> - case ejabberd_odbc_sup:get_random_pid(Host) of + case ejabberd_sql_sup:get_random_pid(Host) of none -> {error, <<"Unknown Host">>}; Pid -> (?GEN_FSM):sync_send_event(Pid,{sql_cmd, Msg, @@ -183,7 +190,7 @@ sql_query_t(Query) -> %% Escape character that will confuse an SQL engine escape(S) -> - << <<(odbc_queries:escape(Char))/binary>> || <<Char>> <= S >>. + << <<(sql_queries:escape(Char))/binary>> || <<Char>> <= S >>. %% Escape character that will confuse an SQL engine %% Percent and underscore only need to be escaped for pattern matching like @@ -192,7 +199,14 @@ escape_like(S) when is_binary(S) -> << <<(escape_like(C))/binary>> || <<C>> <= S >>; escape_like($%) -> <<"\\%">>; escape_like($_) -> <<"\\_">>; -escape_like(C) when is_integer(C), C >= 0, C =< 255 -> odbc_queries:escape(C). +escape_like(C) when is_integer(C), C >= 0, C =< 255 -> sql_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; @@ -218,7 +232,7 @@ sqlite_db(Host) -> -spec sqlite_file(binary()) -> string(). sqlite_file(Host) -> - case ejabberd_config:get_option({odbc_database, Host}, + case ejabberd_config:get_option({sql_database, Host}, fun iolist_to_binary/1) of undefined -> {ok, Cwd} = file:get_cwd(), @@ -233,7 +247,7 @@ sqlite_file(Host) -> %%%---------------------------------------------------------------------- init([Host, StartInterval]) -> case ejabberd_config:get_option( - {odbc_keepalive_interval, Host}, + {sql_keepalive_interval, Host}, fun(I) when is_integer(I), I>0 -> I end) of undefined -> ok; @@ -243,7 +257,7 @@ init([Host, StartInterval]) -> end, [DBType | _] = db_opts(Host), (?GEN_FSM):send_event(self(), connect), - ejabberd_odbc_sup:add_pid(Host, self()), + ejabberd_sql_sup:add_pid(Host, self()), {ok, connecting, #state{db_type = DBType, host = Host, max_pending_requests_len = max_fsm_queue(), @@ -255,19 +269,21 @@ connecting(connect, #state{host = Host} = State) -> [mysql | Args] -> apply(fun mysql_connect/5, Args); [pgsql | Args] -> apply(fun pgsql_connect/5, Args); [sqlite | Args] -> apply(fun sqlite_connect/1, Args); + [mssql | Args] -> apply(fun odbc_connect/1, Args); [odbc | Args] -> apply(fun odbc_connect/1, Args) 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", @@ -355,7 +371,7 @@ handle_info(Info, StateName, State) -> {next_state, StateName, State}. terminate(_Reason, _StateName, State) -> - ejabberd_odbc_sup:remove_pid(State#state.host, self()), + ejabberd_sql_sup:remove_pid(State#state.host, self()), case State#state.db_type of mysql -> catch p1_mysql_conn:stop(State#state.db_ref); sqlite -> catch sqlite3:close(sqlite_db(State#state.host)); @@ -469,12 +485,82 @@ 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); + mssql -> + 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, + to_odbc(odbc:sql_query(State#state.db_ref, [Query], + (?TRANSACTION_TIMEOUT) - 1000)); + mssql -> + to_odbc(odbc:sql_query(State#state.db_ref, [Query], (?TRANSACTION_TIMEOUT) - 1000)); pgsql -> pgsql_to_odbc(pgsql:squery(State#state.db_ref, Query)); @@ -495,6 +581,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, @@ -606,6 +779,18 @@ 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 @@ -658,6 +843,24 @@ to_odbc({error, Reason}) when is_list(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); @@ -666,14 +869,14 @@ log(Level, Format, Args) -> end. db_opts(Host) -> - Type = ejabberd_config:get_option({odbc_type, Host}, + Type = ejabberd_config:get_option({sql_type, Host}, fun(mysql) -> mysql; (pgsql) -> pgsql; (sqlite) -> sqlite; (mssql) -> mssql; (odbc) -> odbc end, odbc), - Server = ejabberd_config:get_option({odbc_server, Host}, + Server = ejabberd_config:get_option({sql_server, Host}, fun iolist_to_binary/1, <<"localhost">>), case Type of @@ -683,41 +886,40 @@ db_opts(Host) -> [sqlite, Host]; _ -> Port = ejabberd_config:get_option( - {odbc_port, Host}, + {sql_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), - DB = ejabberd_config:get_option({odbc_database, Host}, + DB = ejabberd_config:get_option({sql_database, Host}, fun iolist_to_binary/1, <<"ejabberd">>), - User = ejabberd_config:get_option({odbc_username, Host}, + User = ejabberd_config:get_option({sql_username, Host}, fun iolist_to_binary/1, <<"ejabberd">>), - Pass = ejabberd_config:get_option({odbc_password, Host}, + Pass = ejabberd_config:get_option({sql_password, Host}, fun iolist_to_binary/1, <<"">>), case Type of mssql -> - Username = get_mssql_user(Server, User), - [odbc, <<"DSN=", Host/binary, ";UID=", Username/binary, - ";PWD=", Pass/binary>>]; + [mssql, <<"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}, + Server = ejabberd_config:get_option({sql_server, Host}, fun iolist_to_binary/1, <<"localhost">>), Port = ejabberd_config:get_option( - {odbc_port, Host}, + {sql_port, Host}, fun(P) when is_integer(P), P > 0, P < 65536 -> P end, ?MSSQL_PORT), - DB = ejabberd_config:get_option({odbc_database, Host}, + DB = ejabberd_config:get_option({sql_database, Host}, fun iolist_to_binary/1, <<"ejabberd">>), FreeTDS = io_lib:fwrite("[~s]~n" @@ -762,21 +964,6 @@ init_mssql(Host) -> Err end. -get_mssql_user(Server, User) -> - HostName = case inet_parse:address(binary_to_list(Server)) of - {ok, _} -> - Server; - {error, _} -> - hd(str:tokens(Server, <<".">>)) - end, - UserName = case str:chr(User, $@) of - 0 -> - <<User/binary, $@, HostName/binary>>; - _ -> - User - end, - UserName. - tmp_dir() -> filename:join(["/tmp", "ejabberd"]). @@ -800,30 +987,39 @@ fsm_limit_opts() -> _ -> [] 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) -> - ?ERROR_MSG("SQL query '~s' failed: ~p", [Query, Why]), + 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) -> +opt_type(sql_database) -> fun iolist_to_binary/1; +opt_type(sql_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) -> +opt_type(sql_password) -> fun iolist_to_binary/1; +opt_type(sql_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) -> +opt_type(sql_server) -> fun iolist_to_binary/1; +opt_type(sql_type) -> fun (mysql) -> mysql; (pgsql) -> pgsql; (sqlite) -> sqlite; (mssql) -> mssql; (odbc) -> odbc end; -opt_type(odbc_username) -> fun iolist_to_binary/1; +opt_type(sql_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]. + [max_fsm_queue, sql_database, sql_keepalive_interval, + sql_password, sql_port, sql_server, sql_type, + sql_username]. diff --git a/src/ejabberd_sql_pt.erl b/src/ejabberd_sql_pt.erl new file mode 100644 index 000000000..47e4a07c7 --- /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_sql), + 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_sql), + 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_sql), + 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_sql), + 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_odbc_sup.erl b/src/ejabberd_sql_sup.erl index 65fa3c1c6..682414557 100644 --- a/src/ejabberd_odbc_sup.erl +++ b/src/ejabberd_sql_sup.erl @@ -1,7 +1,7 @@ %%%---------------------------------------------------------------------- -%%% File : ejabberd_odbc_sup.erl +%%% File : ejabberd_sql_sup.erl %%% Author : Alexey Shchepin <alexey@process-one.net> -%%% Purpose : ODBC connections supervisor +%%% Purpose : SQL connections supervisor %%% Created : 22 Dec 2004 by Alexey Shchepin <alexey@process-one.net> %%% %%% @@ -23,7 +23,7 @@ %%% %%%---------------------------------------------------------------------- --module(ejabberd_odbc_sup). +-module(ejabberd_sql_sup). -behaviour(ejabberd_config). @@ -42,7 +42,7 @@ -define(DEFAULT_POOL_SIZE, 10). --define(DEFAULT_ODBC_START_INTERVAL, 30). +-define(DEFAULT_SQL_START_INTERVAL, 30). -define(CONNECT_TIMEOUT, 500). @@ -62,14 +62,14 @@ start_link(Host) -> init([Host]) -> PoolSize = ejabberd_config:get_option( - {odbc_pool_size, Host}, + {sql_pool_size, Host}, fun(I) when is_integer(I), I>0 -> I end, ?DEFAULT_POOL_SIZE), StartInterval = ejabberd_config:get_option( - {odbc_start_interval, Host}, + {sql_start_interval, Host}, fun(I) when is_integer(I), I>0 -> I end, - ?DEFAULT_ODBC_START_INTERVAL), - Type = ejabberd_config:get_option({odbc_type, Host}, + ?DEFAULT_SQL_START_INTERVAL), + Type = ejabberd_config:get_option({sql_type, Host}, fun(mysql) -> mysql; (pgsql) -> pgsql; (sqlite) -> sqlite; @@ -80,7 +80,7 @@ init([Host]) -> sqlite -> check_sqlite_db(Host); mssql -> - ejabberd_odbc:init_mssql(Host); + ejabberd_sql:init_mssql(Host); _ -> ok end, @@ -89,7 +89,7 @@ init([Host]) -> {{one_for_one, PoolSize * 10, 1}, lists:map(fun (I) -> {I, - {ejabberd_odbc, start_link, + {ejabberd_sql, start_link, [Host, StartInterval * 1000]}, transient, 2000, worker, [?MODULE]} end, @@ -121,12 +121,12 @@ transform_options(Opts) -> lists:foldl(fun transform_options/2, [], Opts). transform_options({odbc_server, {Type, Server, Port, DB, User, Pass}}, Opts) -> - [{odbc_type, Type}, - {odbc_server, Server}, - {odbc_port, Port}, - {odbc_database, DB}, - {odbc_username, User}, - {odbc_password, Pass}|Opts]; + [{sql_type, Type}, + {sql_server, Server}, + {sql_port, Port}, + {sql_database, DB}, + {sql_username, User}, + {sql_password, Pass}|Opts]; transform_options({odbc_server, {mysql, Server, DB, User, Pass}}, Opts) -> transform_options({odbc_server, {mysql, Server, ?MYSQL_PORT, DB, User, Pass}}, Opts); transform_options({odbc_server, {pgsql, Server, DB, User, Pass}}, Opts) -> @@ -137,8 +137,8 @@ transform_options(Opt, Opts) -> [Opt|Opts]. check_sqlite_db(Host) -> - DB = ejabberd_odbc:sqlite_db(Host), - File = ejabberd_odbc:sqlite_file(Host), + DB = ejabberd_sql:sqlite_db(Host), + File = ejabberd_sql:sqlite_file(Host), Ret = case filelib:ensure_dir(File) of ok -> case sqlite3:open(DB, [{file, File}]) of @@ -211,11 +211,11 @@ read_lines(Fd, File, Acc) -> [] end. -opt_type(odbc_pool_size) -> +opt_type(sql_pool_size) -> fun (I) when is_integer(I), I > 0 -> I end; -opt_type(odbc_start_interval) -> +opt_type(sql_start_interval) -> fun (I) when is_integer(I), I > 0 -> I end; -opt_type(odbc_type) -> +opt_type(sql_type) -> fun (mysql) -> mysql; (pgsql) -> pgsql; (sqlite) -> sqlite; @@ -223,4 +223,4 @@ opt_type(odbc_type) -> (odbc) -> odbc end; opt_type(_) -> - [odbc_pool_size, odbc_start_interval, odbc_type]. + [sql_pool_size, sql_start_interval, sql_type]. diff --git a/src/ejabberd_stun.erl b/src/ejabberd_stun.erl index f7ae19cd4..3c91117d1 100644 --- a/src/ejabberd_stun.erl +++ b/src/ejabberd_stun.erl @@ -38,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_system_monitor.erl b/src/ejabberd_system_monitor.erl index 9a0213c1d..3f6c05667 100644 --- a/src/ejabberd_system_monitor.erl +++ b/src/ejabberd_system_monitor.erl @@ -71,7 +71,7 @@ process_command(From, To, Packet) -> 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), @@ -186,18 +186,24 @@ process_large_heap(Pid, Info) -> "much memory:~n~p~n~s", [node(), Pid, Info, DetailedInfo])), From = jid:make(<<"">>, Host, <<"watchdog">>), + Hint = [#xmlel{name = <<"no-permanent-store">>, + attrs = [{<<"xmlns">>, ?NS_HINTS}]}], lists:foreach(fun (JID) -> - send_message(From, jid:make(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( diff --git a/src/ejabberd_web_admin.erl b/src/ejabberd_web_admin.erl index d711f2fc3..3281f6430 100644 --- a/src/ejabberd_web_admin.erl +++ b/src/ejabberd_web_admin.erl @@ -264,7 +264,7 @@ get_auth_admin(Auth, HostHTTP, RPath, Method) -> get_auth_account(HostOfRule, AccessRule, User, Server, Pass) -> - case ejabberd_auth:check_password(User, Server, Pass) of + case ejabberd_auth:check_password(User, <<"">>, Server, Pass) of true -> case is_acl_match(HostOfRule, AccessRule, jid:make(User, Server, <<"">>)) @@ -383,6 +383,9 @@ css(Host) -> " 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" @@ -461,13 +464,15 @@ css(Host) -> " 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" - " position: absolute;\n" - " top: 75px;\n" - " left: 0;\n" " padding: 0;\n" " margin: 0;\n" - " width: 17em;\n" + " width: 90%;\n" " background: #fff;\n" "}\n" "#navigation ul li {\n" @@ -512,6 +517,9 @@ css(Host) -> " background: #3eaffa;\n" " color: #fff;\n" "}\n" + "thead tr td a {\n" + " color: #fff;\n" + "}\n" "td.copy {\n" " text-align: center;\n" "}\n" @@ -592,8 +600,10 @@ css(Host) -> " list-style-type: none;\n" "}\n" "#content {\n" - " padding-left: 19em;\n" + " display: inline-block;\n" + " vertical-align: top;\n" " padding-top: 25px;\n" + " width: 70%;\n" "}\n" "div.guidelink,\n" "p[dir=ltr] {\n" @@ -735,7 +745,7 @@ process_admin(Host, [{{acl, '$1', '$2'}}]}])), {NumLines, ACLsP} = term_to_paragraph(ACLs, 80), make_xhtml((?H1GL((?T(<<"Access Control Lists">>)), - <<"acl-definition">>, <<"ACL Definition">>)) + <<"acldefinition">>, <<"ACL Definition">>)) ++ case Res of ok -> [?XREST(<<"Submitted">>)]; @@ -771,7 +781,7 @@ process_admin(Host, [{{acl, {'$1', Host}, '$2'}, [], [{{acl, '$1', '$2'}}]}])), make_xhtml((?H1GL((?T(<<"Access Control Lists">>)), - <<"acl-definition">>, <<"ACL Definition">>)) + <<"acldefinition">>, <<"ACL Definition">>)) ++ case Res of ok -> [?XREST(<<"Submitted">>)]; @@ -837,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">>)), - <<"access-rights">>, <<"Access Rights">>)) + <<"accessrights">>, <<"Access Rights">>)) ++ case Res of ok -> [?XREST(<<"Submitted">>)]; @@ -870,7 +880,7 @@ process_admin(Host, [{{access, {'$1', Host}, '$2'}, [], [{{access, '$1', '$2'}}]}]), make_xhtml((?H1GL((?T(<<"Access Rules">>)), - <<"access-rights">>, <<"Access Rights">>)) + <<"accessrights">>, <<"Access Rights">>)) ++ case Res of ok -> [?XREST(<<"Submitted">>)]; @@ -929,7 +939,7 @@ process_admin(global, lang = Lang}) -> Res = list_vhosts(Lang, AJID), make_xhtml((?H1GL((?T(<<"Virtual Hosts">>)), - <<"virtual-hosting">>, <<"Virtual Hosting">>)) + <<"virtualhosting">>, <<"Virtual Hosting">>)) ++ Res, global, Lang, AJID); process_admin(Host, @@ -1510,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) -> @@ -2114,7 +2123,7 @@ get_node(global, Node, [<<"ports">>], Query, Lang) -> []])), H1String = <<(?T(<<"Listened Ports at ">>))/binary, (iolist_to_binary(atom_to_list(Node)))/binary>>, - (?H1GL(H1String, <<"listening-ports">>, <<"Listening Ports">>)) + (?H1GL(H1String, <<"listeningports">>, <<"Listening Ports">>)) ++ case Res of ok -> [?XREST(<<"Submitted">>)]; @@ -2142,7 +2151,7 @@ get_node(Host, Node, [<<"modules">>], Query, Lang) 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, <<"modules-overview">>, + (?H1GL(H1String, <<"modulesoverview">>, <<"Modules Overview">>)) ++ case Res of @@ -2405,7 +2414,7 @@ node_backup_parse_query(Node, Query) -> lists:keysearch(<<Action/binary, "host">>, 1, Query), - ejabberd_cluster:call(Node, ejd2odbc, + ejabberd_cluster:call(Node, ejd2sql, export, [Host, Path]); <<"import_file">> -> ejabberd_cluster:call(Node, ejabberd_admin, @@ -2722,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)]] @@ -2741,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) -> diff --git a/src/ejabberd_websocket.erl b/src/ejabberd_websocket.erl index d9a1bafc7..0cdd9bac5 100644 --- a/src/ejabberd_websocket.erl +++ b/src/ejabberd_websocket.erl @@ -373,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 6b25adc4c..c7e72d66d 100644 --- a/src/ejabberd_xmlrpc.erl +++ b/src/ejabberd_xmlrpc.erl @@ -228,13 +228,13 @@ process(_, #request{method = 'POST', data = Data, opts = Opts}) -> 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]), @@ -244,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 @@ -491,7 +491,7 @@ 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)}]}; diff --git a/src/ejd2odbc.erl b/src/ejd2sql.erl index 4cb937ef9..aa74286ed 100644 --- a/src/ejd2odbc.erl +++ b/src/ejd2sql.erl @@ -1,5 +1,5 @@ %%%---------------------------------------------------------------------- -%%% File : ejd2odbc.erl +%%% File : ejd2sql.erl %%% Author : Alexey Shchepin <alexey@process-one.net> %%% Purpose : Export some mnesia tables to SQL DB %%% Created : 22 Aug 2005 by Alexey Shchepin <alexey@process-one.net> @@ -23,14 +23,14 @@ %%% %%%---------------------------------------------------------------------- --module(ejd2odbc). +-module(ejd2sql). -author('alexey@process-one.net'). -include("logger.hrl"). -export([export/2, export/3, import_file/2, import/2, - import/3]). + import/3, delete/1]). -define(MAX_RECORDS_PER_TRANSACTION, 100). @@ -43,7 +43,7 @@ %%% A table can be converted from Mnesia to an ODBC database by calling %%% one of the API function with the following parameters: %%% - Server is the server domain you want to convert -%%% - Output can be either odbc to export to the configured relational +%%% - Output can be either sql to export to the configured relational %%% database or "Filename" to export to text file. modules() -> @@ -80,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) -> @@ -154,17 +168,36 @@ export(LServer, Table, IO, ConvertFun) -> output(_LServer, _Table, _IO, []) -> ok; -output(LServer, _Table, odbc, SQLs) -> - ejabberd_odbc:sql_transaction(LServer, SQLs); +output(LServer, _Table, sql, SQLs) -> + ejabberd_sql:sql_transaction(LServer, SQLs); 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 -> fun() -> - case ejabberd_odbc:sql_query_t(SelectQuery) of + case ejabberd_sql:sql_query_t(SelectQuery) of {selected, _, Rows} -> lists:foldl(fun process_sql_row/2, {IO, ConvertFun, undefined}, Rows); @@ -174,16 +207,16 @@ import(LServer, SelectQuery, IO, ConvertFun, Opts) -> end; false -> fun() -> - ejabberd_odbc:sql_query_t( + ejabberd_sql:sql_query_t( [iolist_to_binary( [<<"declare c cursor for ">>, SelectQuery])]), fetch(IO, ConvertFun, undefined) end end, - ejabberd_odbc:sql_transaction(LServer, F). + ejabberd_sql:sql_transaction(LServer, F). fetch(IO, ConvertFun, PrevRow) -> - case ejabberd_odbc:sql_query_t([<<"fetch c;">>]) of + case ejabberd_sql:sql_query_t([<<"fetch c;">>]) of {selected, _, [Row]} -> process_sql_row(Row, {IO, ConvertFun, PrevRow}), fetch(IO, ConvertFun, Row); diff --git a/src/elixir_logger_backend.erl b/src/elixir_logger_backend.erl new file mode 100644 index 000000000..c055853f7 --- /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 46ece873c..91526e71f 100644 --- a/src/ext_mod.erl +++ b/src/ext_mod.erl @@ -509,12 +509,11 @@ compile(_Module, _Spec, DestDir) -> 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, 'P1LOGGER'} || 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; @@ -589,10 +588,10 @@ rebar_dep({App, _, {git, Url, 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) -> diff --git a/src/gen_mod.erl b/src/gen_mod.erl index c45642d47..26e662dc6 100644 --- a/src/gen_mod.erl +++ b/src/gen_mod.erl @@ -35,7 +35,8 @@ 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]). + start_modules/0, start_modules/1, stop_modules/0, stop_modules/1, + default_db/1, v_db/1, opt_type/1, db_mod/2, db_mod/3]). %%-export([behaviour_info/1]). @@ -47,7 +48,7 @@ opts = [] :: opts() | '_' | '$2'}). -type opts() :: [{atom(), any()}]. --type db_type() :: odbc | mnesia | riak. +-type db_type() :: sql | mnesia | riak. -callback start(binary(), opts()) -> any(). -callback stop(binary()) -> any(). @@ -64,23 +65,38 @@ start() -> {keypos, #ejabberd_module.module_host}]), ok. +-spec start_modules() -> any(). + +%% Start all the modules in all the hosts +start_modules() -> + lists:foreach( + fun(Host) -> + start_modules(Host) + end, ?MYHOSTS). + +get_modules_options(Host) -> + ejabberd_config:get_option( + {modules, Host}, + fun(Mods) -> + lists:map( + fun({M, A}) when is_atom(M), is_list(A) -> + {M, A} + end, Mods) + end, []). + -spec start_modules(binary()) -> any(). start_modules(Host) -> - Modules = ejabberd_config:get_option( - {modules, Host}, - fun(L) when is_list(L) -> L end, []), + Modules = get_modules_options(Host), lists:foreach( - fun({Module, Opts}) -> - start_module(Host, Module, Opts) - end, Modules). + fun({Module, Opts}) -> + start_module(Host, Module, Opts) + end, Modules). -spec start_module(binary(), atom()) -> any(). start_module(Host, Module) -> - Modules = ejabberd_config:get_option( - {modules, Host}, - fun(L) when is_list(L) -> L end, []), + Modules = get_modules_options(Host), case lists:keyfind(Module, 1, Modules) of {_, Opts} -> start_module(Host, Module, Opts); @@ -121,6 +137,23 @@ is_app_running(AppName) -> lists:keymember(AppName, 1, application:which_applications(Timeout)). +-spec stop_modules() -> any(). + +stop_modules() -> + lists:foreach( + fun(Host) -> + stop_modules(Host) + end, ?MYHOSTS). + +-spec stop_modules(binary()) -> any(). + +stop_modules(Host) -> + Modules = get_modules_options(Host), + lists:foreach( + fun({Module, _Args}) -> + gen_mod:stop_module_keep_config(Host, Module) + end, Modules). + -spec stop_module(binary(), atom()) -> error | {aborted, any()} | {atomic, any()}. stop_module(Host, Module) -> @@ -264,7 +297,8 @@ validate_opts(Module, Opts) -> -spec v_db(db_type() | internal) -> db_type(). -v_db(odbc) -> odbc; +v_db(odbc) -> sql; +v_db(sql) -> sql; v_db(internal) -> mnesia; v_db(mnesia) -> mnesia; v_db(riak) -> riak. @@ -286,6 +320,20 @@ db_type(Host, Opts) when is_list(Opts) -> default_db(Host) -> ejabberd_config:get_option({default_db, Host}, fun v_db/1, mnesia). +-spec db_mod(binary() | global | db_type(), module()) -> module(). + +db_mod(odbc, Module) -> list_to_atom(atom_to_list(Module) ++ "_sql"); +db_mod(sql, Module) -> list_to_atom(atom_to_list(Module) ++ "_sql"); +db_mod(mnesia, Module) -> list_to_atom(atom_to_list(Module) ++ "_mnesia"); +db_mod(riak, Module) -> list_to_atom(atom_to_list(Module) ++ "_riak"); +db_mod(Host, Module) when is_binary(Host) orelse Host == global -> + db_mod(db_type(Host, Module), Module). + +-spec db_mod(binary() | global, opts(), module()) -> module(). + +db_mod(Host, Opts, Module) when is_list(Opts) -> + db_mod(db_type(Host, Opts), Module). + -spec loaded_modules(binary()) -> [atom()]. loaded_modules(Host) -> diff --git a/src/jd2ejd.erl b/src/jd2ejd.erl index cda8d11ce..099387c9a 100644 --- a/src/jd2ejd.erl +++ b/src/jd2ejd.erl @@ -48,7 +48,7 @@ import_file(File) -> 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} -> @@ -113,16 +113,16 @@ process_xdb(User, Server, xdb_data(_User, _Server, {xmlcdata, _CData}) -> ok; xdb_data(User, Server, #xmlel{attrs = Attrs} = El) -> From = jid:make(User, Server, <<"">>), - case xml:get_attr_s(<<"xmlns">>, Attrs) of + 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), @@ -136,7 +136,7 @@ xdb_data(User, Server, #xmlel{attrs = Attrs} = El) -> <<"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, jid:make(<<"">>, Server, @@ -160,7 +160,7 @@ xdb_data(User, Server, #xmlel{attrs = Attrs} = El) -> process_offline(Server, To, #xmlel{children = Els}) -> 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 <<"">> -> jid:make(<<"">>, Server, <<"">>); diff --git a/src/jid.erl b/src/jid.erl index cc387ecbe..7bdd652ae 100644 --- a/src/jid.erl +++ b/src/jid.erl @@ -87,9 +87,13 @@ split(#jid{user = U, server = S, resource = R}) -> split(_) -> error. --spec from_string(binary()) -> jid() | error. - -from_string(S) -> +-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, diff --git a/src/jlib.erl b/src/jlib.erl index 61aaf690e..8eaebbc80 100644 --- a/src/jlib.erl +++ b/src/jlib.erl @@ -92,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 @@ -133,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 @@ -159,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, @@ -299,8 +299,8 @@ 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(_) -> <<"">>. @@ -326,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}; @@ -336,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}; @@ -399,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 @@ -418,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 -> @@ -437,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). @@ -446,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) @@ -455,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. @@ -535,7 +535,7 @@ is_standalone_chat_state(#xmlel{name = <<"message">>} = El) -> <<"paused">>], Stripped = lists:foldl(fun(ChatState, AccEl) -> - xml:remove_subtags(AccEl, ChatState, + fxml:remove_subtags(AccEl, ChatState, {<<"xmlns">>, ?NS_CHATSTATES}) end, El, ChatStates), case Stripped of @@ -558,15 +558,15 @@ add_delay_info(El, From, Time) -> binary()) -> xmlel(). add_delay_info(El, From, Time, Desc) -> - case xml:get_subtag_with_xmlns(El, <<"delay">>, ?NS_DELAY) of + case fxml:get_subtag_with_xmlns(El, <<"delay">>, ?NS_DELAY) of false -> %% Add new tag DelayTag = create_delay_tag(Time, From, Desc), - xml:append_subtags(El, [DelayTag]); + 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, <<"">>} -> @@ -582,8 +582,8 @@ add_delay_info(El, From, Time, Desc) -> DelayTag#xmlel{children = [{xmlcdata, OldDesc}]} end end, - NewEl = xml:remove_subtags(El, <<"delay">>, {<<"xmlns">>, ?NS_DELAY}), - 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()) diff --git a/src/mod_adhoc.erl b/src/mod_adhoc.erl index 9947c84ac..9e0682f7d 100644 --- a/src/mod_adhoc.erl +++ b/src/mod_adhoc.erl @@ -233,7 +233,7 @@ process_sm_iq(From, To, IQ) -> process_adhoc_request(From, To, IQ, adhoc_sm_commands). process_adhoc_request(From, To, - #iq{sub_el = SubEl} = IQ, Hook) -> + #iq{sub_el = SubEl, lang = Lang} = IQ, Hook) -> ?DEBUG("About to parse ~p...", [IQ]), case adhoc:parse_request(IQ) of {error, Error} -> @@ -245,8 +245,9 @@ process_adhoc_request(From, To, of ignore -> ignore; empty -> + Txt = <<"No hook has processed this command">>, IQ#iq{type = error, - sub_el = [SubEl, ?ERR_ITEM_NOT_FOUND]}; + sub_el = [SubEl, ?ERRT_ITEM_NOT_FOUND(Lang, Txt)]}; {error, Error} -> IQ#iq{type = error, sub_el = [SubEl, Error]}; Command -> IQ#iq{type = result, sub_el = [Command]} @@ -277,7 +278,9 @@ ping_command(_Acc, _From, _To, [{<<"info">>, translate:translate(Lang, <<"Pong">>)}]}); - true -> {error, ?ERR_BAD_REQUEST} + true -> + Txt = <<"Incorrect value of 'action' attribute">>, + {error, ?ERRT_BAD_REQUEST(Lang, Txt)} end; ping_command(Acc, _From, _To, _Request) -> Acc. diff --git a/src/mod_admin_extra.erl b/src/mod_admin_extra.erl index dae775e47..b12a80007 100644 --- a/src/mod_admin_extra.erl +++ b/src/mod_admin_extra.erl @@ -31,7 +31,7 @@ -include("logger.hrl"). -export([start/2, stop/1, compile/1, get_cookie/0, - remove_node/1, set_password/3, + remove_node/1, set_password/3, check_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, @@ -101,89 +101,156 @@ get_commands_spec() -> 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}}, - + 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, + module = ?MODULE, 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, string}, {hashmethod, string}], - 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, @@ -464,7 +531,7 @@ get_commands_spec() -> tags = [offline], desc = "Get the number of unread offline messages", policy = user, - module = mod_offline, function = get_queue_length, + module = mod_offline, function = count_offline_messages, args = [], result = {res, integer}}, #ejabberd_commands{name = send_message, tags = [stanza], @@ -523,36 +590,38 @@ remove_node(Node) -> %%% set_password(User, Host, Password) -> - case ejabberd_auth:set_password(User, Host, Password) of - ok -> - ok; - _ -> - error - end. + Fun = fun () -> ejabberd_auth:set_password(User, Host, Password) end, + user_action(User, Host, Fun, ok). + +check_password(User, Host, Password) -> + ejabberd_auth:check_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 {AccountPass, HashMethod} of {A, _} when is_tuple(A) -> scrammed; - {_, "md5"} -> get_md5(AccountPass); - {_, "sha"} -> get_sha(AccountPass); - _ -> undefined + {_, <<"md5">>} -> get_md5(AccountPass); + {_, <<"sha">>} -> get_sha(AccountPass); + {_, Method} -> + ?ERROR_MSG("check_password_hash called " + "with hash method: ~p", [Method]), + undefined end, case AccountPassHash of scrammed -> - ?ERROR_MSG("Passwords are scrammed, and check_password_hash can not work.", []), + ?ERROR_MSG("Passwords are scrammed, and check_password_hash cannot work.", []), throw(passwords_scrammed_command_cannot_work); - undefined -> error; + undefined -> throw(unkown_hash_method); PasswordHash -> ok; - _ -> error + _ -> false end. get_md5(AccountPass) -> - lists:flatten([io_lib:format("~.16B", [X]) - || X <- binary_to_list(erlang:md5(AccountPass))]). + iolist_to_binary([io_lib:format("~2.16.0B", [X]) + || X <- binary_to_list(erlang:md5(AccountPass))]). get_sha(AccountPass) -> - lists:flatten([io_lib:format("~.16B", [X]) - || X <- binary_to_list(p1_sha:sha1(AccountPass))]). + iolist_to_binary([io_lib:format("~2.16.0B", [X]) + || X <- binary_to_list(p1_sha:sha1(AccountPass))]). num_active_users(Host, Days) -> list_last_activity(Host, true, Days). @@ -681,21 +750,7 @@ 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 = jid:nodeprep(User), - LServer = jid: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), @@ -729,7 +784,8 @@ resource_num(User, Host, Num) -> true -> lists:nth(Num, Resources); false -> - lists:flatten(io_lib:format("Error: Wrong resource number: ~p", [Num])) + throw({bad_argument, + lists:flatten(io_lib:format("Wrong resource number: ~p", [Num]))}) end. kick_session(User, Server, Resource, ReasonText) -> @@ -794,7 +850,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). @@ -912,7 +969,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) @@ -933,7 +990,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 @@ -963,7 +1020,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); @@ -1126,7 +1183,7 @@ push_roster_item(LU, LS, R, 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">>, @@ -1206,10 +1263,10 @@ private_get(Username, Host, Element, Ns) -> [{xmlel, <<"query">>, [{<<"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]), @@ -1255,8 +1312,7 @@ srg_get_info(Group, Host) -> Os when is_list(Os) -> Os; error -> [] end, - [{jlib:atom_to_binary(Title), - io_lib:format("~p", [btl(Value)])} || {Title, Value} <- Opts]. + [{jlib:atom_to_binary(Title), btl(Value)} || {Title, Value} <- Opts]. btl([]) -> []; btl([B|L]) -> [btl(B)|btl(L)]; @@ -1333,7 +1389,7 @@ build_packet(Type, Subject, Body) -> }. send_stanza(FromString, ToString, Stanza) -> - case xml_stream:parse_element(Stanza) of + case fxml_stream:parse_element(Stanza) of {error, Error} -> {error, Error}; XmlEl -> @@ -1344,7 +1400,7 @@ send_stanza(FromString, ToString, Stanza) -> end. send_stanza_c2s(Username, Host, Resource, Stanza) -> - case {xml_stream:parse_element(Stanza), + case {fxml_stream:parse_element(Stanza), ejabberd_sm:get_session_pid(Username, Host, Resource)} of {{error, Error}, _} -> @@ -1358,7 +1414,7 @@ send_stanza_c2s(Username, Host, Resource, Stanza) -> privacy_set(Username, Host, QueryS) -> From = jid:make(Username, Host, <<"">>), To = jid:make(<<"">>, Host, <<"">>), - QueryEl = xml_stream:parse_element(QueryS), + QueryEl = fxml_stream:parse_element(QueryS), StanzaEl = {xmlel, <<"iq">>, [{<<"type">>, <<"set">>}], [QueryEl]}, IQ = jlib:iq_query_info(StanzaEl), ejabberd_hooks:run_fold( @@ -1515,6 +1571,20 @@ decide_rip_jid({UName, UServer}, Match_list) -> end, Match_list). +user_action(User, Server, Fun, OK) -> + case ejabberd_auth:is_user_exists(User, Server) of + true -> + case catch Fun() of + OK -> ok; + {error, Error} -> throw(Error); + Error -> + ?ERROR_MSG("Command returned: ~p", [Error]), + 1 + end; + false -> + throw({not_found, "unknown_user"}) + end. + %% Copied from ejabberd-2.0.0/src/acl.erl is_regexp_match(String, RegExp) -> case ejabberd_regexp:run(String, RegExp) of diff --git a/src/mod_announce.erl b/src/mod_announce.erl index 3d0924a6d..d7251c50b 100644 --- a/src/mod_announce.erl +++ b/src/mod_announce.erl @@ -41,11 +41,16 @@ -include("logger.hrl"). -include("jlib.hrl"). -include("adhoc.hrl"). +-include("mod_announce.hrl"). --record(motd, {server = <<"">> :: binary(), - packet = #xmlel{} :: xmlel()}). --record(motd_users, {us = {<<"">>, <<"">>} :: {binary(), binary()} | '$1', - dummy = [] :: [] | '_'}). +-callback init(binary(), gen_mod:opts()) -> any(). +-callback import(binary(), #motd{} | #motd_users{}) -> ok | pass. +-callback set_motd_users(binary(), [{binary(), binary(), binary()}]) -> {atomic, any()}. +-callback set_motd(binary(), xmlel()) -> {atomic, any()}. +-callback delete_motd(binary()) -> {atomic, any()}. +-callback get_motd(binary()) -> {ok, xmlel()} | error. +-callback is_motd_user(binary(), binary()) -> boolean(). +-callback set_motd_user(binary(), binary()) -> {atomic, any()}. -define(PROCNAME, ejabberd_announce). @@ -55,20 +60,8 @@ tokenize(Node) -> str:tokens(Node, <<"/#">>). start(Host, Opts) -> - case gen_mod:db_type(Host, Opts) of - mnesia -> - mnesia:create_table(motd, - [{disc_copies, [node()]}, - {attributes, - record_info(fields, motd)}]), - mnesia:create_table(motd_users, - [{disc_copies, [node()]}, - {attributes, - record_info(fields, motd_users)}]), - update_tables(); - _ -> - ok - end, + Mod = gen_mod:db_mod(Host, Opts, ?MODULE), + Mod:init(Host, Opts), ejabberd_hooks:add(local_send_to_resource_hook, Host, ?MODULE, announce, 50), ejabberd_hooks:add(disco_local_identity, Host, ?MODULE, disco_identity, 50), @@ -211,15 +204,15 @@ disco_identity(Acc, _From, _To, Node, Lang) -> %%------------------------------------------------------------------------- --define(INFO_RESULT(Allow, Feats), +-define(INFO_RESULT(Allow, Feats, Lang), case Allow of deny -> - {error, ?ERR_FORBIDDEN}; + {error, ?ERRT_FORBIDDEN(Lang, <<"Denied by ACL">>)}; allow -> {result, Feats} end). -disco_features(Acc, From, #jid{lserver = LServer} = _To, <<"announce">>, _Lang) -> +disco_features(Acc, From, #jid{lserver = LServer} = _To, <<"announce">>, Lang) -> case gen_mod:is_loaded(LServer, mod_adhoc) of false -> Acc; @@ -229,13 +222,14 @@ disco_features(Acc, From, #jid{lserver = LServer} = _To, <<"announce">>, _Lang) case {acl:match_rule(LServer, Access1, From), acl:match_rule(global, Access2, From)} of {deny, deny} -> - {error, ?ERR_FORBIDDEN}; + Txt = <<"Denied by ACL">>, + {error, ?ERRT_FORBIDDEN(Lang, Txt)}; _ -> {result, []} end end; -disco_features(Acc, From, #jid{lserver = LServer} = _To, Node, _Lang) -> +disco_features(Acc, From, #jid{lserver = LServer} = _To, Node, Lang) -> case gen_mod:is_loaded(LServer, mod_adhoc) of false -> Acc; @@ -246,25 +240,25 @@ disco_features(Acc, From, #jid{lserver = LServer} = _To, Node, _Lang) -> AllowGlobal = acl:match_rule(global, AccessGlobal, From), case Node of ?NS_ADMIN_ANNOUNCE -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMIN_ANNOUNCE_ALL -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMIN_SET_MOTD -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMIN_EDIT_MOTD -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMIN_DELETE_MOTD -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMIN_ANNOUNCE_ALLHOSTS -> - ?INFO_RESULT(AllowGlobal, [?NS_COMMANDS]); + ?INFO_RESULT(AllowGlobal, [?NS_COMMANDS], Lang); ?NS_ADMIN_ANNOUNCE_ALL_ALLHOSTS -> - ?INFO_RESULT(AllowGlobal, [?NS_COMMANDS]); + ?INFO_RESULT(AllowGlobal, [?NS_COMMANDS], Lang); ?NS_ADMIN_SET_MOTD_ALLHOSTS -> - ?INFO_RESULT(AllowGlobal, [?NS_COMMANDS]); + ?INFO_RESULT(AllowGlobal, [?NS_COMMANDS], Lang); ?NS_ADMIN_EDIT_MOTD_ALLHOSTS -> - ?INFO_RESULT(AllowGlobal, [?NS_COMMANDS]); + ?INFO_RESULT(AllowGlobal, [?NS_COMMANDS], Lang); ?NS_ADMIN_DELETE_MOTD_ALLHOSTS -> - ?INFO_RESULT(AllowGlobal, [?NS_COMMANDS]); + ?INFO_RESULT(AllowGlobal, [?NS_COMMANDS], Lang); _ -> Acc end @@ -283,10 +277,10 @@ disco_features(Acc, From, #jid{lserver = LServer} = _To, Node, _Lang) -> } )). --define(ITEMS_RESULT(Allow, Items), +-define(ITEMS_RESULT(Allow, Items, Lang), case Allow of deny -> - {error, ?ERR_FORBIDDEN}; + {error, ?ERRT_FORBIDDEN(Lang, <<"Denied by ACL">>)}; allow -> {result, Items} end). @@ -320,7 +314,7 @@ disco_items(Acc, From, #jid{lserver = LServer} = To, <<"announce">>, Lang) -> announce_items(Acc, From, To, Lang) end; -disco_items(Acc, From, #jid{lserver = LServer} = _To, Node, _Lang) -> +disco_items(Acc, From, #jid{lserver = LServer} = _To, Node, Lang) -> case gen_mod:is_loaded(LServer, mod_adhoc) of false -> Acc; @@ -331,25 +325,25 @@ disco_items(Acc, From, #jid{lserver = LServer} = _To, Node, _Lang) -> AllowGlobal = acl:match_rule(global, AccessGlobal, From), case Node of ?NS_ADMIN_ANNOUNCE -> - ?ITEMS_RESULT(Allow, []); + ?ITEMS_RESULT(Allow, [], Lang); ?NS_ADMIN_ANNOUNCE_ALL -> - ?ITEMS_RESULT(Allow, []); + ?ITEMS_RESULT(Allow, [], Lang); ?NS_ADMIN_SET_MOTD -> - ?ITEMS_RESULT(Allow, []); + ?ITEMS_RESULT(Allow, [], Lang); ?NS_ADMIN_EDIT_MOTD -> - ?ITEMS_RESULT(Allow, []); + ?ITEMS_RESULT(Allow, [], Lang); ?NS_ADMIN_DELETE_MOTD -> - ?ITEMS_RESULT(Allow, []); + ?ITEMS_RESULT(Allow, [], Lang); ?NS_ADMIN_ANNOUNCE_ALLHOSTS -> - ?ITEMS_RESULT(AllowGlobal, []); + ?ITEMS_RESULT(AllowGlobal, [], Lang); ?NS_ADMIN_ANNOUNCE_ALL_ALLHOSTS -> - ?ITEMS_RESULT(AllowGlobal, []); + ?ITEMS_RESULT(AllowGlobal, [], Lang); ?NS_ADMIN_SET_MOTD_ALLHOSTS -> - ?ITEMS_RESULT(AllowGlobal, []); + ?ITEMS_RESULT(AllowGlobal, [], Lang); ?NS_ADMIN_EDIT_MOTD_ALLHOSTS -> - ?ITEMS_RESULT(AllowGlobal, []); + ?ITEMS_RESULT(AllowGlobal, [], Lang); ?NS_ADMIN_DELETE_MOTD_ALLHOSTS -> - ?ITEMS_RESULT(AllowGlobal, []); + ?ITEMS_RESULT(AllowGlobal, [], Lang); _ -> Acc end @@ -396,7 +390,8 @@ announce_items(Acc, From, #jid{lserver = LServer, server = Server} = _To, Lang) commands_result(Allow, From, To, Request) -> case Allow of deny -> - {error, ?ERR_FORBIDDEN}; + Lang = Request#adhoc_request.lang, + {error, ?ERRT_FORBIDDEN(Lang, <<"Denied by ACL">>)}; allow -> announce_commands(From, To, Request) end. @@ -463,12 +458,13 @@ announce_commands(From, To, %% User returns form. case jlib:parse_xdata_submit(XData) of invalid -> - {error, ?ERR_BAD_REQUEST}; + {error, ?ERRT_BAD_REQUEST(Lang, <<"Incorrect data form">>)}; Fields -> handle_adhoc_form(From, To, Request, Fields) end; true -> - {error, ?ERR_BAD_REQUEST} + Txt = <<"Incorrect action or data form">>, + {error, ?ERRT_BAD_REQUEST(Lang, Txt)} end. -define(VVALUE(Val), @@ -688,7 +684,9 @@ announce_all(From, To, Packet) -> Access = get_access(Host), case acl:match_rule(Host, Access, From) of deny -> - Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), + Lang = fxml:get_tag_attr_s(<<"xml:lang">>, Packet), + Txt = <<"Denied by ACL">>, + Err = jlib:make_error_reply(Packet, ?ERRT_FORBIDDEN(Lang, Txt)), ejabberd_router:route(To, From, Err); allow -> Local = jid:make(<<>>, To#jid.server, <<>>), @@ -703,7 +701,9 @@ announce_all_hosts_all(From, To, Packet) -> Access = get_access(global), case acl:match_rule(global, Access, From) of deny -> - Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), + Lang = fxml:get_tag_attr_s(<<"xml:lang">>, Packet), + Txt = <<"Denied by ACL">>, + Err = jlib:make_error_reply(Packet, ?ERRT_FORBIDDEN(Lang, Txt)), ejabberd_router:route(To, From, Err); allow -> Local = jid:make(<<>>, To#jid.server, <<>>), @@ -719,7 +719,9 @@ announce_online(From, To, Packet) -> Access = get_access(Host), case acl:match_rule(Host, Access, From) of deny -> - Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), + Lang = fxml:get_tag_attr_s(<<"xml:lang">>, Packet), + Txt = <<"Denied by ACL">>, + Err = jlib:make_error_reply(Packet, ?ERRT_FORBIDDEN(Lang, Txt)), ejabberd_router:route(To, From, Err); allow -> announce_online1(ejabberd_sm:get_vh_session_list(Host), @@ -731,7 +733,9 @@ announce_all_hosts_online(From, To, Packet) -> Access = get_access(global), case acl:match_rule(global, Access, From) of deny -> - Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), + Lang = fxml:get_tag_attr_s(<<"xml:lang">>, Packet), + Txt = <<"Denied by ACL">>, + Err = jlib:make_error_reply(Packet, ?ERRT_FORBIDDEN(Lang, Txt)), ejabberd_router:route(To, From, Err); allow -> announce_online1(ejabberd_sm:dirty_get_sessions_list(), @@ -752,7 +756,9 @@ announce_motd(From, To, Packet) -> Access = get_access(Host), case acl:match_rule(Host, Access, From) of deny -> - Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), + Lang = fxml:get_tag_attr_s(<<"xml:lang">>, Packet), + Txt = <<"Denied by ACL">>, + Err = jlib:make_error_reply(Packet, ?ERRT_FORBIDDEN(Lang, Txt)), ejabberd_router:route(To, From, Err); allow -> announce_motd(Host, Packet) @@ -762,7 +768,9 @@ announce_all_hosts_motd(From, To, Packet) -> Access = get_access(global), case acl:match_rule(global, Access, From) of deny -> - Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), + Lang = fxml:get_tag_attr_s(<<"xml:lang">>, Packet), + Txt = <<"Denied by ACL">>, + Err = jlib:make_error_reply(Packet, ?ERRT_FORBIDDEN(Lang, Txt)), ejabberd_router:route(To, From, Err); allow -> Hosts = ?MYHOSTS, @@ -774,48 +782,17 @@ announce_motd(Host, Packet) -> announce_motd_update(LServer, Packet), Sessions = ejabberd_sm:get_vh_session_list(LServer), announce_online1(Sessions, LServer, Packet), - case gen_mod:db_type(LServer, ?MODULE) of - mnesia -> - F = fun() -> - lists:foreach( - fun({U, S, _R}) -> - mnesia:write(#motd_users{us = {U, S}}) - end, Sessions) - end, - mnesia:transaction(F); - riak -> - try - lists:foreach( - fun({U, S, _R}) -> - ok = ejabberd_riak:put(#motd_users{us = {U, S}}, - motd_users_schema(), - [{'2i', [{<<"server">>, S}]}]) - end, Sessions), - {atomic, ok} - catch _:{badmatch, Err} -> - {atomic, Err} - end; - odbc -> - F = fun() -> - lists:foreach( - fun({U, _S, _R}) -> - Username = ejabberd_odbc:escape(U), - odbc_queries:update_t( - <<"motd">>, - [<<"username">>, <<"xml">>], - [Username, <<"">>], - [<<"username='">>, Username, <<"'">>]) - end, Sessions) - end, - ejabberd_odbc:sql_transaction(LServer, F) - end. + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:set_motd_users(LServer, Sessions). announce_motd_update(From, To, Packet) -> Host = To#jid.lserver, Access = get_access(Host), case acl:match_rule(Host, Access, From) of deny -> - Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), + Lang = fxml:get_tag_attr_s(<<"xml:lang">>, Packet), + Txt = <<"Denied by ACL">>, + Err = jlib:make_error_reply(Packet, ?ERRT_FORBIDDEN(Lang, Txt)), ejabberd_router:route(To, From, Err); allow -> announce_motd_update(Host, Packet) @@ -825,7 +802,9 @@ announce_all_hosts_motd_update(From, To, Packet) -> Access = get_access(global), case acl:match_rule(global, Access, From) of deny -> - Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), + Lang = fxml:get_tag_attr_s(<<"xml:lang">>, Packet), + Txt = <<"Denied by ACL">>, + Err = jlib:make_error_reply(Packet, ?ERRT_FORBIDDEN(Lang, Txt)), ejabberd_router:route(To, From, Err); allow -> Hosts = ?MYHOSTS, @@ -834,34 +813,17 @@ announce_all_hosts_motd_update(From, To, Packet) -> announce_motd_update(LServer, Packet) -> announce_motd_delete(LServer), - case gen_mod:db_type(LServer, ?MODULE) of - mnesia -> - F = fun() -> - mnesia:write(#motd{server = LServer, packet = Packet}) - end, - mnesia:transaction(F); - riak -> - {atomic, ejabberd_riak:put(#motd{server = LServer, - packet = Packet}, - motd_schema())}; - odbc -> - XML = ejabberd_odbc:escape(xml:element_to_binary(Packet)), - F = fun() -> - odbc_queries:update_t( - <<"motd">>, - [<<"username">>, <<"xml">>], - [<<"">>, XML], - [<<"username=''">>]) - end, - ejabberd_odbc:sql_transaction(LServer, F) - end. + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:set_motd(LServer, Packet). announce_motd_delete(From, To, Packet) -> Host = To#jid.lserver, Access = get_access(Host), case acl:match_rule(Host, Access, From) of deny -> - Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), + Lang = fxml:get_tag_attr_s(<<"xml:lang">>, Packet), + Txt = <<"Denied by ACL">>, + Err = jlib:make_error_reply(Packet, ?ERRT_FORBIDDEN(Lang, Txt)), ejabberd_router:route(To, From, Err); allow -> announce_motd_delete(Host) @@ -871,7 +833,9 @@ announce_all_hosts_motd_delete(From, To, Packet) -> Access = get_access(global), case acl:match_rule(global, Access, From) of deny -> - Err = jlib:make_error_reply(Packet, ?ERR_FORBIDDEN), + Lang = fxml:get_tag_attr_s(<<"xml:lang">>, Packet), + Txt = <<"Denied by ACL">>, + Err = jlib:make_error_reply(Packet, ?ERRT_FORBIDDEN(Lang, Txt)), ejabberd_router:route(To, From, Err); allow -> Hosts = ?MYHOSTS, @@ -879,147 +843,37 @@ announce_all_hosts_motd_delete(From, To, Packet) -> end. announce_motd_delete(LServer) -> - case gen_mod:db_type(LServer, ?MODULE) of - mnesia -> - F = fun() -> - mnesia:delete({motd, LServer}), - mnesia:write_lock_table(motd_users), - Users = mnesia:select( - motd_users, - [{#motd_users{us = '$1', _ = '_'}, - [{'==', {element, 2, '$1'}, LServer}], - ['$1']}]), - lists:foreach(fun(US) -> - mnesia:delete({motd_users, US}) - end, Users) - end, - mnesia:transaction(F); - riak -> - try - ok = ejabberd_riak:delete(motd, LServer), - ok = ejabberd_riak:delete_by_index(motd_users, - <<"server">>, - LServer), - {atomic, ok} - catch _:{badmatch, Err} -> - {atomic, Err} - end; - odbc -> - F = fun() -> - ejabberd_odbc:sql_query_t([<<"delete from motd;">>]) - end, - ejabberd_odbc:sql_transaction(LServer, F) - end. - -send_motd(JID) -> - send_motd(JID, gen_mod:db_type(JID#jid.lserver, ?MODULE)). - -send_motd(#jid{luser = LUser, lserver = LServer} = JID, mnesia) -> - case catch mnesia:dirty_read({motd, LServer}) of - [#motd{packet = Packet}] -> - US = {LUser, LServer}, - case catch mnesia:dirty_read({motd_users, US}) of - [#motd_users{}] -> - ok; - _ -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:delete_motd(LServer). + +send_motd(#jid{luser = LUser, lserver = LServer} = JID) when LUser /= <<>> -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + case Mod:get_motd(LServer) of + {ok, Packet} -> + case Mod:is_motd_user(LUser, LServer) of + false -> Local = jid:make(<<>>, LServer, <<>>), ejabberd_router:route(Local, JID, Packet), - F = fun() -> - mnesia:write(#motd_users{us = US}) - end, - mnesia:transaction(F) + Mod:set_motd_user(LUser, LServer); + true -> + ok end; - _ -> + error -> ok end; -send_motd(#jid{luser = LUser, lserver = LServer} = JID, riak) -> - case catch ejabberd_riak:get(motd, motd_schema(), LServer) of - {ok, #motd{packet = Packet}} -> - US = {LUser, LServer}, - case ejabberd_riak:get(motd_users, motd_users_schema(), US) of - {ok, #motd_users{}} -> - ok; - _ -> - Local = jid:make(<<>>, LServer, <<>>), - ejabberd_router:route(Local, JID, Packet), - {atomic, ejabberd_riak:put( - #motd_users{us = US}, motd_users_schema(), - [{'2i', [{<<"server">>, LServer}]}])} - end; - _ -> - ok - end; -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 - {error, _} -> - ok; - Packet -> - Username = ejabberd_odbc:escape(LUser), - case catch ejabberd_odbc:sql_query( - LServer, - [<<"select username from motd " - "where username='">>, Username, <<"';">>]) of - {selected, [<<"username">>], []} -> - Local = jid:make(<<"">>, LServer, <<"">>), - ejabberd_router:route(Local, JID, Packet), - F = fun() -> - odbc_queries:update_t( - <<"motd">>, - [<<"username">>, <<"xml">>], - [Username, <<"">>], - [<<"username='">>, Username, <<"'">>]) - end, - ejabberd_odbc:sql_transaction(LServer, F); - _ -> - ok - end - end; - _ -> - ok - end; -send_motd(_, odbc) -> +send_motd(_) -> ok. get_stored_motd(LServer) -> - case get_stored_motd_packet(LServer, gen_mod:db_type(LServer, ?MODULE)) of + Mod = gen_mod:db_mod(LServer, ?MODULE), + case Mod:get_motd(LServer) 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. -get_stored_motd_packet(LServer, mnesia) -> - case catch mnesia:dirty_read({motd, LServer}) of - [#motd{packet = Packet}] -> - {ok, Packet}; - _ -> - error - end; -get_stored_motd_packet(LServer, riak) -> - case ejabberd_riak:get(motd, motd_schema(), LServer) of - {ok, #motd{packet = Packet}} -> - {ok, Packet}; - _ -> - error - end; -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 - {error, _} -> - error; - Packet -> - {ok, Packet} - end; - _ -> - error - end. - %% This function is similar to others, but doesn't perform any ACL verification send_announcement_to_all(Host, SubjectS, BodyS) -> SubjectEls = if SubjectS /= <<>> -> @@ -1053,96 +907,17 @@ get_access(Host) -> none). %%------------------------------------------------------------------------- - -update_tables() -> - update_motd_table(), - update_motd_users_table(). - -update_motd_table() -> - Fields = record_info(fields, motd), - case mnesia:table_info(motd, attributes) of - Fields -> - ejabberd_config:convert_table_to_binary( - motd, Fields, set, - fun(#motd{server = S}) -> S end, - fun(#motd{server = S, packet = P} = R) -> - NewS = iolist_to_binary(S), - NewP = xml:to_xmlel(P), - R#motd{server = NewS, packet = NewP} - end); - _ -> - ?INFO_MSG("Recreating motd table", []), - mnesia:transform_table(motd, ignore, Fields) - end. - - -update_motd_users_table() -> - Fields = record_info(fields, motd_users), - case mnesia:table_info(motd_users, attributes) of - Fields -> - ejabberd_config:convert_table_to_binary( - motd_users, Fields, set, - fun(#motd_users{us = {U, _}}) -> U end, - fun(#motd_users{us = {U, S}} = R) -> - NewUS = {iolist_to_binary(U), - iolist_to_binary(S)}, - R#motd_users{us = NewUS} - end); - _ -> - ?INFO_MSG("Recreating motd_users table", []), - mnesia:transform_table(motd_users, ignore, Fields) - end. - -motd_schema() -> - {record_info(fields, motd), #motd{}}. - -motd_users_schema() -> - {record_info(fields, motd_users), #motd_users{}}. - -export(_Server) -> - [{motd, - fun(Host, #motd{server = LServer, packet = El}) - when LServer == Host -> - [[<<"delete from motd where username='';">>], - [<<"insert into motd(username, xml) values ('', '">>, - ejabberd_odbc:escape(xml:element_to_binary(El)), - <<"');">>]]; - (_Host, _R) -> - [] - end}, - {motd_users, - fun(Host, #motd_users{us = {LUser, LServer}}) - when LServer == Host, LUser /= <<"">> -> - Username = ejabberd_odbc:escape(LUser), - [[<<"delete from motd where username='">>, Username, <<"';">>], - [<<"insert into motd(username, xml) values ('">>, - Username, <<"', '');">>]]; - (_Host, _R) -> - [] - end}]. +export(LServer) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:export(LServer). import(LServer) -> - [{<<"select xml from motd where username='';">>, - fun([XML]) -> - El = xml_stream:parse_element(XML), - #motd{server = LServer, packet = El} - end}, - {<<"select username from motd where xml='';">>, - fun([LUser]) -> - #motd_users{us = {LUser, LServer}} - end}]. - -import(_LServer, mnesia, #motd{} = Motd) -> - mnesia:dirty_write(Motd); -import(_LServer, mnesia, #motd_users{} = Users) -> - mnesia:dirty_write(Users); -import(_LServer, riak, #motd{} = Motd) -> - ejabberd_riak:put(Motd, motd_schema()); -import(_LServer, riak, #motd_users{us = {_, S}} = Users) -> - ejabberd_riak:put(Users, motd_users_schema(), - [{'2i', [{<<"server">>, S}]}]); -import(_, _, _) -> - pass. + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:import(LServer). + +import(LServer, DBType, LA) -> + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:import(LServer, LA). mod_opt_type(access) -> fun (A) when is_atom(A) -> A end; diff --git a/src/mod_announce_mnesia.erl b/src/mod_announce_mnesia.erl new file mode 100644 index 000000000..c43eb853b --- /dev/null +++ b/src/mod_announce_mnesia.erl @@ -0,0 +1,129 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 13 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_announce_mnesia). +-behaviour(mod_announce). + +%% API +-export([init/2, set_motd_users/2, set_motd/2, delete_motd/1, + get_motd/1, is_motd_user/2, set_motd_user/2, import/2]). + +-include("jlib.hrl"). +-include("mod_announce.hrl"). +-include("logger.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + mnesia:create_table(motd, + [{disc_copies, [node()]}, + {attributes, + record_info(fields, motd)}]), + mnesia:create_table(motd_users, + [{disc_copies, [node()]}, + {attributes, + record_info(fields, motd_users)}]), + update_tables(). + +set_motd_users(_LServer, USRs) -> + F = fun() -> + lists:foreach( + fun({U, S, _R}) -> + mnesia:write(#motd_users{us = {U, S}}) + end, USRs) + end, + mnesia:transaction(F). + +set_motd(LServer, Packet) -> + F = fun() -> + mnesia:write(#motd{server = LServer, packet = Packet}) + end, + mnesia:transaction(F). + +delete_motd(LServer) -> + F = fun() -> + mnesia:delete({motd, LServer}), + mnesia:write_lock_table(motd_users), + Users = mnesia:select( + motd_users, + [{#motd_users{us = '$1', _ = '_'}, + [{'==', {element, 2, '$1'}, LServer}], + ['$1']}]), + lists:foreach(fun(US) -> + mnesia:delete({motd_users, US}) + end, Users) + end, + mnesia:transaction(F). + +get_motd(LServer) -> + case mnesia:dirty_read({motd, LServer}) of + [#motd{packet = Packet}] -> + {ok, Packet}; + _ -> + error + end. + +is_motd_user(LUser, LServer) -> + case mnesia:dirty_read({motd_users, {LUser, LServer}}) of + [#motd_users{}] -> true; + _ -> false + end. + +set_motd_user(LUser, LServer) -> + F = fun() -> + mnesia:write(#motd_users{us = {LUser, LServer}}) + end, + mnesia:transaction(F). + +import(_LServer, #motd{} = Motd) -> + mnesia:dirty_write(Motd); +import(_LServer, #motd_users{} = Users) -> + mnesia:dirty_write(Users). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +update_tables() -> + update_motd_table(), + update_motd_users_table(). + +update_motd_table() -> + Fields = record_info(fields, motd), + case mnesia:table_info(motd, attributes) of + Fields -> + ejabberd_config:convert_table_to_binary( + motd, Fields, set, + fun(#motd{server = S}) -> S end, + fun(#motd{server = S, packet = P} = R) -> + NewS = iolist_to_binary(S), + NewP = fxml:to_xmlel(P), + R#motd{server = NewS, packet = NewP} + end); + _ -> + ?INFO_MSG("Recreating motd table", []), + mnesia:transform_table(motd, ignore, Fields) + end. + + +update_motd_users_table() -> + Fields = record_info(fields, motd_users), + case mnesia:table_info(motd_users, attributes) of + Fields -> + ejabberd_config:convert_table_to_binary( + motd_users, Fields, set, + fun(#motd_users{us = {U, _}}) -> U end, + fun(#motd_users{us = {U, S}} = R) -> + NewUS = {iolist_to_binary(U), + iolist_to_binary(S)}, + R#motd_users{us = NewUS} + end); + _ -> + ?INFO_MSG("Recreating motd_users table", []), + mnesia:transform_table(motd_users, ignore, Fields) + end. diff --git a/src/mod_announce_riak.erl b/src/mod_announce_riak.erl new file mode 100644 index 000000000..7ced0b3ce --- /dev/null +++ b/src/mod_announce_riak.erl @@ -0,0 +1,87 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 13 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_announce_riak). +-behaviour(mod_announce). + +%% API +-export([init/2, set_motd_users/2, set_motd/2, delete_motd/1, + get_motd/1, is_motd_user/2, set_motd_user/2, import/2]). + +-include("jlib.hrl"). +-include("mod_announce.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ok. + +set_motd_users(_LServer, USRs) -> + try + lists:foreach( + fun({U, S, _R}) -> + ok = ejabberd_riak:put(#motd_users{us = {U, S}}, + motd_users_schema(), + [{'2i', [{<<"server">>, S}]}]) + end, USRs), + {atomic, ok} + catch _:{badmatch, Err} -> + {atomic, Err} + end. + +set_motd(LServer, Packet) -> + {atomic, ejabberd_riak:put(#motd{server = LServer, + packet = Packet}, + motd_schema())}. + +delete_motd(LServer) -> + try + ok = ejabberd_riak:delete(motd, LServer), + ok = ejabberd_riak:delete_by_index(motd_users, + <<"server">>, + LServer), + {atomic, ok} + catch _:{badmatch, Err} -> + {atomic, Err} + end. + +get_motd(LServer) -> + case ejabberd_riak:get(motd, motd_schema(), LServer) of + {ok, #motd{packet = Packet}} -> + {ok, Packet}; + _ -> + error + end. + +is_motd_user(LUser, LServer) -> + case ejabberd_riak:get(motd_users, motd_users_schema(), + {LUser, LServer}) of + {ok, #motd_users{}} -> true; + _ -> false + end. + +set_motd_user(LUser, LServer) -> + {atomic, ejabberd_riak:put( + #motd_users{us = {LUser, LServer}}, motd_users_schema(), + [{'2i', [{<<"server">>, LServer}]}])}. + +import(_LServer, #motd{} = Motd) -> + ejabberd_riak:put(Motd, motd_schema()); +import(_LServer, #motd_users{us = {_, S}} = Users) -> + ejabberd_riak:put(Users, motd_users_schema(), + [{'2i', [{<<"server">>, S}]}]). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +motd_schema() -> + {record_info(fields, motd), #motd{}}. + +motd_users_schema() -> + {record_info(fields, motd_users), #motd_users{}}. diff --git a/src/mod_announce_sql.erl b/src/mod_announce_sql.erl new file mode 100644 index 000000000..692c8fae4 --- /dev/null +++ b/src/mod_announce_sql.erl @@ -0,0 +1,132 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 13 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_announce_sql). +-behaviour(mod_announce). + +%% API +-export([init/2, set_motd_users/2, set_motd/2, delete_motd/1, + get_motd/1, is_motd_user/2, set_motd_user/2, import/1, + import/2, export/1]). + +-include("jlib.hrl"). +-include("mod_announce.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ok. + +set_motd_users(LServer, USRs) -> + F = fun() -> + lists:foreach( + fun({U, _S, _R}) -> + Username = ejabberd_sql:escape(U), + sql_queries:update_t( + <<"motd">>, + [<<"username">>, <<"xml">>], + [Username, <<"">>], + [<<"username='">>, Username, <<"'">>]) + end, USRs) + end, + ejabberd_sql:sql_transaction(LServer, F). + +set_motd(LServer, Packet) -> + XML = ejabberd_sql:escape(fxml:element_to_binary(Packet)), + F = fun() -> + sql_queries:update_t( + <<"motd">>, + [<<"username">>, <<"xml">>], + [<<"">>, XML], + [<<"username=''">>]) + end, + ejabberd_sql:sql_transaction(LServer, F). + +delete_motd(LServer) -> + F = fun() -> + ejabberd_sql:sql_query_t([<<"delete from motd;">>]) + end, + ejabberd_sql:sql_transaction(LServer, F). + +get_motd(LServer) -> + case catch ejabberd_sql:sql_query( + LServer, [<<"select xml from motd where username='';">>]) of + {selected, [<<"xml">>], [[XML]]} -> + case fxml_stream:parse_element(XML) of + {error, _} -> + error; + Packet -> + {ok, Packet} + end; + _ -> + error + end. + +is_motd_user(LUser, LServer) -> + Username = ejabberd_sql:escape(LUser), + case catch ejabberd_sql:sql_query( + LServer, + [<<"select username from motd " + "where username='">>, Username, <<"';">>]) of + {selected, [<<"username">>], [_|_]} -> + true; + _ -> + false + end. + +set_motd_user(LUser, LServer) -> + Username = ejabberd_sql:escape(LUser), + F = fun() -> + sql_queries:update_t( + <<"motd">>, + [<<"username">>, <<"xml">>], + [Username, <<"">>], + [<<"username='">>, Username, <<"'">>]) + end, + ejabberd_sql:sql_transaction(LServer, F). + +export(_Server) -> + [{motd, + fun(Host, #motd{server = LServer, packet = El}) + when LServer == Host -> + [[<<"delete from motd where username='';">>], + [<<"insert into motd(username, xml) values ('', '">>, + ejabberd_sql:escape(fxml:element_to_binary(El)), + <<"');">>]]; + (_Host, _R) -> + [] + end}, + {motd_users, + fun(Host, #motd_users{us = {LUser, LServer}}) + when LServer == Host, LUser /= <<"">> -> + Username = ejabberd_sql:escape(LUser), + [[<<"delete from motd where username='">>, Username, <<"';">>], + [<<"insert into motd(username, xml) values ('">>, + Username, <<"', '');">>]]; + (_Host, _R) -> + [] + end}]. + +import(LServer) -> + [{<<"select xml from motd where username='';">>, + fun([XML]) -> + El = fxml_stream:parse_element(XML), + #motd{server = LServer, packet = El} + end}, + {<<"select username from motd where xml='';">>, + fun([LUser]) -> + #motd_users{us = {LUser, LServer}} + end}]. + +import(_, _) -> + pass. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== diff --git a/src/mod_blocking.erl b/src/mod_blocking.erl index 4f9c82734..af06e650d 100644 --- a/src/mod_blocking.erl +++ b/src/mod_blocking.erl @@ -39,6 +39,10 @@ -include("mod_privacy.hrl"). +-callback process_blocklist_block(binary(), binary(), function()) -> {atomic, any()}. +-callback unblock_by_filter(binary(), binary(), function()) -> {atomic, any()}. +-callback process_blocklist_get(binary(), binary()) -> [listitem()] | error. + start(Host, Opts) -> IQDisc = gen_mod:get_opt(iqdisc, Opts, fun gen_iq_handler:check_type/1, one_queue), @@ -64,29 +68,33 @@ process_iq(_From, _To, IQ) -> IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}. process_iq_get(_, From, _To, - #iq{xmlns = ?NS_BLOCKING, + #iq{xmlns = ?NS_BLOCKING, lang = Lang, sub_el = #xmlel{name = <<"blocklist">>}}, _) -> #jid{luser = LUser, lserver = LServer} = From, - {stop, process_blocklist_get(LUser, LServer)}; + {stop, process_blocklist_get(LUser, LServer, Lang)}; process_iq_get(Acc, _, _, _, _) -> Acc. process_iq_set(_, From, _To, - #iq{xmlns = ?NS_BLOCKING, + #iq{xmlns = ?NS_BLOCKING, lang = Lang, sub_el = #xmlel{name = SubElName, children = SubEls}}) -> #jid{luser = LUser, lserver = LServer} = From, - Res = case {SubElName, xml:remove_cdata(SubEls)} of - {<<"block">>, []} -> {error, ?ERR_BAD_REQUEST}; + Res = case {SubElName, fxml:remove_cdata(SubEls)} of + {<<"block">>, []} -> + Txt = <<"No items found in this query">>, + {error, ?ERRT_BAD_REQUEST(Lang, Txt)}; {<<"block">>, Els} -> JIDs = parse_blocklist_items(Els, []), - process_blocklist_block(LUser, LServer, JIDs); + process_blocklist_block(LUser, LServer, JIDs, Lang); {<<"unblock">>, []} -> - process_blocklist_unblock_all(LUser, LServer); + process_blocklist_unblock_all(LUser, LServer, Lang); {<<"unblock">>, Els} -> JIDs = parse_blocklist_items(Els, []), - process_blocklist_unblock(LUser, LServer, JIDs); - _ -> {error, ?ERR_BAD_REQUEST} + process_blocklist_unblock(LUser, LServer, JIDs, Lang); + _ -> + Txt = <<"Unknown blocking command">>, + {error, ?ERRT_BAD_REQUEST(Lang, Txt)} end, {stop, Res}; process_iq_set(Acc, _, _, _) -> Acc. @@ -116,7 +124,7 @@ 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 = jid:tolower(jid:from_string(JID1)), parse_blocklist_items(Els, [JID | JIDs]); @@ -125,7 +133,7 @@ parse_blocklist_items([#xmlel{name = <<"item">>, parse_blocklist_items([_ | Els], JIDs) -> parse_blocklist_items(Els, JIDs). -process_blocklist_block(LUser, LServer, JIDs) -> +process_blocklist_block(LUser, LServer, JIDs, Lang) -> Filter = fun (List) -> AlreadyBlocked = list_to_blocklist_jids(List, []), lists:foldr(fun (JID, List1) -> @@ -143,9 +151,8 @@ process_blocklist_block(LUser, LServer, JIDs) -> end, List, JIDs) end, - case process_blocklist_block(LUser, LServer, Filter, - gen_mod:db_type(LServer, mod_privacy)) - of + Mod = db_mod(LServer), + case Mod:process_blocklist_block(LUser, LServer, Filter) of {atomic, {ok, Default, List}} -> UserList = make_userlist(Default, List), broadcast_list_update(LUser, LServer, Default, @@ -155,110 +162,17 @@ process_blocklist_block(LUser, LServer, JIDs) -> {result, [], UserList}; _Err -> ?ERROR_MSG("Error processing ~p: ~p", [{LUser, LServer, JIDs}, _Err]), - {error, ?ERR_INTERNAL_SERVER_ERROR} + {error, ?ERRT_INTERNAL_SERVER_ERROR(Lang, <<"Database failure">>)} end. -process_blocklist_block(LUser, LServer, Filter, - mnesia) -> - F = fun () -> - case mnesia:wread({privacy, {LUser, LServer}}) of - [] -> - P = #privacy{us = {LUser, LServer}}, - NewDefault = <<"Blocked contacts">>, - NewLists1 = [], - List = []; - [#privacy{default = Default, lists = Lists} = P] -> - case lists:keysearch(Default, 1, Lists) of - {value, {_, List}} -> - NewDefault = Default, - NewLists1 = lists:keydelete(Default, 1, Lists); - false -> - NewDefault = <<"Blocked contacts">>, - NewLists1 = Lists, - List = [] - end - end, - NewList = Filter(List), - NewLists = [{NewDefault, NewList} | NewLists1], - mnesia:write(P#privacy{default = NewDefault, - lists = NewLists}), - {ok, NewDefault, NewList} - end, - mnesia:transaction(F); -process_blocklist_block(LUser, LServer, Filter, - riak) -> - {atomic, - begin - case ejabberd_riak:get(privacy, mod_privacy:privacy_schema(), - {LUser, LServer}) of - {ok, #privacy{default = Default, lists = Lists} = P} -> - case lists:keysearch(Default, 1, Lists) of - {value, {_, List}} -> - NewDefault = Default, - NewLists1 = lists:keydelete(Default, 1, Lists); - false -> - NewDefault = <<"Blocked contacts">>, - NewLists1 = Lists, - List = [] - end; - {error, _} -> - P = #privacy{us = {LUser, LServer}}, - NewDefault = <<"Blocked contacts">>, - NewLists1 = [], - List = [] - end, - NewList = Filter(List), - NewLists = [{NewDefault, NewList} | NewLists1], - case ejabberd_riak:put(P#privacy{default = NewDefault, - lists = NewLists}, - mod_privacy:privacy_schema()) of - ok -> - {ok, NewDefault, NewList}; - Err -> - Err - end - end}; -process_blocklist_block(LUser, LServer, Filter, odbc) -> - F = fun () -> - Default = case - mod_privacy:sql_get_default_privacy_list_t(LUser) - of - {selected, [<<"name">>], []} -> - 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 - end, - {selected, [<<"id">>], [[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 = [_ | _]} -> - List = lists:flatmap(fun mod_privacy:raw_to_item/1, RItems); - _ -> List = [] - end, - NewList = Filter(List), - NewRItems = lists:map(fun mod_privacy:item_to_raw/1, - NewList), - mod_privacy:sql_set_privacy_list(ID, NewRItems), - {ok, Default, NewList} - end, - ejabberd_odbc:sql_transaction(LServer, F). - -process_blocklist_unblock_all(LUser, LServer) -> +process_blocklist_unblock_all(LUser, LServer, Lang) -> Filter = fun (List) -> lists:filter(fun (#listitem{action = A}) -> A =/= deny end, List) end, - DBType = gen_mod:db_type(LServer, mod_privacy), - case unblock_by_filter(LUser, LServer, Filter, DBType) of + Mod = db_mod(LServer), + case Mod:unblock_by_filter(LUser, LServer, Filter) of {atomic, ok} -> {result, []}; {atomic, {ok, Default, List}} -> UserList = make_userlist(Default, List), @@ -268,10 +182,10 @@ process_blocklist_unblock_all(LUser, LServer) -> {result, [], UserList}; _Err -> ?ERROR_MSG("Error processing ~p: ~p", [{LUser, LServer}, _Err]), - {error, ?ERR_INTERNAL_SERVER_ERROR} + {error, ?ERRT_INTERNAL_SERVER_ERROR(Lang, <<"Database failure">>)} end. -process_blocklist_unblock(LUser, LServer, JIDs) -> +process_blocklist_unblock(LUser, LServer, JIDs, Lang) -> Filter = fun (List) -> lists:filter(fun (#listitem{action = deny, type = jid, value = JID}) -> @@ -280,8 +194,8 @@ process_blocklist_unblock(LUser, LServer, JIDs) -> end, List) end, - DBType = gen_mod:db_type(LServer, mod_privacy), - case unblock_by_filter(LUser, LServer, Filter, DBType) of + Mod = db_mod(LServer), + case Mod:unblock_by_filter(LUser, LServer, Filter) of {atomic, ok} -> {result, []}; {atomic, {ok, Default, List}} -> UserList = make_userlist(Default, List), @@ -292,84 +206,9 @@ process_blocklist_unblock(LUser, LServer, JIDs) -> {result, [], UserList}; _Err -> ?ERROR_MSG("Error processing ~p: ~p", [{LUser, LServer, JIDs}, _Err]), - {error, ?ERR_INTERNAL_SERVER_ERROR} + {error, ?ERRT_INTERNAL_SERVER_ERROR(Lang, <<"Database failure">>)} end. -unblock_by_filter(LUser, LServer, Filter, mnesia) -> - F = fun () -> - case mnesia:read({privacy, {LUser, LServer}}) of - [] -> - % No lists, nothing to unblock - ok; - [#privacy{default = Default, lists = Lists} = P] -> - case lists:keysearch(Default, 1, Lists) of - {value, {_, List}} -> - NewList = Filter(List), - NewLists1 = lists:keydelete(Default, 1, Lists), - NewLists = [{Default, NewList} | NewLists1], - mnesia:write(P#privacy{lists = NewLists}), - {ok, Default, NewList}; - false -> - % No default list, nothing to unblock - ok - end - end - end, - mnesia:transaction(F); -unblock_by_filter(LUser, LServer, Filter, riak) -> - {atomic, - case ejabberd_riak:get(privacy, mod_privacy:privacy_schema(), - {LUser, LServer}) of - {error, _} -> - %% No lists, nothing to unblock - ok; - {ok, #privacy{default = Default, lists = Lists} = P} -> - case lists:keysearch(Default, 1, Lists) of - {value, {_, List}} -> - NewList = Filter(List), - NewLists1 = lists:keydelete(Default, 1, Lists), - NewLists = [{Default, NewList} | NewLists1], - case ejabberd_riak:put(P#privacy{lists = NewLists}, - mod_privacy:privacy_schema()) of - ok -> - {ok, Default, NewList}; - Err -> - Err - end; - false -> - %% No default list, nothing to unblock - ok - end - end}; -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]]} = - 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 = [_ | _]} -> - List = lists:flatmap(fun mod_privacy:raw_to_item/1, - RItems), - NewList = Filter(List), - NewRItems = lists:map(fun mod_privacy:item_to_raw/1, - NewList), - mod_privacy:sql_set_privacy_list(ID, NewRItems), - {ok, Default, NewList}; - _ -> ok - end; - _ -> ok - end - end, - ejabberd_odbc:sql_transaction(LServer, F). - make_userlist(Name, List) -> NeedDb = mod_privacy:is_list_needdb(List), #userlist{name = Name, list = List, needdb = NeedDb}. @@ -385,11 +224,11 @@ broadcast_blocklist_event(LUser, LServer, Event) -> ejabberd_sm:route(JID, JID, {broadcast, {blocking, Event}}). -process_blocklist_get(LUser, LServer) -> - case process_blocklist_get(LUser, LServer, - gen_mod:db_type(LServer, mod_privacy)) - of - error -> {error, ?ERR_INTERNAL_SERVER_ERROR}; +process_blocklist_get(LUser, LServer, Lang) -> + Mod = db_mod(LServer), + case Mod:process_blocklist_get(LUser, LServer) of + error -> + {error, ?ERRT_INTERNAL_SERVER_ERROR(Lang, <<"Database failure">>)}; List -> JIDs = list_to_blocklist_jids(List, []), Items = lists:map(fun (JID) -> @@ -407,49 +246,9 @@ process_blocklist_get(LUser, LServer) -> children = Items}]} end. -process_blocklist_get(LUser, LServer, mnesia) -> - case catch mnesia:dirty_read(privacy, {LUser, LServer}) - of - {'EXIT', _Reason} -> error; - [] -> []; - [#privacy{default = Default, lists = Lists}] -> - case lists:keysearch(Default, 1, Lists) of - {value, {_, List}} -> List; - _ -> [] - end - end; -process_blocklist_get(LUser, LServer, riak) -> - case ejabberd_riak:get(privacy, mod_privacy:privacy_schema(), - {LUser, LServer}) of - {ok, #privacy{default = Default, lists = Lists}} -> - case lists:keysearch(Default, 1, Lists) of - {value, {_, List}} -> List; - _ -> [] - end; - {error, notfound} -> - []; - {error, _} -> - error - end; -process_blocklist_get(LUser, LServer, odbc) -> - case catch - mod_privacy:sql_get_default_privacy_list(LUser, LServer) - of - {selected, [<<"name">>], []} -> []; - {selected, [<<"name">>], [[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} -> - lists:flatmap(fun mod_privacy:raw_to_item/1, RItems); - {'EXIT', _} -> error - end; - {'EXIT', _} -> error - end. +db_mod(LServer) -> + DBType = gen_mod:db_type(LServer, mod_privacy), + gen_mod:db_mod(DBType, ?MODULE). mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; mod_opt_type(_) -> [iqdisc]. diff --git a/src/mod_blocking_mnesia.erl b/src/mod_blocking_mnesia.erl new file mode 100644 index 000000000..5a4bde64c --- /dev/null +++ b/src/mod_blocking_mnesia.erl @@ -0,0 +1,85 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 14 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_blocking_mnesia). + +-behaviour(mod_blocking). + +%% API +-export([process_blocklist_block/3, unblock_by_filter/3, + process_blocklist_get/2]). + +-include("jlib.hrl"). +-include("mod_privacy.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +process_blocklist_block(LUser, LServer, Filter) -> + F = fun () -> + case mnesia:wread({privacy, {LUser, LServer}}) of + [] -> + P = #privacy{us = {LUser, LServer}}, + NewDefault = <<"Blocked contacts">>, + NewLists1 = [], + List = []; + [#privacy{default = Default, lists = Lists} = P] -> + case lists:keysearch(Default, 1, Lists) of + {value, {_, List}} -> + NewDefault = Default, + NewLists1 = lists:keydelete(Default, 1, Lists); + false -> + NewDefault = <<"Blocked contacts">>, + NewLists1 = Lists, + List = [] + end + end, + NewList = Filter(List), + NewLists = [{NewDefault, NewList} | NewLists1], + mnesia:write(P#privacy{default = NewDefault, + lists = NewLists}), + {ok, NewDefault, NewList} + end, + mnesia:transaction(F). + +unblock_by_filter(LUser, LServer, Filter) -> + F = fun () -> + case mnesia:read({privacy, {LUser, LServer}}) of + [] -> + %% No lists, nothing to unblock + ok; + [#privacy{default = Default, lists = Lists} = P] -> + case lists:keysearch(Default, 1, Lists) of + {value, {_, List}} -> + NewList = Filter(List), + NewLists1 = lists:keydelete(Default, 1, Lists), + NewLists = [{Default, NewList} | NewLists1], + mnesia:write(P#privacy{lists = NewLists}), + {ok, Default, NewList}; + false -> + %% No default list, nothing to unblock + ok + end + end + end, + mnesia:transaction(F). + +process_blocklist_get(LUser, LServer) -> + case catch mnesia:dirty_read(privacy, {LUser, LServer}) of + {'EXIT', _Reason} -> error; + [] -> []; + [#privacy{default = Default, lists = Lists}] -> + case lists:keysearch(Default, 1, Lists) of + {value, {_, List}} -> List; + _ -> [] + end + end. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== diff --git a/src/mod_blocking_riak.erl b/src/mod_blocking_riak.erl new file mode 100644 index 000000000..5dd5cfa92 --- /dev/null +++ b/src/mod_blocking_riak.erl @@ -0,0 +1,98 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 14 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_blocking_riak). + +-behaviour(mod_blocking). + +%% API +-export([process_blocklist_block/3, unblock_by_filter/3, + process_blocklist_get/2]). + +-include("jlib.hrl"). +-include("mod_privacy.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +process_blocklist_block(LUser, LServer, Filter) -> + {atomic, + begin + case ejabberd_riak:get(privacy, mod_privacy_riak:privacy_schema(), + {LUser, LServer}) of + {ok, #privacy{default = Default, lists = Lists} = P} -> + case lists:keysearch(Default, 1, Lists) of + {value, {_, List}} -> + NewDefault = Default, + NewLists1 = lists:keydelete(Default, 1, Lists); + false -> + NewDefault = <<"Blocked contacts">>, + NewLists1 = Lists, + List = [] + end; + {error, _} -> + P = #privacy{us = {LUser, LServer}}, + NewDefault = <<"Blocked contacts">>, + NewLists1 = [], + List = [] + end, + NewList = Filter(List), + NewLists = [{NewDefault, NewList} | NewLists1], + case ejabberd_riak:put(P#privacy{default = NewDefault, + lists = NewLists}, + mod_privacy_riak:privacy_schema()) of + ok -> + {ok, NewDefault, NewList}; + Err -> + Err + end + end}. + +unblock_by_filter(LUser, LServer, Filter) -> + {atomic, + case ejabberd_riak:get(privacy, mod_privacy_riak:privacy_schema(), + {LUser, LServer}) of + {error, _} -> + %% No lists, nothing to unblock + ok; + {ok, #privacy{default = Default, lists = Lists} = P} -> + case lists:keysearch(Default, 1, Lists) of + {value, {_, List}} -> + NewList = Filter(List), + NewLists1 = lists:keydelete(Default, 1, Lists), + NewLists = [{Default, NewList} | NewLists1], + case ejabberd_riak:put(P#privacy{lists = NewLists}, + mod_privacy_riak:privacy_schema()) of + ok -> + {ok, Default, NewList}; + Err -> + Err + end; + false -> + %% No default list, nothing to unblock + ok + end + end}. + +process_blocklist_get(LUser, LServer) -> + case ejabberd_riak:get(privacy, mod_privacy_riak:privacy_schema(), + {LUser, LServer}) of + {ok, #privacy{default = Default, lists = Lists}} -> + case lists:keysearch(Default, 1, Lists) of + {value, {_, List}} -> List; + _ -> [] + end; + {error, notfound} -> + []; + {error, _} -> + error + end. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== diff --git a/src/mod_blocking_sql.erl b/src/mod_blocking_sql.erl new file mode 100644 index 000000000..fb8380e2c --- /dev/null +++ b/src/mod_blocking_sql.erl @@ -0,0 +1,87 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 14 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_blocking_sql). + +-behaviour(mod_blocking). + +%% API +-export([process_blocklist_block/3, unblock_by_filter/3, + process_blocklist_get/2]). + +-include("jlib.hrl"). +-include("mod_privacy.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +process_blocklist_block(LUser, LServer, Filter) -> + F = fun () -> + Default = case mod_privacy_sql:sql_get_default_privacy_list_t(LUser) of + {selected, []} -> + Name = <<"Blocked contacts">>, + mod_privacy_sql:sql_add_privacy_list(LUser, Name), + mod_privacy_sql:sql_set_default_privacy_list(LUser, Name), + Name; + {selected, [{Name}]} -> Name + end, + {selected, [{ID}]} = + mod_privacy_sql:sql_get_privacy_list_id_t(LUser, Default), + case mod_privacy_sql:sql_get_privacy_list_data_by_id_t(ID) of + {selected, RItems = [_ | _]} -> + List = lists:flatmap(fun mod_privacy_sql:raw_to_item/1, RItems); + _ -> + List = [] + end, + NewList = Filter(List), + NewRItems = lists:map(fun mod_privacy_sql:item_to_raw/1, + NewList), + mod_privacy_sql:sql_set_privacy_list(ID, NewRItems), + {ok, Default, NewList} + end, + ejabberd_sql:sql_transaction(LServer, F). + +unblock_by_filter(LUser, LServer, Filter) -> + F = fun () -> + case mod_privacy_sql:sql_get_default_privacy_list_t(LUser) of + {selected, []} -> ok; + {selected, [{Default}]} -> + {selected, [{ID}]} = + mod_privacy_sql:sql_get_privacy_list_id_t(LUser, Default), + case mod_privacy_sql:sql_get_privacy_list_data_by_id_t(ID) of + {selected, RItems = [_ | _]} -> + List = lists:flatmap(fun mod_privacy_sql:raw_to_item/1, + RItems), + NewList = Filter(List), + NewRItems = lists:map(fun mod_privacy_sql:item_to_raw/1, + NewList), + mod_privacy_sql:sql_set_privacy_list(ID, NewRItems), + {ok, Default, NewList}; + _ -> ok + end; + _ -> ok + end + end, + ejabberd_sql:sql_transaction(LServer, F). + +process_blocklist_get(LUser, LServer) -> + case catch mod_privacy_sql:sql_get_default_privacy_list(LUser, LServer) of + {selected, []} -> []; + {selected, [{Default}]} -> + case catch mod_privacy_sql:sql_get_privacy_list_data( + LUser, LServer, Default) of + {selected, RItems} -> + lists:flatmap(fun mod_privacy_sql:raw_to_item/1, RItems); + {'EXIT', _} -> error + end; + {'EXIT', _} -> error + end. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== diff --git a/src/mod_caps.erl b/src/mod_caps.erl index 7d764c4bf..3d5c360a8 100644 --- a/src/mod_caps.erl +++ b/src/mod_caps.erl @@ -80,6 +80,12 @@ -record(state, {host = <<"">> :: binary()}). +-callback init(binary(), gen_mod:opts()) -> any(). +-callback caps_read(binary(), {binary(), binary()}) -> + {ok, non_neg_integer() | [binary()]} | error. +-callback caps_write(binary(), {binary(), binary()}, + non_neg_integer() | [binary()]) -> any(). + start_link(Host, Opts) -> Proc = gen_mod:get_module_proc(Host, ?PROCNAME), gen_server:start_link({local, Proc}, ?MODULE, @@ -120,12 +126,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, @@ -135,7 +141,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; @@ -149,7 +155,7 @@ user_send_packet(#xmlel{name = <<"presence">>, attrs = Attrs, #jid{luser = User, lserver = Server} = From, #jid{luser = User, lserver = Server, lresource = <<"">>}) -> - Type = xml:get_attr_s(<<"type">>, Attrs), + Type = fxml:get_attr_s(<<"type">>, Attrs), if Type == <<"">>; Type == <<"available">> -> case read_caps(Els) of nothing -> ok; @@ -167,7 +173,7 @@ user_receive_packet(#xmlel{name = <<"presence">>, attrs = Attrs, _C2SState, #jid{lserver = Server}, From, _To) -> - Type = xml:get_attr_s(<<"type">>, Attrs), + Type = fxml:get_attr_s(<<"type">>, Attrs), IsRemote = not lists:member(From#jid.lserver, ?MYHOSTS), if IsRemote and ((Type == <<"">>) or (Type == <<"available">>)) -> @@ -227,7 +233,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">>)) @@ -300,28 +306,9 @@ c2s_broadcast_recipients(InAcc, Host, C2SState, end; c2s_broadcast_recipients(Acc, _, _, _, _, _) -> Acc. -init_db(mnesia, _Host) -> - case catch mnesia:table_info(caps_features, storage_type) of - {'EXIT', _} -> - ok; - disc_only_copies -> - ok; - _ -> - mnesia:delete_table(caps_features) - end, - mnesia:create_table(caps_features, - [{disc_only_copies, [node()]}, - {local_content, true}, - {attributes, - record_info(fields, caps_features)}]), - update_table(), - mnesia:add_table_copy(caps_features, node(), - disc_only_copies); -init_db(_, _) -> - ok. - init([Host, Opts]) -> - init_db(gen_mod:db_type(Host, Opts), Host), + Mod = gen_mod:db_mod(Host, Opts, ?MODULE), + Mod:init(Host, Opts), MaxSize = gen_mod:get_opt(cache_size, Opts, fun(I) when is_integer(I), I>0 -> I end, 1000), @@ -434,7 +421,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), @@ -450,65 +437,13 @@ feature_response(_IQResult, Host, From, Caps, caps_read_fun(Host, Node) -> LServer = jid:nameprep(Host), - DBType = gen_mod:db_type(LServer, ?MODULE), - caps_read_fun(LServer, Node, DBType). - -caps_read_fun(_LServer, Node, mnesia) -> - fun () -> - case mnesia:dirty_read({caps_features, Node}) of - [#caps_features{features = Features}] -> {ok, Features}; - _ -> error - end - end; -caps_read_fun(_LServer, Node, riak) -> - fun() -> - case ejabberd_riak:get(caps_features, caps_features_schema(), Node) of - {ok, #caps_features{features = Features}} -> {ok, Features}; - _ -> error - end - end; -caps_read_fun(LServer, {Node, SubNode}, odbc) -> - fun() -> - SNode = ejabberd_odbc:escape(Node), - SSubNode = ejabberd_odbc:escape(SubNode), - case ejabberd_odbc:sql_query( - LServer, [<<"select feature from caps_features where ">>, - <<"node='">>, SNode, <<"' and subnode='">>, - SSubNode, <<"';">>]) of - {selected, [<<"feature">>], [[H]|_] = Fs} -> - case catch jlib:binary_to_integer(H) of - Int when is_integer(Int), Int>=0 -> - {ok, Int}; - _ -> - {ok, lists:flatten(Fs)} - end; - _ -> - error - end - end. + Mod = gen_mod:db_mod(LServer, ?MODULE), + fun() -> Mod:caps_read(LServer, Node) end. caps_write_fun(Host, Node, Features) -> LServer = jid:nameprep(Host), - DBType = gen_mod:db_type(LServer, ?MODULE), - caps_write_fun(LServer, Node, Features, DBType). - -caps_write_fun(_LServer, Node, Features, mnesia) -> - fun () -> - mnesia:dirty_write(#caps_features{node_pair = Node, - features = Features}) - end; -caps_write_fun(_LServer, Node, Features, riak) -> - fun () -> - ejabberd_riak:put(#caps_features{node_pair = Node, - features = Features}, - caps_features_schema()) - end; -caps_write_fun(LServer, NodePair, Features, odbc) -> - fun () -> - ejabberd_odbc:sql_transaction( - LServer, - sql_write_features_t(NodePair, Features)) - end. + Mod = gen_mod:db_mod(LServer, ?MODULE), + fun() -> Mod:caps_write(LServer, Node, Features) end. make_my_disco_hash(Host) -> JID = jid:make(<<"">>, Host, <<"">>), @@ -567,7 +502,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 +511,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 +524,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 +541,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 +557,7 @@ concat_xdata_fields(Fields) -> children = VEls}) -> - [[xml:get_cdata(VEls), + [[fxml:get_cdata(VEls), $<]]; (_) -> [] @@ -658,64 +593,23 @@ is_valid_node(Node) -> false end. -update_table() -> - Fields = record_info(fields, caps_features), - case mnesia:table_info(caps_features, attributes) of - Fields -> - ejabberd_config:convert_table_to_binary( - caps_features, Fields, set, - fun(#caps_features{node_pair = {N, _}}) -> N end, - fun(#caps_features{node_pair = {N, P}, - features = Fs} = R) -> - NewFs = if is_integer(Fs) -> - Fs; - true -> - [iolist_to_binary(F) || F <- Fs] - end, - R#caps_features{node_pair = {iolist_to_binary(N), - iolist_to_binary(P)}, - features = NewFs} - end); - _ -> - ?INFO_MSG("Recreating caps_features table", []), - mnesia:transform_table(caps_features, ignore, Fields) - end. - -sql_write_features_t({Node, SubNode}, Features) -> - SNode = ejabberd_odbc:escape(Node), - SSubNode = ejabberd_odbc:escape(SubNode), - NewFeatures = if is_integer(Features) -> - [jlib:integer_to_binary(Features)]; - true -> - Features - end, - [[<<"delete from caps_features where node='">>, - SNode, <<"' and subnode='">>, SSubNode, <<"';">>]| - [[<<"insert into caps_features(node, subnode, feature) ">>, - <<"values ('">>, SNode, <<"', '">>, SSubNode, <<"', '">>, - ejabberd_odbc:escape(F), <<"');">>] || F <- NewFeatures]]. - caps_features_schema() -> {record_info(fields, caps_features), #caps_features{}}. -export(_Server) -> - [{caps_features, - fun(_Host, #caps_features{node_pair = NodePair, - features = Features}) -> - sql_write_features_t(NodePair, Features); - (_Host, _R) -> - [] - end}]. +export(LServer) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:export(LServer). import_info() -> [{<<"caps_features">>, 4}]. import_start(LServer, DBType) -> ets:new(caps_features_tmp, [private, named_table, bag]), - init_db(DBType, LServer), + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:init(LServer, []), ok. -import(_LServer, {odbc, _}, _DBType, <<"caps_features">>, +import(_LServer, {sql, _}, _DBType, <<"caps_features">>, [Node, SubNode, Feature, _TimeStamp]) -> Feature1 = case catch jlib:binary_to_integer(Feature) of I when is_integer(I), I>0 -> I; @@ -748,7 +642,7 @@ import_next(LServer, DBType, NodePair) -> ejabberd_riak:put( #caps_features{node_pair = NodePair, features = Features}, caps_features_schema()); - _ when DBType == odbc -> + _ when DBType == sql -> ok end, import_next(LServer, DBType, ets:next(caps_features_tmp, NodePair)). diff --git a/src/mod_caps_mnesia.erl b/src/mod_caps_mnesia.erl new file mode 100644 index 000000000..0bf04b2c3 --- /dev/null +++ b/src/mod_caps_mnesia.erl @@ -0,0 +1,73 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 13 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_caps_mnesia). +-behaviour(mod_caps). + +%% API +-export([init/2, caps_read/2, caps_write/3]). + +-include("mod_caps.hrl"). +-include("logger.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + case catch mnesia:table_info(caps_features, storage_type) of + {'EXIT', _} -> + ok; + disc_only_copies -> + ok; + _ -> + mnesia:delete_table(caps_features) + end, + mnesia:create_table(caps_features, + [{disc_only_copies, [node()]}, + {local_content, true}, + {attributes, + record_info(fields, caps_features)}]), + update_table(), + mnesia:add_table_copy(caps_features, node(), + disc_only_copies). + +caps_read(_LServer, Node) -> + case mnesia:dirty_read({caps_features, Node}) of + [#caps_features{features = Features}] -> {ok, Features}; + _ -> error + end. + +caps_write(_LServer, Node, Features) -> + mnesia:dirty_write(#caps_features{node_pair = Node, + features = Features}). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +update_table() -> + Fields = record_info(fields, caps_features), + case mnesia:table_info(caps_features, attributes) of + Fields -> + ejabberd_config:convert_table_to_binary( + caps_features, Fields, set, + fun(#caps_features{node_pair = {N, _}}) -> N end, + fun(#caps_features{node_pair = {N, P}, + features = Fs} = R) -> + NewFs = if is_integer(Fs) -> + Fs; + true -> + [iolist_to_binary(F) || F <- Fs] + end, + R#caps_features{node_pair = {iolist_to_binary(N), + iolist_to_binary(P)}, + features = NewFs} + end); + _ -> + ?INFO_MSG("Recreating caps_features table", []), + mnesia:transform_table(caps_features, ignore, Fields) + end. diff --git a/src/mod_caps_riak.erl b/src/mod_caps_riak.erl new file mode 100644 index 000000000..6e59ba867 --- /dev/null +++ b/src/mod_caps_riak.erl @@ -0,0 +1,38 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 13 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_caps_riak). +-behaviour(mod_caps). + +%% API +-export([init/2, caps_read/2, caps_write/3]). + +-include("mod_caps.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ok. + +caps_read(_LServer, Node) -> + case ejabberd_riak:get(caps_features, caps_features_schema(), Node) of + {ok, #caps_features{features = Features}} -> {ok, Features}; + _ -> error + end. + +caps_write(_LServer, Node, Features) -> + ejabberd_riak:put(#caps_features{node_pair = Node, + features = Features}, + caps_features_schema()). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +caps_features_schema() -> + {record_info(fields, caps_features), #caps_features{}}. diff --git a/src/mod_caps_sql.erl b/src/mod_caps_sql.erl new file mode 100644 index 000000000..9f587db35 --- /dev/null +++ b/src/mod_caps_sql.erl @@ -0,0 +1,71 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 13 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_caps_sql). +-behaviour(mod_caps). + +%% API +-export([init/2, caps_read/2, caps_write/3, export/1]). + +-include("mod_caps.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ok. + +caps_read(LServer, {Node, SubNode}) -> + SNode = ejabberd_sql:escape(Node), + SSubNode = ejabberd_sql:escape(SubNode), + case ejabberd_sql:sql_query( + LServer, [<<"select feature from caps_features where ">>, + <<"node='">>, SNode, <<"' and subnode='">>, + SSubNode, <<"';">>]) of + {selected, [<<"feature">>], [[H]|_] = Fs} -> + case catch jlib:binary_to_integer(H) of + Int when is_integer(Int), Int>=0 -> + {ok, Int}; + _ -> + {ok, lists:flatten(Fs)} + end; + _ -> + error + end. + +caps_write(LServer, NodePair, Features) -> + ejabberd_sql:sql_transaction( + LServer, + sql_write_features_t(NodePair, Features)). + +export(_Server) -> + [{caps_features, + fun(_Host, #caps_features{node_pair = NodePair, + features = Features}) -> + sql_write_features_t(NodePair, Features); + (_Host, _R) -> + [] + end}]. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +sql_write_features_t({Node, SubNode}, Features) -> + SNode = ejabberd_sql:escape(Node), + SSubNode = ejabberd_sql:escape(SubNode), + NewFeatures = if is_integer(Features) -> + [jlib:integer_to_binary(Features)]; + true -> + Features + end, + [[<<"delete from caps_features where node='">>, + SNode, <<"' and subnode='">>, SSubNode, <<"';">>]| + [[<<"insert into caps_features(node, subnode, feature) ">>, + <<"values ('">>, SNode, <<"', '">>, SSubNode, <<"', '">>, + ejabberd_sql:escape(F), <<"');">>] || F <- NewFeatures]]. + diff --git a/src/mod_carboncopy.erl b/src/mod_carboncopy.erl index a4ca45ddf..ebf1d0b0f 100644 --- a/src/mod_carboncopy.erl +++ b/src/mod_carboncopy.erl @@ -43,21 +43,20 @@ -include("logger.hrl"). -include("jlib.hrl"). -define(PROCNAME, ?MODULE). --define(TABLE, carboncopy). --type matchspec_atom() :: '_' | '$1' | '$2' | '$3'. --record(carboncopy,{us :: {binary(), binary()} | matchspec_atom(), - resource :: binary() | matchspec_atom(), - version :: binary() | matchspec_atom()}). +-callback init(binary(), gen_mod:opts()) -> any(). +-callback enable(binary(), binary(), binary(), binary()) -> ok | {error, any()}. +-callback disable(binary(), binary(), binary()) -> ok | {error, any()}. +-callback list(binary(), binary()) -> [{binary(), binary()}]. is_carbon_copy(Packet) -> is_carbon_copy(Packet, <<"sent">>) orelse 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 @@ -69,17 +68,8 @@ start(Host, Opts) -> IQDisc = gen_mod:get_opt(iqdisc, Opts,fun gen_iq_handler:check_type/1, one_queue), mod_disco:register_feature(Host, ?NS_CARBONS_1), mod_disco:register_feature(Host, ?NS_CARBONS_2), - Fields = record_info(fields, ?TABLE), - try mnesia:table_info(?TABLE, attributes) of - Fields -> ok; - _ -> mnesia:delete_table(?TABLE) %% recreate.. - catch _:_Error -> ok %%probably table don't exist - end, - mnesia:create_table(?TABLE, - [{ram_copies, [node()]}, - {attributes, record_info(fields, ?TABLE)}, - {type, bag}]), - mnesia:add_table_copy(?TABLE, node(), ram_copies), + Mod = gen_mod:db_mod(Host, ?MODULE), + Mod:init(Host, Opts), ejabberd_hooks:add(unset_presence_hook,Host, ?MODULE, remove_connection, 10), %% why priority 89: to define clearly that we must run BEFORE mod_logdb hook (90) ejabberd_hooks:add(user_send_packet,Host, ?MODULE, user_send_packet, 89), @@ -102,7 +92,9 @@ iq_handler2(From, To, IQ) -> iq_handler1(From, To, IQ) -> iq_handler(From, To, IQ, ?NS_CARBONS_1). -iq_handler(From, _To, #iq{type=set, sub_el = #xmlel{name = Operation, children = []}} = IQ, CC)-> +iq_handler(From, _To, + #iq{type=set, lang = Lang, + sub_el = #xmlel{name = Operation} = SubEl} = IQ, CC)-> ?DEBUG("carbons IQ received: ~p", [IQ]), {U, S, R} = jid:tolower(From), Result = case Operation of @@ -118,12 +110,14 @@ iq_handler(From, _To, #iq{type=set, sub_el = #xmlel{name = Operation, children ?DEBUG("carbons IQ result: ok", []), IQ#iq{type=result, sub_el=[]}; {error,_Error} -> - ?WARNING_MSG("Error enabling / disabling carbons: ~p", [Result]), - IQ#iq{type=error,sub_el = [?ERR_BAD_REQUEST]} + ?ERROR_MSG("Error enabling / disabling carbons: ~p", [Result]), + Txt = <<"Database failure">>, + IQ#iq{type=error,sub_el = [SubEl, ?ERRT_INTERNAL_SERVER_ERROR(Lang, Txt)]} end; -iq_handler(_From, _To, IQ, _CC)-> - IQ#iq{type=error, sub_el = [?ERR_NOT_ALLOWED]}. +iq_handler(_From, _To, #iq{lang = Lang, sub_el = SubEl} = IQ, _CC)-> + Txt = <<"Value 'get' of 'type' attribute is not allowed">>, + IQ#iq{type=error, sub_el = [SubEl, ?ERRT_NOT_ALLOWED(Lang, Txt)]}. user_send_packet(Packet, _C2SState, From, To) -> check_and_forward(From, To, Packet, sent). @@ -137,8 +131,8 @@ user_receive_packet(Packet, _C2SState, JID, _From, To) -> % - we also replicate "read" notifications check_and_forward(JID, To, Packet, Direction)-> case is_chat_message(Packet) andalso - xml:get_subtag(Packet, <<"private">>) == false andalso - xml:get_subtag(Packet, <<"no-copy">>) == false of + fxml:get_subtag(Packet, <<"private">>) == false andalso + fxml:get_subtag(Packet, <<"no-copy">>) == false of true -> case is_carbon_copy(Packet) of false -> @@ -240,18 +234,13 @@ build_forward_packet(JID, Packet, Sender, Dest, Direction, ?NS_CARBONS_1) -> enable(Host, U, R, CC)-> ?DEBUG("enabling for ~p", [U]), - try mnesia:dirty_write(#carboncopy{us = {U, Host}, resource=R, version = CC}) of - ok -> ok - catch _:Error -> {error, Error} - end. + Mod = gen_mod:db_mod(Host, ?MODULE), + Mod:enable(U, Host, R, CC). disable(Host, U, R)-> ?DEBUG("disabling for ~p", [U]), - ToDelete = mnesia:dirty_match_object(?TABLE, #carboncopy{us = {U, Host}, resource = R, version = '_'}), - try lists:foreach(fun mnesia:dirty_delete_object/1, ToDelete) of - ok -> ok - catch _:Error -> {error, Error} - end. + Mod = gen_mod:db_mod(Host, ?MODULE), + Mod:disable(U, Host, R). complete_packet(From, #xmlel{name = <<"message">>, attrs = OrigAttrs} = Packet, sent) -> %% if this is a packet sent by user on this host, then Packet doesn't @@ -268,7 +257,7 @@ 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. @@ -282,12 +271,16 @@ is_chat_message(#xmlel{name = <<"message">>} = Packet) -> is_chat_message(_Packet) -> false. has_non_empty_body(Packet) -> - xml:get_subtag_cdata(Packet, <<"body">>) =/= <<"">>. + fxml:get_subtag_cdata(Packet, <<"body">>) =/= <<"">>. %% list {resource, cc_version} with carbons enabled for given user and host list(User, Server) -> - mnesia:dirty_select(?TABLE, [{#carboncopy{us = {User, Server}, resource = '$2', version = '$3'}, [], [{{'$2','$3'}}]}]). - + Mod = gen_mod:db_mod(Server, ?MODULE), + Mod:list(User, Server). mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; -mod_opt_type(_) -> [iqdisc]. +mod_opt_type(db_type) -> + fun(internal) -> mnesia; + (mnesia) -> mnesia + end; +mod_opt_type(_) -> [db_type, iqdisc]. diff --git a/src/mod_carboncopy_mnesia.erl b/src/mod_carboncopy_mnesia.erl new file mode 100644 index 000000000..bf69bd21c --- /dev/null +++ b/src/mod_carboncopy_mnesia.erl @@ -0,0 +1,68 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 15 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_carboncopy_mnesia). + +-behaviour(mod_carboncopy). + +%% API +-export([init/2, enable/4, disable/3, list/2]). + +-include("mod_carboncopy.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + Fields = record_info(fields, carboncopy), + try mnesia:table_info(carboncopy, attributes) of + Fields -> + ok; + _ -> + %% recreate.. + mnesia:delete_table(carboncopy) + catch _:_Error -> + %% probably table don't exist + ok + end, + mnesia:create_table(carboncopy, + [{ram_copies, [node()]}, + {attributes, record_info(fields, carboncopy)}, + {type, bag}]), + mnesia:add_table_copy(carboncopy, node(), ram_copies). + +enable(LUser, LServer, LResource, NS) -> + try mnesia:dirty_write( + #carboncopy{us = {LUser, LServer}, + resource = LResource, + version = NS}) of + ok -> ok + catch _:Error -> + {error, Error} + end. + +disable(LUser, LServer, LResource) -> + ToDelete = mnesia:dirty_match_object( + #carboncopy{us = {LUser, LServer}, + resource = LResource, + version = '_'}), + try lists:foreach(fun mnesia:dirty_delete_object/1, ToDelete) of + ok -> ok + catch _:Error -> + {error, Error} + end. + +list(LUser, LServer) -> + mnesia:dirty_select( + carboncopy, + [{#carboncopy{us = {LUser, LServer}, resource = '$2', version = '$3'}, + [], [{{'$2','$3'}}]}]). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== diff --git a/src/mod_client_state.erl b/src/mod_client_state.erl index 3c046d776..dfbfc028e 100644 --- a/src/mod_client_state.erl +++ b/src/mod_client_state.erl @@ -80,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}; diff --git a/src/mod_configure.erl b/src/mod_configure.erl index 0c7e75268..a836c33bd 100644 --- a/src/mod_configure.erl +++ b/src/mod_configure.erl @@ -198,81 +198,81 @@ get_local_identity(Acc, _From, _To, Node, Lang) -> %%%----------------------------------------------------------------------- --define(INFO_RESULT(Allow, Feats), +-define(INFO_RESULT(Allow, Feats, Lang), case Allow of - deny -> {error, ?ERR_FORBIDDEN}; + deny -> {error, ?ERRT_FORBIDDEN(Lang, <<"Denied by ACL">>)}; allow -> {result, Feats} end). get_sm_features(Acc, From, - #jid{lserver = LServer} = _To, Node, _Lang) -> + #jid{lserver = LServer} = _To, Node, Lang) -> case gen_mod:is_loaded(LServer, mod_adhoc) of false -> Acc; _ -> Allow = acl:match_rule(LServer, configure, From), case Node of - <<"config">> -> ?INFO_RESULT(Allow, [?NS_COMMANDS]); + <<"config">> -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); _ -> Acc end end. get_local_features(Acc, From, - #jid{lserver = LServer} = _To, Node, _Lang) -> + #jid{lserver = LServer} = _To, Node, Lang) -> case gen_mod:is_loaded(LServer, mod_adhoc) of false -> Acc; _ -> LNode = tokenize(Node), Allow = acl:match_rule(LServer, configure, From), case LNode of - [<<"config">>] -> ?INFO_RESULT(Allow, []); - [<<"user">>] -> ?INFO_RESULT(Allow, []); - [<<"online users">>] -> ?INFO_RESULT(Allow, []); - [<<"all users">>] -> ?INFO_RESULT(Allow, []); + [<<"config">>] -> ?INFO_RESULT(Allow, [], Lang); + [<<"user">>] -> ?INFO_RESULT(Allow, [], Lang); + [<<"online users">>] -> ?INFO_RESULT(Allow, [], Lang); + [<<"all users">>] -> ?INFO_RESULT(Allow, [], Lang); [<<"all users">>, <<$@, _/binary>>] -> - ?INFO_RESULT(Allow, []); - [<<"outgoing s2s">> | _] -> ?INFO_RESULT(Allow, []); - [<<"running nodes">>] -> ?INFO_RESULT(Allow, []); - [<<"stopped nodes">>] -> ?INFO_RESULT(Allow, []); + ?INFO_RESULT(Allow, [], Lang); + [<<"outgoing s2s">> | _] -> ?INFO_RESULT(Allow, [], Lang); + [<<"running nodes">>] -> ?INFO_RESULT(Allow, [], Lang); + [<<"stopped nodes">>] -> ?INFO_RESULT(Allow, [], Lang); [<<"running nodes">>, _ENode] -> - ?INFO_RESULT(Allow, [?NS_STATS]); + ?INFO_RESULT(Allow, [?NS_STATS], Lang); [<<"running nodes">>, _ENode, <<"DB">>] -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); [<<"running nodes">>, _ENode, <<"modules">>] -> - ?INFO_RESULT(Allow, []); + ?INFO_RESULT(Allow, [], Lang); [<<"running nodes">>, _ENode, <<"modules">>, _] -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); [<<"running nodes">>, _ENode, <<"backup">>] -> - ?INFO_RESULT(Allow, []); + ?INFO_RESULT(Allow, [], Lang); [<<"running nodes">>, _ENode, <<"backup">>, _] -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); [<<"running nodes">>, _ENode, <<"import">>] -> - ?INFO_RESULT(Allow, []); + ?INFO_RESULT(Allow, [], Lang); [<<"running nodes">>, _ENode, <<"import">>, _] -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); [<<"running nodes">>, _ENode, <<"restart">>] -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); [<<"running nodes">>, _ENode, <<"shutdown">>] -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); [<<"config">>, _] -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"add-user">>) -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"delete-user">>) -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"end-user-session">>) -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"get-user-password">>) -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"change-user-password">>) -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"get-user-lastlogin">>) -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"user-stats">>) -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"get-registered-users-num">>) -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"get-online-users-num">>) -> - ?INFO_RESULT(Allow, [?NS_COMMANDS]); + ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); _ -> Acc end end. @@ -318,7 +318,8 @@ get_sm_items(Acc, From, {result, Items ++ Nodes ++ get_user_resources(User, Server)}; {allow, <<"config">>} -> {result, []}; - {_, <<"config">>} -> {error, ?ERR_FORBIDDEN}; + {_, <<"config">>} -> + {error, ?ERRT_FORBIDDEN(Lang, <<"Denied by ACL">>)}; _ -> Acc end end. @@ -350,7 +351,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 @@ -379,9 +380,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 == <<"">>) -> @@ -448,63 +449,64 @@ get_local_items(Acc, From, #jid{lserver = LServer} = To, _ -> LNode = tokenize(Node), Allow = acl:match_rule(LServer, configure, From), + Err = ?ERRT_FORBIDDEN(Lang, <<"Denied by ACL">>), case LNode of [<<"config">>] -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"user">>] -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"online users">>] -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"all users">>] -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"all users">>, <<$@, _/binary>>] -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"outgoing s2s">> | _] -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"running nodes">>] -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"stopped nodes">>] -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"running nodes">>, _ENode] -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"running nodes">>, _ENode, <<"DB">>] -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"running nodes">>, _ENode, <<"modules">>] -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"running nodes">>, _ENode, <<"modules">>, _] -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"running nodes">>, _ENode, <<"backup">>] -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"running nodes">>, _ENode, <<"backup">>, _] -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"running nodes">>, _ENode, <<"import">>] -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"running nodes">>, _ENode, <<"import">>, _] -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"running nodes">>, _ENode, <<"restart">>] -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"running nodes">>, _ENode, <<"shutdown">>] -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"config">>, _] -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"add-user">>) -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"delete-user">>) -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"end-user-session">>) -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"get-user-password">>) -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"change-user-password">>) -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"get-user-lastlogin">>) -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"user-stats">>) -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"get-registered-users-num">>) -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"get-online-users-num">>) -> - ?ITEMS_RESULT(Allow, LNode, {error, ?ERR_FORBIDDEN}); + ?ITEMS_RESULT(Allow, LNode, {error, Err}); _ -> Acc end end. @@ -562,33 +564,29 @@ get_local_items({_, Host}, [<<"all users">>], _Server, get_local_items({_, Host}, [<<"all users">>, <<$@, Diap/binary>>], _Server, _Lang) -> - case catch ejabberd_auth:get_vh_registered_users(Host) - of - {'EXIT', _Reason} -> ?ERR_INTERNAL_SERVER_ERROR; - Users -> - SUsers = lists:sort([{S, U} || {U, S} <- Users]), - case catch begin - [S1, S2] = ejabberd_regexp:split(Diap, <<"-">>), - N1 = jlib:binary_to_integer(S1), - N2 = jlib:binary_to_integer(S2), - Sub = lists:sublist(SUsers, N1, N2 - N1 + 1), - lists:map(fun ({S, U}) -> - #xmlel{name = <<"item">>, - attrs = - [{<<"jid">>, - <<U/binary, "@", - S/binary>>}, - {<<"name">>, - <<U/binary, "@", - S/binary>>}], - children = []} - end, - Sub) - end - of - {'EXIT', _Reason} -> ?ERR_NOT_ACCEPTABLE; - Res -> {result, Res} - end + Users = ejabberd_auth:get_vh_registered_users(Host), + SUsers = lists:sort([{S, U} || {U, S} <- Users]), + case catch begin + [S1, S2] = ejabberd_regexp:split(Diap, <<"-">>), + N1 = jlib:binary_to_integer(S1), + N2 = jlib:binary_to_integer(S2), + Sub = lists:sublist(SUsers, N1, N2 - N1 + 1), + lists:map(fun ({S, U}) -> + #xmlel{name = <<"item">>, + attrs = + [{<<"jid">>, + <<U/binary, "@", + S/binary>>}, + {<<"name">>, + <<U/binary, "@", + S/binary>>}], + children = []} + end, + Sub) + end + of + {'EXIT', _Reason} -> ?ERR_NOT_ACCEPTABLE; + Res -> {result, Res} end; get_local_items({_, Host}, [<<"outgoing s2s">>], _Server, Lang) -> @@ -826,33 +824,33 @@ get_stopped_nodes(_Lang) -> %%------------------------------------------------------------------------- -define(COMMANDS_RESULT(LServerOrGlobal, From, To, - Request), + Request, Lang), case acl:match_rule(LServerOrGlobal, configure, From) of - deny -> {error, ?ERR_FORBIDDEN}; + deny -> {error, ?ERRT_FORBIDDEN(Lang, <<"Denied by ACL">>)}; allow -> adhoc_local_commands(From, To, Request) end). adhoc_local_commands(Acc, From, #jid{lserver = LServer} = To, - #adhoc_request{node = Node} = Request) -> + #adhoc_request{node = Node, lang = Lang} = Request) -> LNode = tokenize(Node), case LNode of [<<"running nodes">>, _ENode, <<"DB">>] -> - ?COMMANDS_RESULT(global, From, To, Request); + ?COMMANDS_RESULT(global, From, To, Request, Lang); [<<"running nodes">>, _ENode, <<"modules">>, _] -> - ?COMMANDS_RESULT(LServer, From, To, Request); + ?COMMANDS_RESULT(LServer, From, To, Request, Lang); [<<"running nodes">>, _ENode, <<"backup">>, _] -> - ?COMMANDS_RESULT(global, From, To, Request); + ?COMMANDS_RESULT(global, From, To, Request, Lang); [<<"running nodes">>, _ENode, <<"import">>, _] -> - ?COMMANDS_RESULT(global, From, To, Request); + ?COMMANDS_RESULT(global, From, To, Request, Lang); [<<"running nodes">>, _ENode, <<"restart">>] -> - ?COMMANDS_RESULT(global, From, To, Request); + ?COMMANDS_RESULT(global, From, To, Request, Lang); [<<"running nodes">>, _ENode, <<"shutdown">>] -> - ?COMMANDS_RESULT(global, From, To, Request); + ?COMMANDS_RESULT(global, From, To, Request, Lang); [<<"config">>, _] -> - ?COMMANDS_RESULT(LServer, From, To, Request); + ?COMMANDS_RESULT(LServer, From, To, Request, Lang); ?NS_ADMINL(_) -> - ?COMMANDS_RESULT(LServer, From, To, Request); + ?COMMANDS_RESULT(LServer, From, To, Request, Lang); _ -> Acc end. @@ -882,7 +880,8 @@ adhoc_local_commands(From, end; XData /= false, ActionIsExecute -> case jlib:parse_xdata_submit(XData) of - invalid -> {error, ?ERR_BAD_REQUEST}; + invalid -> + {error, ?ERRT_BAD_REQUEST(Lang, <<"Incorrect data form">>)}; Fields -> case catch set_form(From, LServer, LNode, Lang, Fields) of @@ -898,7 +897,8 @@ adhoc_local_commands(From, {error, Error} -> {error, Error} end end; - true -> {error, ?ERR_BAD_REQUEST} + true -> + {error, ?ERRT_BAD_REQUEST(Lang, <<"Incorrect action or data form">>)} end. -define(TVFIELD(Type, Var, Val), @@ -980,10 +980,14 @@ adhoc_local_commands(From, get_form(_Host, [<<"running nodes">>, ENode, <<"DB">>], Lang) -> case search_running_node(ENode) of - false -> {error, ?ERR_ITEM_NOT_FOUND}; + false -> + Txt = <<"No running node found">>, + {error, ?ERRT_ITEM_NOT_FOUND(Lang, Txt)}; Node -> case ejabberd_cluster:call(Node, mnesia, system_info, [tables]) of - {badrpc, _Reason} -> + {badrpc, Reason} -> + ?ERROR_MSG("RPC call mnesia:system_info(tables) on node " + "~s failed: ~p", [Node, Reason]), {error, ?ERR_INTERNAL_SERVER_ERROR}; Tables -> STables = lists:sort(Tables), @@ -1023,10 +1027,14 @@ get_form(Host, [<<"running nodes">>, ENode, <<"modules">>, <<"stop">>], Lang) -> case search_running_node(ENode) of - false -> {error, ?ERR_ITEM_NOT_FOUND}; + false -> + Txt = <<"No running node found">>, + {error, ?ERRT_ITEM_NOT_FOUND(Lang, Txt)}; Node -> case ejabberd_cluster:call(Node, gen_mod, loaded_modules, [Host]) of - {badrpc, _Reason} -> + {badrpc, Reason} -> + ?ERROR_MSG("RPC call gen_mod:loaded_modules(~s) on node " + "~s failed: ~p", [Host, Node, Reason]), {error, ?ERR_INTERNAL_SERVER_ERROR}; Modules -> SModules = lists:sort(Modules), @@ -1562,9 +1570,11 @@ get_form(_Host, _, _Lang) -> {error, ?ERR_SERVICE_UNAVAILABLE}. set_form(_From, _Host, - [<<"running nodes">>, ENode, <<"DB">>], _Lang, XData) -> + [<<"running nodes">>, ENode, <<"DB">>], Lang, XData) -> case search_running_node(ENode) of - false -> {error, ?ERR_ITEM_NOT_FOUND}; + false -> + Txt = <<"No running node found">>, + {error, ?ERRT_ITEM_NOT_FOUND(Lang, Txt)}; Node -> lists:foreach(fun ({SVar, SVals}) -> Table = jlib:binary_to_atom(SVar), @@ -1596,9 +1606,11 @@ set_form(_From, _Host, end; set_form(_From, Host, [<<"running nodes">>, ENode, <<"modules">>, <<"stop">>], - _Lang, XData) -> + Lang, XData) -> case search_running_node(ENode) of - false -> {error, ?ERR_ITEM_NOT_FOUND}; + false -> + Txt = <<"No running node found">>, + {error, ?ERRT_ITEM_NOT_FOUND(Lang, Txt)}; Node -> lists:foreach(fun ({Var, Vals}) -> case Vals of @@ -1615,12 +1627,16 @@ set_form(_From, Host, set_form(_From, Host, [<<"running nodes">>, ENode, <<"modules">>, <<"start">>], - _Lang, XData) -> + Lang, XData) -> case search_running_node(ENode) of - false -> {error, ?ERR_ITEM_NOT_FOUND}; + false -> + Txt = <<"No running node found">>, + {error, ?ERRT_ITEM_NOT_FOUND(Lang, Txt)}; Node -> case lists:keysearch(<<"modules">>, 1, XData) of - false -> {error, ?ERR_BAD_REQUEST}; + false -> + Txt = <<"No 'modules' found in data form">>, + {error, ?ERRT_BAD_REQUEST(Lang, Txt)}; {value, {_, Strings}} -> String = lists:foldl(fun (S, Res) -> <<Res/binary, S/binary, "\n">> @@ -1637,98 +1653,143 @@ set_form(_From, Host, end, Modules), {result, []}; - _ -> {error, ?ERR_BAD_REQUEST} + _ -> {error, ?ERRT_BAD_REQUEST(Lang, <<"Parse failed">>)} end; - _ -> {error, ?ERR_BAD_REQUEST} + _ -> {error, ?ERRT_BAD_REQUEST(Lang, <<"Scan failed">>)} end end end; set_form(_From, _Host, [<<"running nodes">>, ENode, <<"backup">>, <<"backup">>], - _Lang, XData) -> + Lang, XData) -> case search_running_node(ENode) of - false -> {error, ?ERR_ITEM_NOT_FOUND}; + false -> + Txt = <<"No running node found">>, + {error, ?ERRT_ITEM_NOT_FOUND(Lang, Txt)}; Node -> case lists:keysearch(<<"path">>, 1, XData) of - false -> {error, ?ERR_BAD_REQUEST}; + false -> + Txt = <<"No 'path' found in data form">>, + {error, ?ERRT_BAD_REQUEST(Lang, Txt)}; {value, {_, [String]}} -> case ejabberd_cluster:call(Node, mnesia, backup, [String]) of - {badrpc, _Reason} -> + {badrpc, Reason} -> + ?ERROR_MSG("RPC call mnesia:backup(~s) to node ~s " + "failed: ~p", [String, Node, Reason]), + {error, ?ERR_INTERNAL_SERVER_ERROR}; + {error, Reason} -> + ?ERROR_MSG("RPC call mnesia:backup(~s) to node ~s " + "failed: ~p", [String, Node, Reason]), {error, ?ERR_INTERNAL_SERVER_ERROR}; - {error, _Reason} -> {error, ?ERR_INTERNAL_SERVER_ERROR}; _ -> {result, []} end; - _ -> {error, ?ERR_BAD_REQUEST} + _ -> + Txt = <<"Incorrect value of 'path' in data form">>, + {error, ?ERRT_BAD_REQUEST(Lang, Txt)} end end; set_form(_From, _Host, [<<"running nodes">>, ENode, <<"backup">>, <<"restore">>], - _Lang, XData) -> + Lang, XData) -> case search_running_node(ENode) of - false -> {error, ?ERR_ITEM_NOT_FOUND}; + false -> + Txt = <<"No running node found">>, + {error, ?ERRT_ITEM_NOT_FOUND(Lang, Txt)}; Node -> case lists:keysearch(<<"path">>, 1, XData) of - false -> {error, ?ERR_BAD_REQUEST}; + false -> + Txt = <<"No 'path' found in data form">>, + {error, ?ERRT_BAD_REQUEST(Lang, Txt)}; {value, {_, [String]}} -> case ejabberd_cluster:call(Node, ejabberd_admin, restore, [String]) of - {badrpc, _Reason} -> + {badrpc, Reason} -> + ?ERROR_MSG("RPC call ejabberd_admin:restore(~s) to node " + "~s failed: ~p", [String, Node, Reason]), + {error, ?ERR_INTERNAL_SERVER_ERROR}; + {error, Reason} -> + ?ERROR_MSG("RPC call ejabberd_admin:restore(~s) to node " + "~s failed: ~p", [String, Node, Reason]), {error, ?ERR_INTERNAL_SERVER_ERROR}; - {error, _Reason} -> {error, ?ERR_INTERNAL_SERVER_ERROR}; _ -> {result, []} end; - _ -> {error, ?ERR_BAD_REQUEST} + _ -> + Txt = <<"Incorrect value of 'path' in data form">>, + {error, ?ERRT_BAD_REQUEST(Lang, Txt)} end end; set_form(_From, _Host, [<<"running nodes">>, ENode, <<"backup">>, <<"textfile">>], - _Lang, XData) -> + Lang, XData) -> case search_running_node(ENode) of - false -> {error, ?ERR_ITEM_NOT_FOUND}; + false -> + Txt = <<"No running node found">>, + {error, ?ERRT_ITEM_NOT_FOUND(Lang, Txt)}; Node -> case lists:keysearch(<<"path">>, 1, XData) of - false -> {error, ?ERR_BAD_REQUEST}; + false -> + Txt = <<"No 'path' found in data form">>, + {error, ?ERRT_BAD_REQUEST(Lang, Txt)}; {value, {_, [String]}} -> case ejabberd_cluster:call(Node, ejabberd_admin, dump_to_textfile, [String]) of - {badrpc, _Reason} -> + {badrpc, Reason} -> + ?ERROR_MSG("RPC call ejabberd_admin:dump_to_textfile(~s) " + "to node ~s failed: ~p", [String, Node, Reason]), + {error, ?ERR_INTERNAL_SERVER_ERROR}; + {error, Reason} -> + ?ERROR_MSG("RPC call ejabberd_admin:dump_to_textfile(~s) " + "to node ~s failed: ~p", [String, Node, Reason]), {error, ?ERR_INTERNAL_SERVER_ERROR}; - {error, _Reason} -> {error, ?ERR_INTERNAL_SERVER_ERROR}; _ -> {result, []} end; - _ -> {error, ?ERR_BAD_REQUEST} + _ -> + Txt = <<"Incorrect value of 'path' in data form">>, + {error, ?ERRT_BAD_REQUEST(Lang, Txt)} end end; set_form(_From, _Host, [<<"running nodes">>, ENode, <<"import">>, <<"file">>], - _Lang, XData) -> + Lang, XData) -> case search_running_node(ENode) of - false -> {error, ?ERR_ITEM_NOT_FOUND}; + false -> + Txt = <<"No running node found">>, + {error, ?ERRT_ITEM_NOT_FOUND(Lang, Txt)}; Node -> case lists:keysearch(<<"path">>, 1, XData) of - false -> {error, ?ERR_BAD_REQUEST}; + false -> + Txt = <<"No 'path' found in data form">>, + {error, ?ERRT_BAD_REQUEST(Lang, Txt)}; {value, {_, [String]}} -> ejabberd_cluster:call(Node, jd2ejd, import_file, [String]), {result, []}; - _ -> {error, ?ERR_BAD_REQUEST} + _ -> + Txt = <<"Incorrect value of 'path' in data form">>, + {error, ?ERRT_BAD_REQUEST(Lang, Txt)} end end; set_form(_From, _Host, [<<"running nodes">>, ENode, <<"import">>, <<"dir">>], - _Lang, XData) -> + Lang, XData) -> case search_running_node(ENode) of - false -> {error, ?ERR_ITEM_NOT_FOUND}; + false -> + Txt = <<"No running node found">>, + {error, ?ERRT_ITEM_NOT_FOUND(Lang, Txt)}; Node -> case lists:keysearch(<<"path">>, 1, XData) of - false -> {error, ?ERR_BAD_REQUEST}; + false -> + Txt = <<"No 'path' found in data form">>, + {error, ?ERRT_BAD_REQUEST(Lang, Txt)}; {value, {_, [String]}} -> ejabberd_cluster:call(Node, jd2ejd, import_dir, [String]), {result, []}; - _ -> {error, ?ERR_BAD_REQUEST} + _ -> + Txt = <<"Incorrect value of 'path' in data form">>, + {error, ?ERRT_BAD_REQUEST(Lang, Txt)} end end; set_form(From, Host, @@ -1739,7 +1800,7 @@ set_form(From, Host, [<<"running nodes">>, ENode, <<"shutdown">>], _Lang, XData) -> stop_node(From, Host, ENode, stop, XData); -set_form(_From, Host, [<<"config">>, <<"acls">>], _Lang, +set_form(_From, Host, [<<"config">>, <<"acls">>], Lang, XData) -> case lists:keysearch(<<"acls">>, 1, XData) of {value, {_, Strings}} -> @@ -1753,14 +1814,16 @@ set_form(_From, Host, [<<"config">>, <<"acls">>], _Lang, {ok, ACLs} -> acl:add_list(Host, ACLs, true), {result, []}; - _ -> {error, ?ERR_BAD_REQUEST} + _ -> {error, ?ERRT_BAD_REQUEST(Lang, <<"Parse failed">>)} end; - _ -> {error, ?ERR_BAD_REQUEST} + _ -> {error, ?ERRT_BAD_REQUEST(Lang, <<"Scan failed">>)} end; - _ -> {error, ?ERR_BAD_REQUEST} + _ -> + Txt = <<"No 'acls' found in data form">>, + {error, ?ERRT_BAD_REQUEST(Lang, Txt)} end; set_form(_From, Host, [<<"config">>, <<"access">>], - _Lang, XData) -> + Lang, XData) -> SetAccess = fun (Rs) -> mnesia:transaction(fun () -> Os = mnesia:select(local_config, @@ -1803,11 +1866,13 @@ set_form(_From, Host, [<<"config">>, <<"access">>], {atomic, _} -> {result, []}; _ -> {error, ?ERR_BAD_REQUEST} end; - _ -> {error, ?ERR_BAD_REQUEST} + _ -> {error, ?ERRT_BAD_REQUEST(Lang, <<"Parse failed">>)} end; - _ -> {error, ?ERR_BAD_REQUEST} + _ -> {error, ?ERRT_BAD_REQUEST(Lang, <<"Scan failed">>)} end; - _ -> {error, ?ERR_BAD_REQUEST} + _ -> + Txt = <<"No 'access' found in data form">>, + {error, ?ERRT_BAD_REQUEST(Lang, Txt)} end; set_form(From, Host, ?NS_ADMINL(<<"add-user">>), _Lang, XData) -> @@ -2052,7 +2117,7 @@ adhoc_sm_commands(_Acc, From, action = Action, xdata = XData} = Request) -> case acl:match_rule(LServer, configure, From) of - deny -> {error, ?ERR_FORBIDDEN}; + deny -> {error, ?ERRT_FORBIDDEN(Lang, <<"Denied by ACL">>)}; allow -> ActionIsExecute = lists:member(Action, [<<"">>, <<"execute">>, @@ -2071,11 +2136,15 @@ adhoc_sm_commands(_Acc, From, end; XData /= false, ActionIsExecute -> case jlib:parse_xdata_submit(XData) of - invalid -> {error, ?ERR_BAD_REQUEST}; + invalid -> + Txt = <<"Incorrect data form">>, + {error, ?ERRT_BAD_REQUEST(Lang, Txt)}; Fields -> set_sm_form(User, Server, <<"config">>, Request, Fields) end; - true -> {error, ?ERR_BAD_REQUEST} + true -> + Txt = <<"Incorrect action or data form">>, + {error, ?ERRT_BAD_REQUEST(Lang, Txt)} end end; adhoc_sm_commands(Acc, _From, _To, _Request) -> Acc. @@ -2135,12 +2204,16 @@ set_sm_form(User, Server, <<"config">>, {value, {_, [Password]}} -> ejabberd_auth:set_password(User, Server, Password), adhoc:produce_response(Response); - _ -> {error, ?ERR_NOT_ACCEPTABLE} + _ -> + Txt = <<"No 'password' found in data form">>, + {error, ?ERRT_NOT_ACCEPTABLE(Lang, Txt)} end; {value, {_, [<<"remove">>]}} -> catch ejabberd_auth:remove_user(User, Server), adhoc:produce_response(Response); - _ -> {error, ?ERR_NOT_ACCEPTABLE} + _ -> + Txt = <<"Incorrect value of 'action' in data form">>, + {error, ?ERRT_NOT_ACCEPTABLE(Lang, Txt)} end; set_sm_form(_User, _Server, _Node, _Request, _Fields) -> {error, ?ERR_SERVICE_UNAVAILABLE}. diff --git a/src/mod_configure2.erl b/src/mod_configure2.erl index 3e00a9cf7..a8287b4d4 100644 --- a/src/mod_configure2.erl +++ b/src/mod_configure2.erl @@ -56,16 +56,18 @@ stop(Host) -> ?NS_ECONFIGURE). process_local_iq(From, To, - #iq{type = Type, lang = _Lang, sub_el = SubEl} = IQ) -> + #iq{type = Type, lang = Lang, sub_el = SubEl} = IQ) -> case acl:match_rule(To#jid.lserver, configure, From) of deny -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; + Txt = <<"Denied by ACL">>, + IQ#iq{type = error, sub_el = [SubEl, ?ERRT_NOT_ALLOWED(Lang, Txt)]}; allow -> case Type of set -> + Txt = <<"Value 'set' of 'type' attribute is not allowed">>, IQ#iq{type = error, - sub_el = [SubEl, ?ERR_FEATURE_NOT_IMPLEMENTED]}; - %%case xml:get_tag_attr_s("type", SubEl) of + sub_el = [SubEl, ?ERRT_FEATURE_NOT_IMPLEMENTED(Lang, Txt)]}; + %%case fxml:get_tag_attr_s("type", SubEl) of %% "cancel" -> %% IQ#iq{type = result, %% sub_el = [{xmlelement, "query", @@ -79,7 +81,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} -> @@ -98,7 +100,7 @@ process_local_iq(From, To, %% sub_el = [SubEl, ?ERR_NOT_ALLOWED]} %%end; get -> - case process_get(SubEl) of + case process_get(SubEl, Lang) of {result, Res} -> IQ#iq{type = result, sub_el = [Res]}; {error, Error} -> IQ#iq{type = error, sub_el = [SubEl, Error]} @@ -106,7 +108,7 @@ process_local_iq(From, To, end end. -process_get(#xmlel{name = <<"info">>}) -> +process_get(#xmlel{name = <<"info">>}, _Lang) -> S2SConns = ejabberd_s2s:dirty_get_connections(), TConns = lists:usort([element(2, C) || C <- S2SConns]), Attrs = [{<<"registered-users">>, @@ -130,7 +132,7 @@ process_get(#xmlel{name = <<"info">>}) -> attrs = [{<<"xmlns">>, ?NS_ECONFIGURE} | Attrs], children = []}}; process_get(#xmlel{name = <<"welcome-message">>, - attrs = Attrs}) -> + attrs = Attrs}, _Lang) -> {Subj, Body} = ejabberd_config:get_option( welcome_message, fun({Subj, Body}) -> @@ -146,7 +148,7 @@ process_get(#xmlel{name = <<"welcome-message">>, #xmlel{name = <<"body">>, attrs = [], children = [{xmlcdata, Body}]}]}}; process_get(#xmlel{name = <<"registration-watchers">>, - attrs = Attrs}) -> + attrs = Attrs}, _Lang) -> SubEls = ejabberd_config:get_option( registration_watchers, fun(JIDs) when is_list(JIDs) -> @@ -160,14 +162,14 @@ process_get(#xmlel{name = <<"registration-watchers">>, {result, #xmlel{name = <<"registration_watchers">>, attrs = Attrs, children = SubEls}}; -process_get(#xmlel{name = <<"acls">>, attrs = Attrs}) -> +process_get(#xmlel{name = <<"acls">>, attrs = Attrs}, _Lang) -> Str = iolist_to_binary(io_lib:format("~p.", [ets:tab2list(acl)])), {result, #xmlel{name = <<"acls">>, attrs = Attrs, children = [{xmlcdata, Str}]}}; process_get(#xmlel{name = <<"access">>, - attrs = Attrs}) -> + attrs = Attrs}, _Lang) -> Str = iolist_to_binary(io_lib:format("~p.", [ets:select(local_config, [{{local_config, {access, '$1'}, @@ -178,13 +180,14 @@ process_get(#xmlel{name = <<"access">>, {result, #xmlel{name = <<"access">>, attrs = Attrs, children = [{xmlcdata, Str}]}}; -process_get(#xmlel{name = <<"last">>, attrs = Attrs}) -> +process_get(#xmlel{name = <<"last">>, attrs = Attrs}, Lang) -> case catch mnesia:dirty_select(last_activity, [{{last_activity, '_', '$1', '_'}, [], ['$1']}]) of {'EXIT', _Reason} -> - {error, ?ERR_INTERNAL_SERVER_ERROR}; + Txt = <<"Database failure">>, + {error, ?ERRT_INTERNAL_SERVER_ERROR(Lang, Txt)}; Vals -> TimeStamp = p1_time_compat:system_time(seconds), Str = list_to_binary( @@ -196,7 +199,7 @@ process_get(#xmlel{name = <<"last">>, attrs = Attrs}) -> end; %%process_get({xmlelement, Name, Attrs, SubEls}) -> %% {result, }; -process_get(_) -> {error, ?ERR_BAD_REQUEST}. +process_get(_, _) -> {error, ?ERR_BAD_REQUEST}. mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; mod_opt_type(_) -> [iqdisc]. diff --git a/src/mod_disco.erl b/src/mod_disco.erl index 6e0b9c929..0d5abcb4b 100644 --- a/src/mod_disco.erl +++ b/src/mod_disco.erl @@ -150,9 +150,10 @@ process_local_iq_items(From, To, #iq{type = Type, lang = Lang, sub_el = SubEl} = IQ) -> case Type of set -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; + Txt = <<"Value 'set' of 'type' attribute is not allowed">>, + IQ#iq{type = error, sub_el = [SubEl, ?ERRT_NOT_ALLOWED(Lang, Txt)]}; 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,10 +178,11 @@ process_local_iq_info(From, To, #iq{type = Type, lang = Lang, sub_el = SubEl} = IQ) -> case Type of set -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; + Txt = <<"Value 'set' of 'type' attribute is not allowed">>, + IQ#iq{type = error, sub_el = [SubEl, ?ERRT_NOT_ALLOWED(Lang, Txt)]}; 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, [], @@ -229,10 +231,12 @@ get_local_features(Acc, _From, To, <<>>, _Lang) -> ets:select(disco_features, [{{{'_', Host}}, [], ['$_']}]) ++ Feats}; -get_local_features(Acc, _From, _To, _Node, _Lang) -> +get_local_features(Acc, _From, _To, _Node, Lang) -> case Acc of {result, _Features} -> Acc; - empty -> {error, ?ERR_ITEM_NOT_FOUND} + empty -> + Txt = <<"No features available">>, + {error, ?ERRT_ITEM_NOT_FOUND(Lang, Txt)} end. features_to_xml(FeatureList) -> @@ -271,8 +275,8 @@ get_local_services(Acc, _From, To, <<>>, _Lang) -> get_local_services({result, _} = Acc, _From, _To, _Node, _Lang) -> Acc; -get_local_services(empty, _From, _To, _Node, _Lang) -> - {error, ?ERR_ITEM_NOT_FOUND}. +get_local_services(empty, _From, _To, _Node, Lang) -> + {error, ?ERRT_ITEM_NOT_FOUND(Lang, <<"No services available">>)}. get_vh_services(Host) -> Hosts = lists:sort(fun (H1, H2) -> @@ -300,12 +304,13 @@ process_sm_iq_items(From, To, #iq{type = Type, lang = Lang, sub_el = SubEl} = IQ) -> case Type of set -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; + Txt = <<"Value 'set' of 'type' attribute is not allowed">>, + IQ#iq{type = error, sub_el = [SubEl, ?ERRT_NOT_ALLOWED(Lang, Txt)]}; get -> 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 @@ -325,8 +330,9 @@ process_sm_iq_items(From, To, IQ#iq{type = error, sub_el = [SubEl, Error]} end; false -> + Txt = <<"Not subscribed">>, IQ#iq{type = error, - sub_el = [SubEl, ?ERR_SERVICE_UNAVAILABLE]} + sub_el = [SubEl, ?ERRT_SERVICE_UNAVAILABLE(Lang, Txt)]} end end. @@ -347,12 +353,14 @@ get_sm_items(Acc, From, get_sm_items({result, _} = Acc, _From, _To, _Node, _Lang) -> Acc; -get_sm_items(empty, From, To, _Node, _Lang) -> +get_sm_items(empty, From, To, _Node, Lang) -> #jid{luser = LFrom, lserver = LSFrom} = From, #jid{luser = LTo, lserver = LSTo} = To, case {LFrom, LSFrom} of {LTo, LSTo} -> {error, ?ERR_ITEM_NOT_FOUND}; - _ -> {error, ?ERR_NOT_ALLOWED} + _ -> + Txt = <<"Query to another users is forbidden">>, + {error, ?ERRT_NOT_ALLOWED(Lang, Txt)} end. is_presence_subscribed(#jid{luser = User, lserver = Server}, @@ -373,15 +381,18 @@ process_sm_iq_info(From, To, #iq{type = Type, lang = Lang, sub_el = SubEl} = IQ) -> case Type of set -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; + Txt = <<"Value 'set' of 'type' attribute is not allowed">>, + IQ#iq{type = error, sub_el = [SubEl, ?ERRT_NOT_ALLOWED(Lang, Txt)]}; get -> 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 @@ -397,14 +408,15 @@ 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]} end; false -> + Txt = <<"Not subscribed">>, IQ#iq{type = error, - sub_el = [SubEl, ?ERR_SERVICE_UNAVAILABLE]} + sub_el = [SubEl, ?ERRT_SERVICE_UNAVAILABLE(Lang, Txt)]} end end. @@ -421,12 +433,14 @@ get_sm_identity(Acc, _From, _ -> [] end. -get_sm_features(empty, From, To, _Node, _Lang) -> +get_sm_features(empty, From, To, _Node, Lang) -> #jid{luser = LFrom, lserver = LSFrom} = From, #jid{luser = LTo, lserver = LSTo} = To, case {LFrom, LSFrom} of {LTo, LSTo} -> {error, ?ERR_ITEM_NOT_FOUND}; - _ -> {error, ?ERR_NOT_ALLOWED} + _ -> + Txt = <<"Query to another users is forbidden">>, + {error, ?ERRT_NOT_ALLOWED(Lang, Txt)} end; get_sm_features(Acc, _From, _To, _Node, _Lang) -> Acc. diff --git a/src/mod_echo.erl b/src/mod_echo.erl index 3f91f1d03..7d9f81f82 100644 --- a/src/mod_echo.erl +++ b/src/mod_echo.erl @@ -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}}. %%-------------------------------------------------------------------- @@ -118,7 +118,10 @@ handle_cast(_Msg, State) -> {noreply, State}. handle_info({route, From, To, Packet}, State) -> Packet2 = case From#jid.user of <<"">> -> - jlib:make_error_reply(Packet, ?ERR_BAD_REQUEST); + Lang = fxml:get_tag_attr_s(<<"xml:lang">>, Packet), + Txt = <<"User part of JID in 'from' is empty">>, + jlib:make_error_reply( + Packet, ?ERRT_BAD_REQUEST(Lang, Txt)); _ -> Packet end, do_client_version(disabled, To, From), @@ -182,7 +185,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 [] diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl index 7f86d5cc1..c4fae2022 100644 --- a/src/mod_http_api.erl +++ b/src/mod_http_api.erl @@ -28,9 +28,14 @@ %% in ejabberd_http listener %% request_handlers: %% "/api": mod_http_api -%% +%% +%% To use a specific API version N, add a vN element in the URL path: +%% in ejabberd_http listener +%% request_handlers: +%% "/api/v2": mod_http_api +%% %% Access rights are defined with: -%% commands_admin_access: configure +%% commands_admin_access: configure %% commands: %% - add_commands: user %% @@ -43,6 +48,25 @@ %% %% Then to perform an action, send a POST request to the following URL: %% http://localhost:5280/api/<call_name> +%% +%% It's also possible to enable unrestricted access to some commands from group +%% of IP addresses by using option `admin_ip_access` by having fragment like +%% this in configuration file: +%% modules: +%% mod_http_api: +%% admin_ip_access: admin_ip_access_rule +%%... +%% access: +%% admin_ip_access_rule: +%% admin_ip_acl: +%% - command1 +%% - command2 +%% %% use `all` to give access to all commands +%%... +%% acl: +%% admin_ip_acl: +%% ip: +%% - "127.0.0.1/8" -module(mod_http_api). @@ -57,6 +81,8 @@ -include("logger.hrl"). -include("ejabberd_http.hrl"). +-define(DEFAULT_API_VERSION, 0). + -define(CT_PLAIN, {<<"Content-Type">>, <<"text/plain">>}). @@ -97,60 +123,94 @@ start(_Host, _Opts) -> stop(_Host) -> ok. - %% ---------- %% basic auth %% ---------- -check_permissions(#request{auth = HTTPAuth, headers = Headers}, Command) - when HTTPAuth /= undefined -> +check_permissions(Request, Command) -> 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 + {ok, CommandPolicy} = ejabberd_commands:get_command_policy(Call), + check_permissions2(Request, Call, CommandPolicy); + _ -> + unauthorized_response() + end. + +check_permissions2(#request{auth = HTTPAuth, headers = Headers}, Call, _) + when HTTPAuth /= undefined -> + 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, - case Auth of - {ok, A} -> {allowed, Call, A}; + end; + {oauth, Token, _} -> + case oauth_check_token(Call, 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; +check_permissions2(_Request, Call, open) -> + {allowed, Call, noauth}; +check_permissions2(#request{ip={IP, _Port}}, Call, _Policy) -> + Access = gen_mod:get_module_opt(global, ?MODULE, admin_ip_access, + mod_opt_type(admin_ip_access), + none), + Res = acl:match_rule(global, Access, IP), + case Res of + all -> + {allowed, Call, admin}; + [all] -> + {allowed, Call, admin}; + allow -> + {allowed, Call, admin}; + Commands when is_list(Commands) -> + case lists:member(Call, Commands) of + true -> {allowed, Call, admin}; _ -> unauthorized_response() end; - _ -> + E -> + ?DEBUG("Unauthorized: ~p", [E]), unauthorized_response() end; -check_permissions(_, _Command) -> +check_permissions2(_Request, _Call, _Policy) -> unauthorized_response(). +oauth_check_token(Scope, Token) when is_atom(Scope) -> + oauth_check_token(atom_to_binary(Scope, utf8), Token); +oauth_check_token(Scope, Token) -> + ejabberd_oauth:check_token(Scope, Token). + %% ------------------ %% command processing %% ------------------ +%process(Call, Request) -> +% ?DEBUG("~p~n~p", [Call, Request]), ok; process(_, #request{method = 'POST', data = <<>>}) -> ?DEBUG("Bad Request: no data", []), - badrequest_response(); + badrequest_response(<<"Missing POST data">>); process([Call], #request{method = 'POST', data = Data, ip = IP} = Req) -> + Version = get_api_version(Req), try Args = case jiffy:decode(Data) of List when is_list(List) -> List; @@ -160,31 +220,37 @@ process([Call], #request{method = 'POST', data = Data, ip = IP} = Req) -> log(Call, Args, IP), case check_permissions(Req, Call) of {allowed, Cmd, Auth} -> - {Code, Result} = handle(Cmd, Auth, Args), + {Code, Result} = handle(Cmd, Auth, Args, Version), json_response(Code, jiffy:encode(Result)); + %% Warning: check_permission direcly formats 401 reply if not authorized ErrorResponse -> ErrorResponse end - catch _:Error -> - ?DEBUG("Bad Request: ~p", [Error]), - badrequest_response() + catch _:{error,{_,invalid_json}} = _Err -> + ?DEBUG("Bad Request: ~p", [_Err]), + badrequest_response(<<"Invalid JSON input">>); + _:_Error -> + ?DEBUG("Bad Request: ~p ~p", [_Error, erlang:get_stacktrace()]), + badrequest_response() end; process([Call], #request{method = 'GET', q = Data, ip = IP} = Req) -> + Version = get_api_version(Req), try Args = case Data of - [{nokey, <<>>}] -> []; - _ -> Data - end, + [{nokey, <<>>}] -> []; + _ -> Data + end, log(Call, Args, IP), case check_permissions(Req, Call) of {allowed, Cmd, Auth} -> - {Code, Result} = handle(Cmd, Auth, Args), + {Code, Result} = handle(Cmd, Auth, Args, Version), json_response(Code, jiffy:encode(Result)); + %% Warning: check_permission direcly formats 401 reply if not authorized ErrorResponse -> ErrorResponse end - catch _:Error -> - ?DEBUG("Bad Request: ~p", [Error]), + catch _:_Error -> + ?DEBUG("Bad Request: ~p ~p", [_Error, erlang:get_stacktrace()]), badrequest_response() end; process([], #request{method = 'OPTIONS', data = <<>>}) -> @@ -193,13 +259,28 @@ process(_Path, Request) -> ?DEBUG("Bad Request: no handler ~p", [Request]), badrequest_response(). +% get API version N from last "vN" element in URL path +get_api_version(#request{path = Path}) -> + get_api_version(lists:reverse(Path)); +get_api_version([<<"v", String/binary>> | Tail]) -> + case catch jlib:binary_to_integer(String) of + N when is_integer(N) -> + N; + _ -> + get_api_version(Tail) + end; +get_api_version([_Head | Tail]) -> + get_api_version(Tail); +get_api_version([]) -> + ?DEFAULT_API_VERSION. + %% ---------------- %% 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 +handle(Call, Auth, Args, Version) when is_atom(Call), is_list(Args) -> + case ejabberd_commands:get_command_format(Call, Auth, Version) of {ArgsSpec, _} when is_list(ArgsSpec) -> Args2 = [{jlib:binary_to_atom(Key), Value} || {Key, Value} <- Args], Spec = lists:foldr( @@ -214,23 +295,48 @@ handle(Call, Auth, Args) when is_atom(Call), is_list(Args) -> ({Key, atom}, Acc) -> [{Key, undefined}|Acc] end, [], ArgsSpec), - handle2(Call, Auth, match(Args2, Spec)); + try + handle2(Call, Auth, match(Args2, Spec), Version) + catch throw:not_found -> + {404, <<"not_found">>}; + throw:{not_found, Why} when is_atom(Why) -> + {404, jlib:atom_to_binary(Why)}; + throw:{not_found, Msg} -> + {404, iolist_to_binary(Msg)}; + throw:not_allowed -> + {401, <<"not_allowed">>}; + throw:{not_allowed, Why} when is_atom(Why) -> + {401, jlib:atom_to_binary(Why)}; + throw:{not_allowed, Msg} -> + {401, iolist_to_binary(Msg)}; + throw:{error, account_unprivileged} -> + {401, iolist_to_binary(<<"Unauthorized: Account Unpriviledged">>)}; + throw:{invalid_parameter, Msg} -> + {400, iolist_to_binary(Msg)}; + throw:{error, Why} when is_atom(Why) -> + {400, jlib:atom_to_binary(Why)}; + throw:{error, Msg} -> + {400, iolist_to_binary(Msg)}; + throw:Error when is_atom(Error) -> + {400, jlib:atom_to_binary(Error)}; + throw:Msg when is_list(Msg); is_binary(Msg) -> + {400, iolist_to_binary(Msg)}; + _Error -> + ?ERROR_MSG("REST API Error: ~p ~p", [_Error, erlang:get_stacktrace()]), + {500, <<"internal_error">>} + end; {error, Msg} -> + ?ERROR_MSG("REST API Error: ~p", [Msg]), {400, Msg}; _Error -> + ?ERROR_MSG("REST API Error: ~p", [_Error]), {400, <<"Error">>} end. -handle2(Call, Auth, Args) when is_atom(Call), is_list(Args) -> - {ArgsF, _ResultF} = ejabberd_commands:get_command_format(Call, Auth), +handle2(Call, Auth, Args, Version) when is_atom(Call), is_list(Args) -> + {ArgsF, _ResultF} = ejabberd_commands:get_command_format(Call, Auth, Version), 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. + ejabberd_command(Auth, Call, ArgsFormatted, Version). get_elem_delete(A, L) -> case proplists:get_all_values(A, L) of @@ -294,7 +400,9 @@ 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. + throw({invalid_parameter, + io_lib:format("Arg ~p is not in format ~p", + [Arg, Format])}). process_unicode_codepoints(Str) -> iolist_to_binary(lists:map(fun(X) when X > 255 -> unicode:characters_to_binary([X]); @@ -308,32 +416,37 @@ process_unicode_codepoints(Str) -> 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 +ejabberd_command(Auth, Cmd, Args, Version) -> + Access = case Auth of + admin -> []; + _ -> undefined + end, + case ejabberd_commands:execute_command(Access, Auth, Cmd, Args, Version) of + {error, Error} -> + throw(Error); + Res -> + format_command_result(Cmd, Auth, Res, Version) end. -format_command_result(Cmd, Auth, Result) -> - {_, ResultFormat} = ejabberd_commands:get_command_format(Cmd, Auth), +format_command_result(Cmd, Auth, Result, Version) -> + {_, ResultFormat} = ejabberd_commands:get_command_format(Cmd, Auth, Version), 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)]}} + {{_, rescode}, V} when V == true; V == ok -> + {200, 0}; + {{_, rescode}, _} -> + {200, 1}; + {{_, 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}) -> @@ -372,21 +485,24 @@ format_result(404, {_Name, _}) -> "not_found". unauthorized_response() -> - {401, ?HEADER(?CT_XML), - #xmlel{name = <<"h1">>, attrs = [], - children = [{xmlcdata, <<"401 Unauthorized">>}]}}. + unauthorized_response(<<"401 Unauthorized">>). +unauthorized_response(Body) -> + json_response(401, jiffy:encode(Body)). badrequest_response() -> - {400, ?HEADER(?CT_XML), - #xmlel{name = <<"h1">>, attrs = [], - children = [{xmlcdata, <<"400 Bad Request">>}]}}. + badrequest_response(<<"400 Bad Request">>). +badrequest_response(Body) -> + json_response(400, jiffy:encode(Body)). + 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]). + ?INFO_MSG("API call ~s ~p from ~s:~p", [Call, Args, AddrS, Port]); +log(Call, Args, IP) -> + ?INFO_MSG("API call ~s ~p (~p)", [Call, Args, IP]). -mod_opt_type(access) -> +mod_opt_type(admin_ip_access) -> fun(Access) when is_atom(Access) -> Access end; -mod_opt_type(_) -> [access]. +mod_opt_type(_) -> [admin_ip_access]. diff --git a/src/mod_http_upload.erl b/src/mod_http_upload.erl index 36d3626be..b01ca9d2d 100644 --- a/src/mod_http_upload.erl +++ b/src/mod_http_upload.erl @@ -86,7 +86,8 @@ %% Utility functions. -export([get_proc_name/2, - expand_home/1]). + expand_home/1, + expand_host/2]). -include("ejabberd.hrl"). -include("ejabberd_http.hrl"). @@ -273,6 +274,8 @@ init({ServerHost, Opts}) -> 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; @@ -289,7 +292,7 @@ init({ServerHost, Opts}) -> undefined -> ok; Mode -> - file:change_mode(DocRoot, Mode) + file:change_mode(DocRoot2, Mode) end, case Thumbnail of true -> @@ -303,13 +306,13 @@ init({ServerHost, Opts}) -> false -> ok end, - ejabberd_router:register_route(Host), + 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 = expand_home(str:strip(DocRoot, right, $/)), + docroot = DocRoot2, put_url = expand_host(str:strip(PutURL, right, $/), ServerHost), get_url = expand_host(str:strip(GetURL, right, $/), ServerHost), service_url = ServiceURL}}. @@ -372,7 +375,7 @@ handle_info(Info, State) -> ?ERROR_MSG("Got unexpected info: ~p", [Info]), {noreply, State}. --spec terminate(normal | shutdown | {shutdown, _} | _, _) -> ok. +-spec terminate(normal | shutdown | {shutdown, _} | _, state()) -> ok. terminate(Reason, #state{server_host = ServerHost, host = Host}) -> ?DEBUG("Stopping HTTP upload process for ~s: ~p", [ServerHost, Reason]), @@ -509,6 +512,12 @@ expand_home(Subject) -> 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. %%-------------------------------------------------------------------- @@ -526,7 +535,8 @@ process_iq(_From, IQ#iq{type = result, sub_el = [#xmlel{name = <<"query">>, attrs = [{<<"xmlns">>, ?NS_DISCO_INFO}], - children = iq_disco_info(Lang, Name) ++ AddInfo}]}; + 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) @@ -559,7 +569,8 @@ process_iq(From, deny -> ?DEBUG("Denying HTTP upload slot request from ~s", [jid:to_string(From)]), - IQ#iq{type = error, sub_el = [SubEl, ?ERR_FORBIDDEN]} + Txt = <<"Denied by ACL">>, + IQ#iq{type = error, sub_el = [SubEl, ?ERRT_FORBIDDEN(Lang, Txt)]} end; process_iq(_From, #iq{sub_el = SubEl} = IQ, _State) -> IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; @@ -572,12 +583,12 @@ process_iq(_From, invalid, _State) -> -> {ok, binary(), pos_integer(), binary()} | {error, xmlel()}. parse_request(#xmlel{name = <<"request">>, attrs = Attrs} = Request, Lang) -> - case xml:get_attr(<<"xmlns">>, Attrs) of + case fxml:get_attr(<<"xmlns">>, Attrs) of {value, XMLNS} when XMLNS == ?NS_HTTP_UPLOAD; XMLNS == ?NS_HTTP_UPLOAD_OLD -> - case {xml:get_subtag_cdata(Request, <<"filename">>), - xml:get_subtag_cdata(Request, <<"size">>), - xml:get_subtag_cdata(Request, <<"content-type">>)} of + 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 -> @@ -591,7 +602,8 @@ parse_request(#xmlel{name = <<"request">>, attrs = Attrs} = Request, Lang) -> {error, ?ERRT_BAD_REQUEST(Lang, Text)} end; _ -> - {error, ?ERR_BAD_REQUEST} + Text = <<"No or invalid XML namespace">>, + {error, ?ERRT_BAD_REQUEST(Lang, Text)} end; parse_request(_El, _Lang) -> {error, ?ERR_BAD_REQUEST}. @@ -629,7 +641,7 @@ create_slot(#state{service_url = undefined, end; create_slot(#state{service_url = ServiceURL}, #jid{luser = U, lserver = S} = JID, File, Size, ContentType, - _Lang) -> + Lang) -> Options = [{body_format, binary}, {full_result, false}], HttpOptions = [{timeout, ?SERVICE_REQUEST_TIMEOUT}], SizeStr = jlib:integer_to_binary(Size), @@ -649,7 +661,8 @@ create_slot(#state{service_url = ServiceURL}, Lines -> ?ERROR_MSG("Can't parse data received for ~s from <~s>: ~p", [jid:to_string(JID), ServiceURL, Lines]), - {error, ?ERR_SERVICE_UNAVAILABLE} + Txt = <<"Failed to parse HTTP response">>, + {error, ?ERRT_SERVICE_UNAVAILABLE(Lang, Txt)} end; {ok, {402, _Body}} -> ?INFO_MSG("Got status code 402 for ~s from <~s>", @@ -737,20 +750,41 @@ 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 expand_host(binary(), binary()) -> binary(). - -expand_host(Subject, Host) -> - Parts = binary:split(Subject, <<"@HOST@">>, [global]), - str:join(Parts, Host). - -spec yield_content_type(binary()) -> binary(). yield_content_type(<<"">>) -> ?DEFAULT_CONTENT_TYPE; yield_content_type(Type) -> Type. --spec iq_disco_info(binary(), binary()) -> [xmlel()]. - -iq_disco_info(Lang, Name) -> +-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">>}, @@ -758,7 +792,7 @@ iq_disco_info(Lang, Name) -> #xmlel{name = <<"feature">>, attrs = [{<<"var">>, ?NS_HTTP_UPLOAD}]}, #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, ?NS_HTTP_UPLOAD_OLD}]}]. + attrs = [{<<"var">>, ?NS_HTTP_UPLOAD_OLD}]} | Form]. %% HTTP request handling. @@ -796,7 +830,7 @@ store_file(Path, Data, FileMode, DirMode, GetPrefix, Slot, Thumbnail) -> {ok, [{<<"Content-Type">>, <<"text/xml; charset=utf-8">>}], - xml:element_to_binary(ThumbEl)}; + fxml:element_to_binary(ThumbEl)}; pass -> ok end; @@ -974,8 +1008,9 @@ remove_user(User, Server) -> (node) -> node end, sha1), + DocRoot1 = expand_host(expand_home(DocRoot), ServerHost), UserStr = make_user_string(jid:make(User, Server, <<"">>), JIDinURL), - UserDir = str:join([expand_home(DocRoot), UserStr], <<$/>>), + UserDir = str:join([DocRoot1, UserStr], <<$/>>), case del_tree(UserDir) of ok -> ?INFO_MSG("Removed HTTP upload directory of ~s@~s", [User, Server]); diff --git a/src/mod_http_upload_quota.erl b/src/mod_http_upload_quota.erl index db0b4aa4c..4f9d95217 100644 --- a/src/mod_http_upload_quota.erl +++ b/src/mod_http_upload_quota.erl @@ -132,6 +132,7 @@ init({ServerHost, Opts}) -> 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), @@ -144,7 +145,7 @@ init({ServerHost, Opts}) -> access_soft_quota = AccessSoftQuota, access_hard_quota = AccessHardQuota, max_days = MaxDays, - docroot = DocRoot2, + docroot = DocRoot3, timers = Timers}}. -spec handle_call(_, {pid(), _}, state()) -> {noreply, state()}. @@ -238,7 +239,7 @@ handle_info(Info, State) -> ?ERROR_MSG("Got unexpected info: ~p", [Info]), {noreply, State}. --spec terminate(normal | shutdown | {shutdown, _} | _, _) -> ok. +-spec terminate(normal | shutdown | {shutdown, _} | _, state()) -> ok. terminate(Reason, #state{server_host = ServerHost, timers = Timers}) -> ?DEBUG("Stopping upload quota process for ~s: ~p", [ServerHost, Reason]), diff --git a/src/mod_irc.erl b/src/mod_irc.erl index 183088956..e0c658dea 100644 --- a/src/mod_irc.erl +++ b/src/mod_irc.erl @@ -33,7 +33,8 @@ %% API -export([start_link/2, start/2, stop/1, export/1, import/1, - import/3, closed_connection/3, get_connection_params/3]). + import/3, closed_connection/3, get_connection_params/3, + data_to_binary/2]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3, @@ -46,6 +47,8 @@ -include("adhoc.hrl"). +-include("mod_irc.hrl"). + -define(DEFAULT_IRC_ENCODING, <<"iso8859-15">>). -define(DEFAULT_IRC_PORT, 6667). @@ -58,27 +61,19 @@ [<<"koi8-r">>, <<"iso8859-15">>, <<"iso8859-1">>, <<"iso8859-2">>, <<"utf-8">>, <<"utf-8+latin-1">>]). --type conn_param() :: {binary(), binary(), inet:port_number(), binary()} | - {binary(), binary(), inet:port_number()} | - {binary(), binary()} | - {binary()}. - --record(irc_connection, - {jid_server_host = {#jid{}, <<"">>, <<"">>} :: {jid(), binary(), binary()}, - pid = self() :: pid()}). - --record(irc_custom, - {us_host = {{<<"">>, <<"">>}, <<"">>} :: {{binary(), binary()}, - binary()}, - data = [] :: [{username, binary()} | - {connections_params, [conn_param()]}]}). - -record(state, {host = <<"">> :: binary(), server_host = <<"">> :: binary(), access = all :: atom()}). -define(PROCNAME, ejabberd_mod_irc). +-callback init(binary(), gen_mod:opts()) -> any(). +-callback import(binary(), #irc_custom{}) -> ok | pass. +-callback get_data(binary(), binary(), {binary(), binary()}) -> + error | empty | irc_data(). +-callback set_data(binary(), binary(), {binary(), binary()}, irc_data()) -> + {atomic, any()}. + %%==================================================================== %% API %%==================================================================== @@ -116,24 +111,18 @@ 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 - mnesia -> - mnesia:create_table(irc_custom, - [{disc_copies, [node()]}, - {attributes, record_info(fields, irc_custom)}]), - update_table(); - _ -> ok - end, + Mod = gen_mod:db_mod(Host, Opts, ?MODULE), + Mod:init(Host, Opts), Access = gen_mod:get_opt(access, Opts, fun(A) when is_atom(A) -> A end, all), 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}}. @@ -216,7 +205,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)), @@ -234,7 +223,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, @@ -262,7 +251,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, @@ -304,8 +293,9 @@ do_route1(Host, ServerHost, From, To, Packet) -> Lang)}]}, Res = jlib:iq_to_xml(ResIQ); _ -> - Res = jlib:make_error_reply(Packet, - ?ERR_ITEM_NOT_FOUND) + Txt = <<"Node not found">>, + Res = jlib:make_error_reply( + Packet, ?ERRT_ITEM_NOT_FOUND(Lang, Txt)) end, ejabberd_router:route(To, From, Res); #iq{xmlns = ?NS_REGISTER} = IQ -> @@ -319,7 +309,7 @@ do_route1(Host, ServerHost, From, To, Packet) -> attrs = [{<<"xmlns">>, XMLNS}], children = iq_get_vcard(Lang)}]}, ejabberd_router:route(To, From, jlib:iq_to_xml(Res)); - #iq{type = set, xmlns = ?NS_COMMANDS, lang = _Lang, + #iq{type = set, xmlns = ?NS_COMMANDS, lang = Lang, sub_el = SubEl} = IQ -> Request = adhoc:parse_request(IQ), @@ -348,8 +338,9 @@ do_route1(Host, ServerHost, From, To, Packet) -> true -> ok end; _ -> - Err = jlib:make_error_reply(Packet, - ?ERR_ITEM_NOT_FOUND), + Txt = <<"Node not found">>, + Err = jlib:make_error_reply( + Packet, ?ERRT_ITEM_NOT_FOUND(Lang, Txt)), ejabberd_router:route(To, From, Err) end; #iq{} = _IQ -> @@ -407,12 +398,14 @@ do_route1(Host, ServerHost, From, To, Packet) -> ok end; _ -> + Lang = fxml:get_tag_attr_s(<<"xml:lang">>, Packet), case str:tokens(ChanServ, <<"!">>) of [<<_, _/binary>> = Nick, <<_, _/binary>> = Server] -> case ets:lookup(irc_connection, {From, Server, Host}) of [] -> - Err = jlib:make_error_reply(Packet, - ?ERR_SERVICE_UNAVAILABLE), + Txt = <<"IRC connection not found">>, + Err = jlib:make_error_reply( + Packet, ?ERRT_SERVICE_UNAVAILABLE(Lang, Txt)), ejabberd_router:route(To, From, Err); [R] -> Pid = R#irc_connection.pid, @@ -421,7 +414,9 @@ do_route1(Host, ServerHost, From, To, Packet) -> ok end; _ -> - Err = jlib:make_error_reply(Packet, ?ERR_BAD_REQUEST), + Txt = <<"Failed to parse chanserv">>, + Err = jlib:make_error_reply( + Packet, ?ERRT_BAD_REQUEST(Lang, Txt)), ejabberd_router:route(To, From, Err) end end @@ -516,7 +511,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) @@ -532,10 +527,11 @@ process_irc_register(ServerHost, Host, From, _To, XDataEl = find_xdata_el(SubEl), case XDataEl of false -> + Txt1 = <<"No data form found">>, IQ#iq{type = error, - sub_el = [SubEl, ?ERR_NOT_ACCEPTABLE]}; + sub_el = [SubEl, ?ERRT_NOT_ACCEPTABLE(Lang, Txt1)]}; #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 = @@ -546,10 +542,11 @@ process_irc_register(ServerHost, Host, From, _To, XData = jlib:parse_xdata_submit(XDataEl), case XData of invalid -> + Txt2 = <<"Incorrect data form">>, IQ#iq{type = error, - sub_el = [SubEl, ?ERR_BAD_REQUEST]}; + sub_el = [SubEl, ?ERRT_BAD_REQUEST(Lang, Txt2)]}; _ -> - 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, @@ -567,11 +564,13 @@ process_irc_register(ServerHost, Host, From, _To, end end; _ -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_BAD_REQUEST]} + Txt3 = <<"Incorrect value of 'type' attribute">>, + IQ#iq{type = error, + sub_el = [SubEl, ?ERRT_BAD_REQUEST(Lang, Txt3)]} 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} -> @@ -587,49 +586,16 @@ process_irc_register(ServerHost, Host, From, _To, get_data(ServerHost, Host, From) -> LServer = jid:nameprep(ServerHost), - get_data(LServer, Host, From, - gen_mod:db_type(LServer, ?MODULE)). - -get_data(_LServer, Host, From, mnesia) -> - #jid{luser = LUser, lserver = LServer} = From, - US = {LUser, LServer}, - case catch mnesia:dirty_read({irc_custom, {US, Host}}) - of - {'EXIT', _Reason} -> error; - [] -> empty; - [#irc_custom{data = Data}] -> Data - end; -get_data(LServer, Host, From, riak) -> - #jid{luser = LUser, lserver = LServer} = From, - US = {LUser, LServer}, - case ejabberd_riak:get(irc_custom, irc_custom_schema(), {US, Host}) of - {ok, #irc_custom{data = Data}} -> - Data; - {error, notfound} -> - empty; - _Err -> - error - end; -get_data(LServer, Host, From, odbc) -> - SJID = - 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='">>, - SJID, <<"' and host='">>, SHost, - <<"';">>]) - of - {selected, [<<"data">>], [[SData]]} -> - data_to_binary(From, ejabberd_odbc:decode_term(SData)); - {'EXIT', _} -> error; - {selected, _, _} -> empty - end. + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:get_data(LServer, Host, From). get_form(ServerHost, Host, From, [], Lang) -> #jid{user = User, server = Server} = From, DefaultEncoding = get_default_encoding(Host), Customs = case get_data(ServerHost, Host, From) of - error -> {error, ?ERR_INTERNAL_SERVER_ERROR}; + error -> + Txt1 = <<"Database failure">>, + {error, ?ERRT_INTERNAL_SERVER_ERROR(Lang, Txt1)}; empty -> {User, []}; Data -> get_username_and_connection_params(Data) end, @@ -731,39 +697,10 @@ get_form(_ServerHost, _Host, _, _, _Lang) -> set_data(ServerHost, Host, From, Data) -> 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, _} = jid:tolower(From), - US = {LUser, LServer}, - F = fun () -> - mnesia:write(#irc_custom{us_host = {US, Host}, - data = Data}) - end, - mnesia:transaction(F); -set_data(LServer, Host, From, Data, riak) -> - {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(jid:to_string(jid:tolower(jid:remove_resource(From)))), - SHost = ejabberd_odbc:escape(Host), - SData = ejabberd_odbc:encode_term(Data), - F = fun () -> - odbc_queries:update_t(<<"irc_custom">>, - [<<"jid">>, <<"host">>, <<"data">>], - [SJID, SHost, SData], - [<<"jid='">>, SJID, <<"' and host='">>, - SHost, <<"'">>]), - ok - end, - ejabberd_odbc:sql_transaction(LServer, F). - -set_form(ServerHost, Host, From, [], _Lang, XData) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:set_data(LServer, Host, From, data_to_binary(From, Data)). + +set_form(ServerHost, Host, From, [], Lang, XData) -> case {lists:keysearch(<<"username">>, 1, XData), lists:keysearch(<<"connections_params">>, 1, XData)} of @@ -781,11 +718,11 @@ set_form(ServerHost, Host, From, [], _Lang, XData) -> {connections_params, ConnectionsParams}]) of {atomic, _} -> {result, []}; - _ -> {error, ?ERR_NOT_ACCEPTABLE} + _ -> {error, ?ERRT_NOT_ACCEPTABLE(Lang, <<"Database failure">>)} end; - _ -> {error, ?ERR_NOT_ACCEPTABLE} + _ -> {error, ?ERRT_NOT_ACCEPTABLE(Lang, <<"Parse error">>)} end; - _ -> {error, ?ERR_NOT_ACCEPTABLE} + _ -> {error, ?ERRT_NOT_ACCEPTABLE(Lang, <<"Scan error">>)} end; _ -> {error, ?ERR_NOT_ACCEPTABLE} end; @@ -909,7 +846,9 @@ adhoc_join(From, To, elements = [Form]}); true -> case jlib:parse_xdata_submit(XData) of - invalid -> {error, ?ERR_BAD_REQUEST}; + invalid -> + Txt1 = <<"Incorrect data form">>, + {error, ?ERRT_BAD_REQUEST(Lang, Txt1)}; Fields -> Channel = case lists:keysearch(<<"channel">>, 1, Fields) of @@ -998,7 +937,8 @@ adhoc_register(ServerHost, From, To, true -> case jlib:parse_xdata_submit(XData) of invalid -> - Error = {error, ?ERR_BAD_REQUEST}, + Txt1 = <<"Incorrect data form">>, + Error = {error, ?ERRT_BAD_REQUEST(Lang, Txt1)}, Username = false, ConnectionsParams = false; Fields -> @@ -1021,7 +961,9 @@ adhoc_register(ServerHost, From, To, {atomic, _} -> adhoc:produce_response(Request, #adhoc_response{status = completed}); - _ -> {error, ?ERR_INTERNAL_SERVER_ERROR} + _ -> + Txt2 = <<"Database failure">>, + {error, ?ERRT_INTERNAL_SERVER_ERROR(Lang, Txt2)} end; true -> Form = generate_adhoc_register_form(Lang, Username, @@ -1297,66 +1239,17 @@ conn_params_to_list(Params) -> Port, binary_to_list(P)} end, Params). -irc_custom_schema() -> - {record_info(fields, irc_custom), #irc_custom{}}. - -update_table() -> - Fields = record_info(fields, irc_custom), - case mnesia:table_info(irc_custom, attributes) of - Fields -> - ejabberd_config:convert_table_to_binary( - irc_custom, Fields, set, - fun(#irc_custom{us_host = {_, H}}) -> H end, - fun(#irc_custom{us_host = {{U, S}, H}, - data = Data} = R) -> - JID = jid:make(U, S, <<"">>), - R#irc_custom{us_host = {{iolist_to_binary(U), - iolist_to_binary(S)}, - iolist_to_binary(H)}, - data = data_to_binary(JID, Data)} - end); - _ -> - ?INFO_MSG("Recreating irc_custom table", []), - mnesia:transform_table(irc_custom, ignore, Fields) - end. - -export(_Server) -> - [{irc_custom, - fun(Host, #irc_custom{us_host = {{U, S}, IRCHost}, - data = Data}) -> - case str:suffix(Host, IRCHost) of - true -> - SJID = ejabberd_odbc:escape( - 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, - <<"' and host='">>, SIRCHost, <<"';">>], - [<<"insert into irc_custom(jid, host, " - "data) values ('">>, - SJID, <<"', '">>, SIRCHost, <<"', '">>, SData, - <<"');">>]]; - false -> - [] - end - end}]. +export(LServer) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:export(LServer). -import(_LServer) -> - [{<<"select jid, host, data from irc_custom;">>, - fun([SJID, IRCHost, SData]) -> - #jid{luser = U, lserver = S} = jid:from_string(SJID), - Data = ejabberd_odbc:decode_term(SData), - #irc_custom{us_host = {{U, S}, IRCHost}, - data = Data} - end}]. +import(LServer) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:import(LServer). -import(_LServer, mnesia, #irc_custom{} = R) -> - mnesia:dirty_write(R); -import(_LServer, riak, #irc_custom{} = R) -> - ejabberd_riak:put(R, irc_custom_schema()); -import(_, _, _) -> - pass. +import(LServer, DBType, Data) -> + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:import(LServer, Data). mod_opt_type(access) -> fun (A) when is_atom(A) -> A end; @@ -1368,7 +1261,7 @@ mod_opt_type(_) -> [access, db_type, default_encoding, host]. extract_ident(Packet) -> - case xml:get_subtag(Packet, <<"headers">>) of + case fxml:get_subtag(Packet, <<"headers">>) of {xmlel, _Name, _Attrs, Headers} -> extract_header(<<"X-Irc-Ident">>, Headers); _ -> @@ -1376,7 +1269,7 @@ extract_ident(Packet) -> end. extract_ip_address(Packet) -> - case xml:get_subtag(Packet, <<"headers">>) of + case fxml:get_subtag(Packet, <<"headers">>) of {xmlel, _Name, _Attrs, Headers} -> extract_header(<<"X-Forwarded-For">>, Headers); _ -> @@ -1384,7 +1277,7 @@ extract_ip_address(Packet) -> end. extract_header(HeaderName, [{xmlel, _Name, _Attrs, [{xmlcdata, Value}]} | Tail]) -> - case xml:get_attr(<<"name">>, _Attrs) of + case fxml:get_attr(<<"name">>, _Attrs) of {value, HeaderName} -> binary_to_list(Value); _ -> diff --git a/src/mod_irc_connection.erl b/src/mod_irc_connection.erl index ae709dc27..098c8c286 100644 --- a/src/mod_irc_connection.erl +++ b/src/mod_irc_connection.erl @@ -233,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; @@ -243,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 @@ -261,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", @@ -312,9 +312,9 @@ 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( @@ -325,7 +325,7 @@ handle_info({route_chan, Channel, Resource, 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 @@ -386,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">>); @@ -481,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">>); @@ -778,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 = jid:from_string(xml:get_attr_s(<<"from">>, + From = jid:from_string(fxml:get_attr_s(<<"from">>, Attrs)), - To = jid:from_string(xml:get_attr_s(<<"to">>, + To = jid:from_string(fxml:get_attr_s(<<"to">>, Attrs)), ejabberd_router:route(To, From, Err) end, @@ -1535,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_irc_mnesia.erl b/src/mod_irc_mnesia.erl new file mode 100644 index 000000000..9f8117ad3 --- /dev/null +++ b/src/mod_irc_mnesia.erl @@ -0,0 +1,69 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 14 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_irc_mnesia). + +-behaviour(mod_irc). + +%% API +-export([init/2, get_data/3, set_data/4, import/2]). + +-include("jlib.hrl"). +-include("mod_irc.hrl"). +-include("logger.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + mnesia:create_table(irc_custom, + [{disc_copies, [node()]}, + {attributes, record_info(fields, irc_custom)}]), + update_table(). + +get_data(_LServer, Host, From) -> + {U, S, _} = jid:tolower(From), + case catch mnesia:dirty_read({irc_custom, {{U, S}, Host}}) of + {'EXIT', _Reason} -> error; + [] -> empty; + [#irc_custom{data = Data}] -> Data + end. + +set_data(_LServer, Host, From, Data) -> + {U, S, _} = jid:tolower(From), + F = fun () -> + mnesia:write(#irc_custom{us_host = {{U, S}, Host}, + data = Data}) + end, + mnesia:transaction(F). + +import(_LServer, #irc_custom{} = R) -> + mnesia:dirty_write(R). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +update_table() -> + Fields = record_info(fields, irc_custom), + case mnesia:table_info(irc_custom, attributes) of + Fields -> + ejabberd_config:convert_table_to_binary( + irc_custom, Fields, set, + fun(#irc_custom{us_host = {_, H}}) -> H end, + fun(#irc_custom{us_host = {{U, S}, H}, + data = Data} = R) -> + JID = jid:make(U, S, <<"">>), + R#irc_custom{us_host = {{iolist_to_binary(U), + iolist_to_binary(S)}, + iolist_to_binary(H)}, + data = mod_irc:data_to_binary(JID, Data)} + end); + _ -> + ?INFO_MSG("Recreating irc_custom table", []), + mnesia:transform_table(irc_custom, ignore, Fields) + end. diff --git a/src/mod_irc_riak.erl b/src/mod_irc_riak.erl new file mode 100644 index 000000000..6ac7befdf --- /dev/null +++ b/src/mod_irc_riak.erl @@ -0,0 +1,49 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 14 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_irc_riak). + +-behaviour(mod_irc). + +%% API +-export([init/2, get_data/3, set_data/4, import/2]). + +-include("jlib.hrl"). +-include("mod_irc.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ok. + +get_data(_LServer, Host, From) -> + {U, S, _} = jid:tolower(From), + case ejabberd_riak:get(irc_custom, irc_custom_schema(), {{U, S}, Host}) of + {ok, #irc_custom{data = Data}} -> + Data; + {error, notfound} -> + empty; + _Err -> + error + end. + +set_data(_LServer, Host, From, Data) -> + {U, S, _} = jid:tolower(From), + {atomic, ejabberd_riak:put(#irc_custom{us_host = {{U, S}, Host}, + data = Data}, + irc_custom_schema())}. + +import(_LServer, #irc_custom{} = R) -> + ejabberd_riak:put(R, irc_custom_schema()). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +irc_custom_schema() -> + {record_info(fields, irc_custom), #irc_custom{}}. diff --git a/src/mod_irc_sql.erl b/src/mod_irc_sql.erl new file mode 100644 index 000000000..9a97d5728 --- /dev/null +++ b/src/mod_irc_sql.erl @@ -0,0 +1,91 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 14 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_irc_sql). + +-behaviour(mod_irc). + +%% API +-export([init/2, get_data/3, set_data/4, import/1, import/2, export/1]). + +-include("jlib.hrl"). +-include("mod_irc.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ok. + +get_data(LServer, Host, From) -> + LJID = jid:tolower(jid:remove_resource(From)), + SJID = ejabberd_sql:escape(jid:to_string(LJID)), + SHost = ejabberd_sql:escape(Host), + case catch ejabberd_sql:sql_query( + LServer, + [<<"select data from irc_custom where jid='">>, + SJID, <<"' and host='">>, SHost, + <<"';">>]) of + {selected, [<<"data">>], [[SData]]} -> + mod_irc:data_to_binary(From, ejabberd_sql:decode_term(SData)); + {'EXIT', _} -> error; + {selected, _, _} -> empty + end. + +set_data(LServer, Host, From, Data) -> + LJID = jid:tolower(jid:remove_resource(From)), + SJID = ejabberd_sql:escape(jid:to_string(LJID)), + SHost = ejabberd_sql:escape(Host), + SData = ejabberd_sql:encode_term(Data), + F = fun () -> + sql_queries:update_t(<<"irc_custom">>, + [<<"jid">>, <<"host">>, <<"data">>], + [SJID, SHost, SData], + [<<"jid='">>, SJID, <<"' and host='">>, + SHost, <<"'">>]), + ok + end, + ejabberd_sql:sql_transaction(LServer, F). + +export(_Server) -> + [{irc_custom, + fun(Host, #irc_custom{us_host = {{U, S}, IRCHost}, + data = Data}) -> + case str:suffix(Host, IRCHost) of + true -> + SJID = ejabberd_sql:escape( + jid:to_string( + jid:make(U, S, <<"">>))), + SIRCHost = ejabberd_sql:escape(IRCHost), + SData = ejabberd_sql:encode_term(Data), + [[<<"delete from irc_custom where jid='">>, SJID, + <<"' and host='">>, SIRCHost, <<"';">>], + [<<"insert into irc_custom(jid, host, " + "data) values ('">>, + SJID, <<"', '">>, SIRCHost, <<"', '">>, SData, + <<"');">>]]; + false -> + [] + end + end}]. + +import(_LServer) -> + [{<<"select jid, host, data from irc_custom;">>, + fun([SJID, IRCHost, SData]) -> + #jid{luser = U, lserver = S} = jid:from_string(SJID), + Data = ejabberd_sql:decode_term(SData), + #irc_custom{us_host = {{U, S}, IRCHost}, + data = Data} + end}]. + +import(_, _) -> + pass. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== diff --git a/src/mod_last.erl b/src/mod_last.erl index d76b60491..1af1847b3 100644 --- a/src/mod_last.erl +++ b/src/mod_last.erl @@ -45,23 +45,21 @@ -include("jlib.hrl"). -include("mod_privacy.hrl"). +-include("mod_last.hrl"). --record(last_activity, {us = {<<"">>, <<"">>} :: {binary(), binary()}, - timestamp = 0 :: non_neg_integer(), - status = <<"">> :: binary()}). +-callback init(binary(), gen_mod:opts()) -> any(). +-callback import(binary(), #last_activity{}) -> ok | pass. +-callback get_last(binary(), binary()) -> + {ok, non_neg_integer(), binary()} | not_found | {error, any()}. +-callback store_last_info(binary(), binary(), non_neg_integer(), binary()) -> + {atomic, any()}. +-callback remove_user(binary(), binary()) -> {atomic, any()}. start(Host, Opts) -> IQDisc = gen_mod:get_opt(iqdisc, Opts, fun gen_iq_handler:check_type/1, one_queue), - case gen_mod:db_type(Host, Opts) of - mnesia -> - mnesia:create_table(last_activity, - [{disc_copies, [node()]}, - {attributes, - record_info(fields, last_activity)}]), - update_table(); - _ -> ok - end, + Mod = gen_mod:db_mod(Host, Opts, ?MODULE), + Mod:init(Host, Opts), gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_LAST, ?MODULE, process_local_iq, IQDisc), gen_iq_handler:add_iq_handler(ejabberd_sm, Host, @@ -86,10 +84,11 @@ stop(Host) -> %%% process_local_iq(_From, _To, - #iq{type = Type, sub_el = SubEl} = IQ) -> + #iq{type = Type, lang = Lang, sub_el = SubEl} = IQ) -> case Type of set -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; + Txt = <<"Value 'set' of 'type' attribute is not allowed">>, + IQ#iq{type = error, sub_el = [SubEl, ?ERRT_NOT_ALLOWED(Lang, Txt)]}; get -> Sec = get_node_uptime(), IQ#iq{type = result, @@ -123,10 +122,11 @@ now_to_seconds({MegaSecs, Secs, _MicroSecs}) -> %%% process_sm_iq(From, To, - #iq{type = Type, sub_el = SubEl} = IQ) -> + #iq{type = Type, lang = Lang, sub_el = SubEl} = IQ) -> case Type of set -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; + Txt = <<"Value 'set' of 'type' attribute is not allowed">>, + IQ#iq{type = error, sub_el = [SubEl, ?ERRT_NOT_ALLOWED(Lang, Txt)]}; get -> User = To#jid.luser, Server = To#jid.lserver, @@ -153,62 +153,29 @@ process_sm_iq(From, To, IQ#iq{type = error, sub_el = [SubEl, ?ERR_FORBIDDEN]} end; true -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_FORBIDDEN]} + Txt = <<"Not subscribed">>, + IQ#iq{type = error, sub_el = [SubEl, ?ERRT_FORBIDDEN(Lang, Txt)]} end end. %% @spec (LUser::string(), LServer::string()) -> %% {ok, TimeStamp::integer(), Status::string()} | not_found | {error, Reason} get_last(LUser, LServer) -> - get_last(LUser, LServer, - gen_mod:db_type(LServer, ?MODULE)). + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:get_last(LUser, LServer). -get_last(LUser, LServer, mnesia) -> - case catch mnesia:dirty_read(last_activity, - {LUser, LServer}) - of - {'EXIT', Reason} -> {error, Reason}; - [] -> not_found; - [#last_activity{timestamp = TimeStamp, - status = Status}] -> - {ok, TimeStamp, Status} - end; -get_last(LUser, LServer, riak) -> - case ejabberd_riak:get(last_activity, last_activity_schema(), - {LUser, LServer}) of - {ok, #last_activity{timestamp = TimeStamp, - status = Status}} -> - {ok, TimeStamp, Status}; - {error, notfound} -> - not_found; - Err -> - 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}} - end. - -get_last_iq(IQ, SubEl, LUser, LServer) -> +get_last_iq(#iq{lang = Lang} = IQ, SubEl, LUser, LServer) -> case ejabberd_sm:get_user_resources(LUser, LServer) of [] -> case get_last(LUser, LServer) of {error, _Reason} -> + Txt = <<"Database failure">>, IQ#iq{type = error, - sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]}; + sub_el = [SubEl, ?ERRT_INTERNAL_SERVER_ERROR(Lang, Txt)]}; not_found -> + Txt = <<"No info about last activity found">>, IQ#iq{type = error, - sub_el = [SubEl, ?ERR_SERVICE_UNAVAILABLE]}; + sub_el = [SubEl, ?ERRT_SERVICE_UNAVAILABLE(Lang, Txt)]}; {ok, TimeStamp, Status} -> TimeStamp2 = p1_time_compat:system_time(seconds), Sec = TimeStamp2 - TimeStamp, @@ -238,34 +205,8 @@ on_presence_update(User, Server, _Resource, Status) -> store_last_info(User, Server, TimeStamp, Status) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), - DBType = gen_mod:db_type(LServer, ?MODULE), - store_last_info(LUser, LServer, TimeStamp, Status, - DBType). - -store_last_info(LUser, LServer, TimeStamp, Status, - mnesia) -> - US = {LUser, LServer}, - F = fun () -> - mnesia:write(#last_activity{us = US, - timestamp = TimeStamp, - status = Status}) - end, - mnesia:transaction(F); -store_last_info(LUser, LServer, TimeStamp, Status, - riak) -> - US = {LUser, LServer}, - {atomic, ejabberd_riak:put(#last_activity{us = US, - timestamp = TimeStamp, - status = 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). + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:store_last_info(LUser, LServer, TimeStamp, Status). %% @spec (LUser::string(), LServer::string()) -> %% {ok, TimeStamp::integer(), Status::string()} | not_found @@ -278,72 +219,20 @@ get_last_info(LUser, LServer) -> remove_user(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), - DBType = gen_mod:db_type(LServer, ?MODULE), - remove_user(LUser, LServer, DBType). - -remove_user(LUser, LServer, mnesia) -> - US = {LUser, LServer}, - 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); -remove_user(LUser, LServer, riak) -> - {atomic, ejabberd_riak:delete(last_activity, {LUser, LServer})}. - -update_table() -> - Fields = record_info(fields, last_activity), - case mnesia:table_info(last_activity, attributes) of - Fields -> - ejabberd_config:convert_table_to_binary( - last_activity, Fields, set, - fun(#last_activity{us = {U, _}}) -> U end, - fun(#last_activity{us = {U, S}, status = Status} = R) -> - R#last_activity{us = {iolist_to_binary(U), - iolist_to_binary(S)}, - status = iolist_to_binary(Status)} - end); - _ -> - ?INFO_MSG("Recreating last_activity table", []), - mnesia:transform_table(last_activity, ignore, Fields) - end. - -last_activity_schema() -> - {record_info(fields, last_activity), #last_activity{}}. + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:remove_user(LUser, LServer). -export(_Server) -> - [{last_activity, - fun(Host, #last_activity{us = {LUser, LServer}, - timestamp = TimeStamp, status = Status}) - when LServer == Host -> - Username = ejabberd_odbc:escape(LUser), - Seconds = - ejabberd_odbc:escape(jlib:integer_to_binary(TimeStamp)), - State = ejabberd_odbc:escape(Status), - [[<<"delete from last where username='">>, Username, <<"';">>], - [<<"insert into last(username, seconds, " - "state) values ('">>, - Username, <<"', '">>, Seconds, <<"', '">>, State, - <<"');">>]]; - (_Host, _R) -> - [] - end}]. +export(LServer) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:export(LServer). import(LServer) -> - [{<<"select username, seconds, state from last">>, - fun([LUser, TimeStamp, State]) -> - #last_activity{us = {LUser, LServer}, - timestamp = jlib:binary_to_integer( - TimeStamp), - status = State} - end}]. + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:import(LServer). -import(_LServer, mnesia, #last_activity{} = LA) -> - mnesia:dirty_write(LA); -import(_LServer, riak, #last_activity{} = LA) -> - ejabberd_riak:put(LA, last_activity_schema()); -import(_, _, _) -> - pass. +import(LServer, DBType, LA) -> + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:import(LServer, LA). transform_options(Opts) -> lists:foldl(fun transform_options/2, [], Opts). diff --git a/src/mod_last_mnesia.erl b/src/mod_last_mnesia.erl new file mode 100644 index 000000000..7a1610abb --- /dev/null +++ b/src/mod_last_mnesia.erl @@ -0,0 +1,72 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 13 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_last_mnesia). +-behaviour(mod_last). + +%% API +-export([init/2, import/2, get_last/2, store_last_info/4, remove_user/2]). + +-include("mod_last.hrl"). +-include("logger.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + mnesia:create_table(last_activity, + [{disc_copies, [node()]}, + {attributes, + record_info(fields, last_activity)}]), + update_table(). + +get_last(LUser, LServer) -> + case mnesia:dirty_read(last_activity, {LUser, LServer}) of + [] -> + not_found; + [#last_activity{timestamp = TimeStamp, + status = Status}] -> + {ok, TimeStamp, Status} + end. + +store_last_info(LUser, LServer, TimeStamp, Status) -> + US = {LUser, LServer}, + F = fun () -> + mnesia:write(#last_activity{us = US, + timestamp = TimeStamp, + status = Status}) + end, + mnesia:transaction(F). + +remove_user(LUser, LServer) -> + US = {LUser, LServer}, + F = fun () -> mnesia:delete({last_activity, US}) end, + mnesia:transaction(F). + +import(_LServer, #last_activity{} = LA) -> + mnesia:dirty_write(LA). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +update_table() -> + Fields = record_info(fields, last_activity), + case mnesia:table_info(last_activity, attributes) of + Fields -> + ejabberd_config:convert_table_to_binary( + last_activity, Fields, set, + fun(#last_activity{us = {U, _}}) -> U end, + fun(#last_activity{us = {U, S}, status = Status} = R) -> + R#last_activity{us = {iolist_to_binary(U), + iolist_to_binary(S)}, + status = iolist_to_binary(Status)} + end); + _ -> + ?INFO_MSG("Recreating last_activity table", []), + mnesia:transform_table(last_activity, ignore, Fields) + end. diff --git a/src/mod_last_riak.erl b/src/mod_last_riak.erl new file mode 100644 index 000000000..d25a3a156 --- /dev/null +++ b/src/mod_last_riak.erl @@ -0,0 +1,53 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 13 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_last_riak). +-behaviour(mod_last). + +%% API +-export([init/2, import/2, get_last/2, store_last_info/4, remove_user/2]). + +-include("mod_last.hrl"). +-include("logger.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ok. + +get_last(LUser, LServer) -> + case ejabberd_riak:get(last_activity, last_activity_schema(), + {LUser, LServer}) of + {ok, #last_activity{timestamp = TimeStamp, + status = Status}} -> + {ok, TimeStamp, Status}; + {error, notfound} -> + not_found; + Err -> + Err + end. + +store_last_info(LUser, LServer, TimeStamp, Status) -> + US = {LUser, LServer}, + {atomic, ejabberd_riak:put(#last_activity{us = US, + timestamp = TimeStamp, + status = Status}, + last_activity_schema())}. + +remove_user(LUser, LServer) -> + {atomic, ejabberd_riak:delete(last_activity, {LUser, LServer})}. + +import(_LServer, #last_activity{} = LA) -> + ejabberd_riak:put(LA, last_activity_schema()). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +last_activity_schema() -> + {record_info(fields, last_activity), #last_activity{}}. diff --git a/src/mod_last_sql.erl b/src/mod_last_sql.erl new file mode 100644 index 000000000..5f67b14fc --- /dev/null +++ b/src/mod_last_sql.erl @@ -0,0 +1,75 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 13 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_last_sql). +-behaviour(mod_last). + +%% API +-export([init/2, get_last/2, store_last_info/4, remove_user/2, + import/1, import/2, export/1]). + +-include("mod_last.hrl"). +-include("logger.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ok. + +get_last(LUser, LServer) -> + case catch sql_queries:get_last(LServer, LUser) of + {selected, []} -> + not_found; + {selected, [{TimeStamp, Status}]} -> + {ok, TimeStamp, Status}; + Reason -> + ?ERROR_MSG("failed to get last for user ~s@~s: ~p", + [LUser, LServer, Reason]), + {error, {invalid_result, Reason}} + end. + +store_last_info(LUser, LServer, TimeStamp, Status) -> + sql_queries:set_last_t(LServer, LUser, TimeStamp, Status). + +remove_user(LUser, LServer) -> + sql_queries:del_last(LServer, LUser). + +import(_LServer, _LA) -> + pass. + +export(_Server) -> + [{last_activity, + fun(Host, #last_activity{us = {LUser, LServer}, + timestamp = TimeStamp, status = Status}) + when LServer == Host -> + Username = ejabberd_sql:escape(LUser), + Seconds = + ejabberd_sql:escape(jlib:integer_to_binary(TimeStamp)), + State = ejabberd_sql:escape(Status), + [[<<"delete from last where username='">>, Username, <<"';">>], + [<<"insert into last(username, seconds, " + "state) values ('">>, + Username, <<"', '">>, Seconds, <<"', '">>, State, + <<"');">>]]; + (_Host, _R) -> + [] + end}]. + +import(LServer) -> + [{<<"select username, seconds, state from last">>, + fun([LUser, TimeStamp, State]) -> + #last_activity{us = {LUser, LServer}, + timestamp = jlib:binary_to_integer( + TimeStamp), + status = State} + end}]. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== diff --git a/src/mod_mam.erl b/src/mod_mam.erl index d6e4e9368..f84519a08 100644 --- a/src/mod_mam.erl +++ b/src/mod_mam.erl @@ -35,41 +35,37 @@ -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, + remove_user/2, remove_room/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]). + get_commands_spec/0, msg_to_el/4]). --include_lib("stdlib/include/ms_transform.hrl"). -include("jlib.hrl"). -include("logger.hrl"). -include("mod_muc_room.hrl"). -include("ejabberd_commands.hrl"). +-include("mod_mam.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()]}). +-callback init(binary(), gen_mod:opts()) -> any(). +-callback remove_user(binary(), binary()) -> any(). +-callback remove_room(binary(), binary(), binary()) -> any(). +-callback delete_old_messages(binary() | global, + erlang:timestamp(), + all | chat | groupchat) -> any(). +-callback extended_fields() -> [xmlel()]. +-callback store(xmlel(), binary(), {binary(), binary()}, chat | groupchat, + jid(), binary(), recv | send) -> {ok, binary()} | any(). +-callback write_prefs(binary(), binary(), #archive_prefs{}, binary()) -> ok | any(). +-callback get_prefs(binary(), binary()) -> {ok, #archive_prefs{}} | error. +-callback select(binary(), jid(), jid(), + none | erlang:timestamp(), + none | erlang:timestamp(), + none | ljid() | {text, binary()}, + none | #rsm_in{}, + chat | groupchat) -> + {[{binary(), non_neg_integer(), xmlel()}], boolean(), non_neg_integer()}. %%%=================================================================== %%% API @@ -77,9 +73,9 @@ 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), + Mod = gen_mod:db_mod(Host, Opts, ?MODULE), + Mod:init(Host, Opts), + init_cache(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, @@ -120,18 +116,7 @@ start(Host, Opts) -> 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) -> +init_cache(Opts) -> MaxSize = gen_mod:get_opt(cache_size, Opts, fun(I) when is_integer(I), I>0 -> I end, 1000), @@ -179,24 +164,14 @@ stop(Host) -> remove_user(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), - remove_user(LUser, LServer, - gen_mod:db_type(LServer, ?MODULE)). + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:remove_user(LUser, LServer). -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, <<"';">>]). +remove_room(LServer, Name, Host) -> + LName = jid:nodeprep(Name), + LHost = jid:nameprep(Host), + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:remove_room(LServer, LName, LHost). user_receive_packet(Pkt, C2SState, JID, Peer, To) -> LUser = JID#jid.luser, @@ -287,7 +262,7 @@ muc_process_iq(#iq{type = set, sub_el = #xmlel{name = <<"query">>, attrs = Attrs} = SubEl} = IQ, MUCState, From, To) -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of + 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)); _ -> @@ -297,7 +272,7 @@ muc_process_iq(#iq{type = get, sub_el = #xmlel{name = <<"query">>, attrs = Attrs} = SubEl} = IQ, MUCState, From, To) -> - case xml:get_attr_s(<<"xmlns">>, Attrs) of + 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 -> @@ -310,8 +285,8 @@ muc_process_iq(IQ, _MUCState, _From, _To) -> IQ. get_xdata_fields(SubEl) -> - case {xml:get_subtag_with_xmlns(SubEl, <<"x">>, ?NS_XDATA), - xml:get_subtag_with_xmlns(SubEl, <<"set">>, ?NS_RSM)} of + 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{}} -> @@ -343,10 +318,10 @@ message_is_archived(false, C2SState, Peer, if_enabled -> get_prefs(LUser, LServer); on_request -> - DBType = gen_mod:db_type(LServer, ?MODULE), + Mod = gen_mod:db_mod(LServer, ?MODULE), cache_tab:lookup(archive_prefs, {LUser, LServer}, fun() -> - get_prefs(LUser, LServer, DBType) + Mod:get_prefs(LUser, LServer) end); never -> error @@ -365,21 +340,19 @@ delete_old_messages(TypeBin, Days) when TypeBin == <<"chat">>; 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), + DBTypes = lists:usort( + lists:map( + fun(Host) -> + case gen_mod:db_type(Host, ?MODULE) of + sql -> {sql, Host}; + Other -> {Other, global} + end + end, ?MYHOSTS)), + Results = lists:map( + fun({DBType, ServerHost}) -> + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:delete_old_messages(ServerHost, TimeStamp, Type) + end, DBTypes), case lists:filter(fun(Res) -> Res /= ok end, Results) of [] -> ok; [NotOk|_] -> NotOk @@ -387,27 +360,12 @@ delete_old_messages(TypeBin, Days) when TypeBin == <<"chat">>; 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 xml:get_attr_s(<<"xmlns">>, Attrs) of + NS = case fxml:get_attr_s(<<"xmlns">>, Attrs) of ?NS_MAM_0 -> ?NS_MAM_0; _ -> @@ -427,15 +385,9 @@ process_iq(LServer, #iq{sub_el = #xmlel{attrs = Attrs}} = IQ) -> #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, + Mod = gen_mod:db_mod(LServer, ?MODULE), + ExtendedFields = Mod:extended_fields(), + Fields = ExtendedFields ++ CommonFields, Form = #xmlel{name = <<"x">>, attrs = [{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"form">>}], children = Fields}, @@ -447,8 +399,8 @@ process_iq(LServer, #iq{sub_el = #xmlel{attrs = Attrs}} = IQ) -> % 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 xml:get_tag_attr_s(<<"default">>, SubEl) of + #iq{type = set, lang = Lang, sub_el = #xmlel{name = <<"prefs">>} = SubEl} = IQ) -> + try {case fxml:get_tag_attr_s(<<"default">>, SubEl) of <<"always">> -> always; <<"never">> -> never; <<"roster">> -> roster @@ -461,14 +413,17 @@ process_iq(#jid{luser = LUser, lserver = LServer}, (_, {A, N}) -> {A, N} end, {[], []}, SubEl#xmlel.children)} of - {Default, {Always, Never}} -> - case write_prefs(LUser, LServer, LServer, Default, - lists:usort(Always), lists:usort(Never)) of + {Default, {Always0, Never0}} -> + Always = lists:usort(Always0), + Never = lists:usort(Never0), + case write_prefs(LUser, LServer, LServer, Default, Always, Never) of ok -> - IQ#iq{type = result, sub_el = []}; + NewPrefs = prefs_el(Default, Always, Never, IQ#iq.xmlns), + IQ#iq{type = result, sub_el = [NewPrefs]}; _Err -> + Txt = <<"Database failure">>, IQ#iq{type = error, - sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]} + sub_el = [SubEl, ?ERRT_INTERNAL_SERVER_ERROR(Lang, Txt)]} end catch _:_ -> IQ#iq{type = error, sub_el = [SubEl, ?ERR_BAD_REQUEST]} @@ -477,21 +432,11 @@ process_iq(#jid{luser = LUser, lserver = LServer}, #jid{lserver = LServer}, #iq{type = get, sub_el = #xmlel{name = <<"prefs">>}} = IQ) -> Prefs = get_prefs(LUser, LServer), - Default = jlib:atom_to_binary(Prefs#archive_prefs.default), - JFun = fun(L) -> - [#xmlel{name = <<"jid">>, - children = [{xmlcdata, jid:to_string(J)}]} - || J <- L] - end, - Always = #xmlel{name = <<"always">>, - children = JFun(Prefs#archive_prefs.always)}, - Never = #xmlel{name = <<"never">>, - children = JFun(Prefs#archive_prefs.never)}, - IQ#iq{type = result, - sub_el = [#xmlel{name = <<"prefs">>, - attrs = [{<<"xmlns">>, IQ#iq.xmlns}, - {<<"default">>, Default}], - children = [Always, Never]}]}; + 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]}. @@ -524,16 +469,13 @@ process_iq(LServer, #jid{luser = LUser} = From, To, IQ, SubEl, Fs, MsgType) -> {_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 = xml:get_tag_attr_s(<<"xmlns">>, SubEl), + 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, - #state{config = #config{members_only = MembersOnly}} = MUCState, - From, To, Fs) -> - case not MembersOnly orelse - mod_muc_room:is_occupant_or_admin(From, MUCState) of +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), @@ -548,13 +490,13 @@ muc_process_iq(#iq{lang = Lang, sub_el = SubEl} = IQ, parse_query_v0_2(Query) -> lists:flatmap( fun (#xmlel{name = <<"start">>} = El) -> - [{<<"start">>, [xml:get_tag_cdata(El)]}]; + [{<<"start">>, [fxml:get_tag_cdata(El)]}]; (#xmlel{name = <<"end">>} = El) -> - [{<<"end">>, [xml:get_tag_cdata(El)]}]; + [{<<"end">>, [fxml:get_tag_cdata(El)]}]; (#xmlel{name = <<"with">>} = El) -> - [{<<"with">>, [xml:get_tag_cdata(El)]}]; + [{<<"with">>, [fxml:get_tag_cdata(El)]}]; (#xmlel{name = <<"withtext">>} = El) -> - [{<<"withtext">>, [xml:get_tag_cdata(El)]}]; + [{<<"withtext">>, [fxml:get_tag_cdata(El)]}]; (#xmlel{name = <<"set">>}) -> [{<<"set">>, Query}]; (_) -> @@ -562,7 +504,7 @@ parse_query_v0_2(Query) -> end, Query#xmlel.children). should_archive(#xmlel{name = <<"message">>} = Pkt, LServer) -> - case xml:get_attr_s(<<"type">>, Pkt#xmlel.attrs) of + case fxml:get_attr_s(<<"type">>, Pkt#xmlel.attrs) of <<"error">> -> false; <<"groupchat">> -> @@ -578,7 +520,7 @@ should_archive(#xmlel{name = <<"message">>} = Pkt, LServer) -> no_store -> false; none -> - case xml:get_subtag_cdata(Pkt, <<"body">>) of + case fxml:get_subtag_cdata(Pkt, <<"body">>) of <<>> -> %% Empty body false; @@ -596,7 +538,7 @@ strip_my_archived_tag(Pkt, LServer) -> fun(#xmlel{name = Tag, attrs = Attrs}) when Tag == <<"archived">>; Tag == <<"stanza-id">> -> case catch jid:nameprep( - xml:get_attr_s( + fxml:get_attr_s( <<"by">>, Attrs)) of LServer -> false; @@ -612,9 +554,9 @@ strip_x_jid_tags(Pkt) -> NewEls = lists:filter( fun(#xmlel{name = <<"x">>} = XEl) -> not lists:any(fun(ItemEl) -> - xml:get_tag_attr(<<"jid">>, ItemEl) + fxml:get_tag_attr(<<"jid">>, ItemEl) /= false - end, xml:get_subtags(XEl, <<"item">>)); + end, fxml:get_subtags(XEl, <<"item">>)); (_) -> true end, Pkt#xmlel.children), @@ -650,7 +592,7 @@ should_archive_peer(C2SState, end. should_archive_muc(Pkt) -> - case xml:get_attr_s(<<"type">>, Pkt#xmlel.attrs) of + case fxml:get_attr_s(<<"type">>, Pkt#xmlel.attrs) of <<"groupchat">> -> case check_store_hint(Pkt) of store -> @@ -658,9 +600,9 @@ should_archive_muc(Pkt) -> no_store -> false; none -> - case xml:get_subtag_cdata(Pkt, <<"body">>) of + case fxml:get_subtag_cdata(Pkt, <<"body">>) of <<>> -> - case xml:get_subtag_cdata(Pkt, <<"subject">>) of + case fxml:get_subtag_cdata(Pkt, <<"subject">>) of <<>> -> false; _ -> @@ -688,23 +630,23 @@ check_store_hint(Pkt) -> end. has_store_hint(Message) -> - xml:get_subtag_with_xmlns(Message, <<"store">>, ?NS_HINTS) + fxml:get_subtag_with_xmlns(Message, <<"store">>, ?NS_HINTS) /= false. has_no_store_hint(Message) -> - xml:get_subtag_with_xmlns(Message, <<"no-store">>, ?NS_HINTS) + fxml:get_subtag_with_xmlns(Message, <<"no-store">>, ?NS_HINTS) /= false orelse - xml:get_subtag_with_xmlns(Message, <<"no-storage">>, ?NS_HINTS) + fxml:get_subtag_with_xmlns(Message, <<"no-storage">>, ?NS_HINTS) /= false orelse - xml:get_subtag_with_xmlns(Message, <<"no-permanent-store">>, ?NS_HINTS) + fxml:get_subtag_with_xmlns(Message, <<"no-permanent-store">>, ?NS_HINTS) /= false orelse - xml:get_subtag_with_xmlns(Message, <<"no-permanent-storage">>, ?NS_HINTS) + fxml:get_subtag_with_xmlns(Message, <<"no-permanent-storage">>, ?NS_HINTS) /= false. is_resent(Pkt, LServer) -> - case xml:get_subtag_with_xmlns(Pkt, <<"stanza-id">>, ?NS_SID_0) of + case fxml:get_subtag_with_xmlns(Pkt, <<"stanza-id">>, ?NS_SID_0) of #xmlel{attrs = Attrs} -> - case xml:get_attr(<<"by">>, Attrs) of + case fxml:get_attr(<<"by">>, Attrs) of {value, LServer} -> true; _ -> @@ -714,13 +656,19 @@ is_resent(Pkt, LServer) -> 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)); + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:store(Pkt, LServer, US, chat, Peer, <<"">>, Dir); false -> pass end. @@ -730,101 +678,26 @@ store_muc(MUCState, Pkt, RoomJID, Peer, Nick) -> 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)); + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:store(Pkt, LServer, {U, S}, groupchat, Peer, Nick, recv); 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 = xml:element_to_binary(Pkt), - Body = xml: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}, + Mod = gen_mod:db_mod(Host, ?MODULE), 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. + fun() -> Mod:write_prefs(LUser, LServer, Prefs, Host) end). get_prefs(LUser, LServer) -> - DBType = gen_mod:db_type(LServer, ?MODULE), + Mod = gen_mod:db_mod(LServer, ?MODULE), Res = cache_tab:lookup(archive_prefs, {LUser, LServer}, - fun() -> get_prefs(LUser, LServer, - DBType) - end), + fun() -> Mod:get_prefs(LUser, LServer) end), case Res of {ok, Prefs} -> Prefs; @@ -846,30 +719,21 @@ get_prefs(LUser, LServer) -> 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, @@ -878,11 +742,10 @@ maybe_activate_mam(LUser, LServer) -> false), case ActivateOpt of true -> + Mod = gen_mod:db_mod(LServer, ?MODULE), Res = cache_tab:lookup(archive_prefs, {LUser, LServer}, fun() -> - get_prefs(LUser, LServer, - gen_mod:db_type(LServer, - ?MODULE)) + Mod:get_prefs(LUser, LServer) end), case Res of {ok, _Prefs} -> @@ -900,31 +763,22 @@ maybe_activate_mam(LUser, LServer) -> 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), + With, RSM, MsgType), 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) -> +select_and_start(LServer, From, To, Start, End, With, RSM, MsgType) -> case MsgType of chat -> - select(LServer, From, Start, End, With, RSM, MsgType, DBType); + select(LServer, From, From, Start, End, With, RSM, MsgType); {groupchat, _Role, _MUCState} -> - select(LServer, To, Start, End, With, RSM, MsgType, DBType) + select(LServer, From, To, Start, End, With, RSM, MsgType) end. -select(_LServer, JidRequestor, Start, End, _With, RSM, +select(_LServer, JidRequestor, JidArchive, Start, End, _With, RSM, {groupchat, _Role, #state{config = #config{mam = false}, - history = History}} = MsgType, - _DBType) -> + history = History}} = MsgType) -> #lqueue{len = L, queue = Q} = History, {Msgs0, _} = lists:mapfoldl( @@ -941,8 +795,8 @@ select(_LServer, JidRequestor, Start, End, _With, RSM, peer = undefined, nick = Nick, packet = Pkt}, - MsgType, - JidRequestor)}], I+1}; + MsgType, JidRequestor, JidArchive)}], + I+1}; false -> {[], I+1} end @@ -958,108 +812,49 @@ select(_LServer, JidRequestor, Start, End, _With, RSM, _ -> {Msgs, true, L} end; -select(_LServer, #jid{luser = LUser, lserver = LServer} = JidRequestor, - 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)} - end, FilteredMsgs), IsComplete, Count}; -select(LServer, #jid{luser = LUser} = JidRequestor, - Start, End, With, RSM, MsgType, {odbc, Host}) -> - User = case MsgType of - chat -> LUser; - {groupchat, _Role, _MUCState} -> jid:to_string(JidRequestor) - 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 = xml_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)}] - 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. +select(LServer, From, From, Start, End, With, RSM, MsgType) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:select(LServer, From, From, Start, End, With, RSM, MsgType). msg_to_el(#archive_msg{timestamp = TS, packet = Pkt1, nick = Nick, peer = Peer}, - MsgType, #jid{lserver = LServer} = JidRequestor) -> - Pkt2 = maybe_update_from_to(Pkt1, JidRequestor, Peer, MsgType, Nick), + 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 = [xml:replace_tag_attr( + 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, - Peer, {groupchat, Role, _MUCState}, Nick) -> - Items = case Role of - moderator when Peer /= undefined -> +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(JidRequestor, Nick), Pkt1), + Pkt2 = jlib:replace_from(jid:replace_resource(JidArchive, Nick), Pkt1), jlib:remove_attr(<<"to">>, Pkt2); -maybe_update_from_to(Pkt, _JidRequestor, _Peer, chat, _Nick) -> +maybe_update_from_to(Pkt, _JidRequestor, _JidArchive, _Peer, chat, _Nick) -> Pkt. is_bare_copy(#jid{luser = U, lserver = S, lresource = R}, To) -> @@ -1095,8 +890,8 @@ is_bare_copy(#jid{luser = U, lserver = S, lresource = R}, To) -> end. send(From, To, Msgs, RSM, Count, IsComplete, #iq{sub_el = SubEl} = IQ) -> - QID = xml:get_tag_attr_s(<<"queryid">>, SubEl), - NS = xml:get_tag_attr_s(<<"xmlns">>, SubEl), + QID = fxml:get_tag_attr_s(<<"queryid">>, SubEl), + NS = fxml:get_tag_attr_s(<<"xmlns">>, SubEl), QIDAttr = if QID /= <<>> -> [{<<"queryid">>, QID}]; true -> @@ -1135,7 +930,6 @@ send(From, To, Msgs, RSM, Count, IsComplete, #iq{sub_el = SubEl} = IQ) -> ignore end. - make_rsm_out([], _, Count, Attrs, NS) -> Tag = if NS == ?NS_MAM_TMP -> <<"query">>; true -> <<"fin">> @@ -1152,32 +946,6 @@ make_rsm_out([{FirstID, _, _}|_] = Msgs, _, Count, Attrs, NS) -> #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 -> @@ -1206,118 +974,6 @@ match_rsm(Now, #rsm_in{id = ID, direction = before}) when ID /= <<"">> -> 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. @@ -1336,35 +992,13 @@ datetime_to_now(DateTime, USecs) -> get_jids(Els) -> lists:flatmap( fun(#xmlel{name = <<"jid">>} = El) -> - J = jid:from_string(xml:get_tag_cdata(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", @@ -1383,7 +1017,12 @@ 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(db_type) -> + fun(sql) -> sql; + (odbc) -> sql; + (internal) -> mnesia; + (mnesia) -> mnesia + end; mod_opt_type(default) -> fun (always) -> always; (never) -> never; diff --git a/src/mod_mam_mnesia.erl b/src/mod_mam_mnesia.erl new file mode 100644 index 000000000..007ef5eba --- /dev/null +++ b/src/mod_mam_mnesia.erl @@ -0,0 +1,178 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 15 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_mam_mnesia). + +-behaviour(mod_mam). + +%% API +-export([init/2, remove_user/2, remove_room/3, delete_old_messages/3, + extended_fields/0, store/7, write_prefs/4, get_prefs/2, select/8]). + +-include_lib("stdlib/include/ms_transform.hrl"). +-include("jlib.hrl"). +-include("mod_mam.hrl"). + +-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))). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + 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)}]). + +remove_user(LUser, LServer) -> + US = {LUser, LServer}, + F = fun () -> + mnesia:delete({archive_msg, US}), + mnesia:delete({archive_prefs, US}) + end, + mnesia:transaction(F). + +remove_room(_LServer, LName, LHost) -> + remove_user(LName, LHost). + +delete_old_messages(global, TimeStamp, Type) -> + 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). + +extended_fields() -> + []. + +store(Pkt, _, {LUser, LServer}, Type, Peer, Nick, _Dir) -> + 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. + +write_prefs(_LUser, _LServer, Prefs, _ServerHost) -> + mnesia:dirty_write(Prefs). + +get_prefs(LUser, LServer) -> + case mnesia:dirty_read(archive_prefs, {LUser, LServer}) of + [Prefs] -> + {ok, Prefs}; + _ -> + error + end. + +select(_LServer, JidRequestor, + #jid{luser = LUser, lserver = LServer} = JidArchive, + Start, End, With, RSM, MsgType) -> + 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), + mod_mam:msg_to_el(Msg, MsgType, JidRequestor, JidArchive)} + end, FilteredMsgs), IsComplete, Count}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +now_to_usec({MSec, Sec, USec}) -> + (MSec*1000000 + Sec)*1000000 + USec. + +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). + +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}. diff --git a/src/mod_mam_sql.erl b/src/mod_mam_sql.erl new file mode 100644 index 000000000..69fdf3238 --- /dev/null +++ b/src/mod_mam_sql.erl @@ -0,0 +1,309 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 15 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_mam_sql). + +-behaviour(mod_mam). + +%% API +-export([init/2, remove_user/2, remove_room/3, delete_old_messages/3, + extended_fields/0, store/7, write_prefs/4, get_prefs/2, select/8]). + +-include_lib("stdlib/include/ms_transform.hrl"). +-include("jlib.hrl"). +-include("mod_mam.hrl"). +-include("logger.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ok. + +remove_user(LUser, LServer) -> + SUser = ejabberd_sql:escape(LUser), + ejabberd_sql:sql_query( + LServer, + [<<"delete from archive where username='">>, SUser, <<"';">>]), + ejabberd_sql:sql_query( + LServer, + [<<"delete from archive_prefs where username='">>, SUser, <<"';">>]). + +remove_room(LServer, LName, LHost) -> + LUser = jid:to_string({LName, LHost, <<>>}), + remove_user(LUser, LServer). + +delete_old_messages(ServerHost, TimeStamp, Type) -> + TypeClause = if Type == all -> <<"">>; + true -> [<<" and kind='">>, jlib:atom_to_binary(Type), <<"'">>] + end, + TS = integer_to_binary(now_to_usec(TimeStamp)), + ejabberd_sql:sql_query( + ServerHost, [<<"delete from archive where timestamp<">>, + TS, TypeClause, <<";">>]), + ok. + +extended_fields() -> + [#xmlel{name = <<"field">>, + attrs = [{<<"type">>, <<"text-single">>}, + {<<"var">>, <<"withtext">>}]}]. + +store(Pkt, LServer, {LUser, LHost}, Type, Peer, Nick, _Dir) -> + 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_sql:sql_query( + LServer, + [<<"insert into archive (username, timestamp, " + "peer, bare_peer, xml, txt, kind, nick) values (">>, + <<"'">>, ejabberd_sql:escape(SUser), <<"', ">>, + <<"'">>, TS, <<"', ">>, + <<"'">>, ejabberd_sql:escape(LPeer), <<"', ">>, + <<"'">>, ejabberd_sql:escape(BarePeer), <<"', ">>, + <<"'">>, ejabberd_sql:escape(XML), <<"', ">>, + <<"'">>, ejabberd_sql:escape(Body), <<"', ">>, + <<"'">>, jlib:atom_to_binary(Type), <<"', ">>, + <<"'">>, ejabberd_sql:escape(Nick), <<"');">>]) of + {updated, _} -> + {ok, ID}; + Err -> + Err + end. + +write_prefs(LUser, _LServer, #archive_prefs{default = Default, + never = Never, + always = Always}, + ServerHost) -> + SUser = ejabberd_sql:escape(LUser), + SDefault = erlang:atom_to_binary(Default, utf8), + SAlways = ejabberd_sql:encode_term(Always), + SNever = ejabberd_sql:encode_term(Never), + case update(ServerHost, <<"archive_prefs">>, + [<<"username">>, <<"def">>, <<"always">>, <<"never">>], + [SUser, SDefault, SAlways, SNever], + [<<"username='">>, SUser, <<"'">>]) of + {updated, _} -> + ok; + Err -> + Err + end. + +get_prefs(LUser, LServer) -> + case ejabberd_sql:sql_query( + LServer, + [<<"select def, always, never from archive_prefs ">>, + <<"where username='">>, + ejabberd_sql:escape(LUser), <<"';">>]) of + {selected, _, [[SDefault, SAlways, SNever]]} -> + Default = erlang:binary_to_existing_atom(SDefault, utf8), + Always = ejabberd_sql:decode_term(SAlways), + Never = ejabberd_sql:decode_term(SNever), + {ok, #archive_prefs{us = {LUser, LServer}, + default = Default, + always = Always, + never = Never}}; + _ -> + error + end. + +select(LServer, JidRequestor, #jid{luser = LUser} = JidArchive, + Start, End, With, RSM, MsgType) -> + 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_sql:sql_query(LServer, Query), + ejabberd_sql:sql_query(LServer, 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), + mod_mam: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. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +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}. + +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, + ODBCType = ejabberd_config:get_option( + {sql_type, LServer}, + ejabberd_sql:opt_type(sql_type)), + LimitClause = if is_integer(Max), Max >= 0, ODBCType /= mssql -> + [<<" limit ">>, jlib:integer_to_binary(Max+1)]; + true -> + [] + end, + TopClause = if is_integer(Max), Max >= 0, ODBCType == mssql -> + [<<" TOP ">>, jlib:integer_to_binary(Max+1)]; + true -> + [] + end, + WithClause = case With of + {text, <<>>} -> + []; + {text, Txt} -> + [<<" and match (txt) against ('">>, + ejabberd_sql:escape(Txt), <<"')">>]; + {_, _, <<>>} -> + [<<" and bare_peer='">>, + ejabberd_sql:escape(jid:to_string(With)), + <<"'">>]; + {_, _, _} -> + [<<" and peer='">>, + ejabberd_sql: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_sql:escape(User), + + Query = [<<"SELECT ">>, TopClause, <<" 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, <<";">>]}. + +update(LServer, Table, Fields, Vals, Where) -> + UPairs = lists:zipwith(fun (A, B) -> + <<A/binary, "='", B/binary, "'">> + end, + Fields, Vals), + case ejabberd_sql:sql_query(LServer, + [<<"update ">>, Table, <<" set ">>, + join(UPairs, <<", ">>), <<" where ">>, Where, + <<";">>]) + of + {updated, 1} -> {updated, 1}; + _ -> + ejabberd_sql: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]]. diff --git a/src/mod_metrics.erl b/src/mod_metrics.erl index 3e64da952..c175fcb8e 100644 --- a/src/mod_metrics.erl +++ b/src/mod_metrics.erl @@ -39,7 +39,7 @@ s2s_send_packet, s2s_receive_packet, remove_user, register_user]). --export([start/2, stop/1, send_metrics/4]). +-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, @@ -123,3 +123,6 @@ send_metrics(Host, Probe, Peer, Port) -> 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 000000000..c0835b74e --- /dev/null +++ b/src/mod_mix.erl @@ -0,0 +1,348 @@ +%%%------------------------------------------------------------------- +%%% @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, lang = Lang} = IQ) -> + Txt = <<"Unsupported MIX query">>, + IQ#iq{type = error, sub_el = [SubEl, ?ERRT_BAD_REQUEST(Lang, Txt)]}. + +%%%=================================================================== +%%% 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 a64a00326..6aa186318 100644 --- a/src/mod_muc.erl +++ b/src/mod_muc.erl @@ -48,6 +48,7 @@ export/1, import/1, import/3, + opts_to_binary/1, can_use_nick/4]). -export([init/1, handle_call/3, handle_cast/2, @@ -72,6 +73,17 @@ -define(MAX_ROOMS_DISCOITEMS, 100). +-type muc_room_opts() :: [{atom(), any()}]. +-callback init(binary(), gen_mod:opts()) -> any(). +-callback import(binary(), #muc_room{} | #muc_registered{}) -> ok | pass. +-callback store_room(binary(), binary(), binary(), list()) -> {atomic, any()}. +-callback restore_room(binary(), binary(), binary()) -> muc_room_opts() | error. +-callback forget_room(binary(), binary(), binary()) -> {atomic, any()}. +-callback can_use_nick(binary(), binary(), jid(), binary()) -> boolean(). +-callback get_rooms(binary(), binary()) -> [#muc_room{}]. +-callback get_nick(binary(), binary(), jid()) -> binary() | error. +-callback set_nick(binary(), binary(), jid(), binary()) -> {atomic, ok | false}. + %%==================================================================== %% API %%==================================================================== @@ -125,96 +137,24 @@ create_room(Host, Name, From, Nick, Opts) -> store_room(ServerHost, Host, Name, Opts) -> LServer = jid:nameprep(ServerHost), - store_room(LServer, Host, Name, Opts, - gen_mod:db_type(LServer, ?MODULE)). - -store_room(_LServer, Host, Name, Opts, mnesia) -> - F = fun () -> - mnesia:write(#muc_room{name_host = {Name, Host}, - opts = Opts}) - end, - mnesia:transaction(F); -store_room(_LServer, Host, Name, Opts, riak) -> - {atomic, ejabberd_riak:put(#muc_room{name_host = {Name, Host}, - opts = Opts}, - muc_room_schema())}; -store_room(LServer, Host, Name, Opts, odbc) -> - SName = ejabberd_odbc:escape(Name), - SHost = ejabberd_odbc:escape(Host), - SOpts = ejabberd_odbc:encode_term(Opts), - F = fun () -> - odbc_queries:update_t(<<"muc_room">>, - [<<"name">>, <<"host">>, <<"opts">>], - [SName, SHost, SOpts], - [<<"name='">>, SName, <<"' and host='">>, - SHost, <<"'">>]) - end, - ejabberd_odbc:sql_transaction(LServer, F). + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:store_room(LServer, Host, Name, Opts). restore_room(ServerHost, Host, Name) -> LServer = jid:nameprep(ServerHost), - restore_room(LServer, Host, Name, - gen_mod:db_type(LServer, ?MODULE)). - -restore_room(_LServer, Host, Name, mnesia) -> - case catch mnesia:dirty_read(muc_room, {Name, Host}) of - [#muc_room{opts = Opts}] -> Opts; - _ -> error - end; -restore_room(_LServer, Host, Name, riak) -> - case ejabberd_riak:get(muc_room, muc_room_schema(), {Name, Host}) of - {ok, #muc_room{opts = Opts}} -> Opts; - _ -> error - end; -restore_room(LServer, Host, Name, odbc) -> - SName = ejabberd_odbc:escape(Name), - SHost = ejabberd_odbc:escape(Host), - case catch ejabberd_odbc:sql_query(LServer, - [<<"select opts from muc_room where name='">>, - SName, <<"' and host='">>, SHost, - <<"';">>]) - of - {selected, [<<"opts">>], [[Opts]]} -> - opts_to_binary(ejabberd_odbc:decode_term(Opts)); - _ -> error - end. + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:restore_room(LServer, Host, Name). forget_room(ServerHost, Host, Name) -> LServer = jid:nameprep(ServerHost), - forget_room(LServer, Host, Name, - gen_mod:db_type(LServer, ?MODULE)). - -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) -> - 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 () -> - ejabberd_odbc:sql_query_t([<<"delete from muc_room where name='">>, - SName, <<"' and host='">>, SHost, - <<"';">>]) - end, - ejabberd_odbc:sql_transaction(LServer, F). + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:forget_room(LServer, Host, Name). 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; + mod_mam:remove_room(LServer, Name, Host); false -> ok end. @@ -222,7 +162,7 @@ remove_room_mam(LServer, Host, Name) -> process_iq_disco_items(Host, From, To, #iq{lang = Lang} = IQ) -> Rsm = jlib:rsm_decode(IQ), - DiscoNode = xml:get_tag_attr_s(<<"node">>, IQ#iq.sub_el), + DiscoNode = fxml:get_tag_attr_s(<<"node">>, IQ#iq.sub_el), Res = IQ#iq{type = result, sub_el = [#xmlel{name = <<"query">>, @@ -233,48 +173,8 @@ process_iq_disco_items(Host, From, To, can_use_nick(_ServerHost, _Host, _JID, <<"">>) -> false; can_use_nick(ServerHost, Host, JID, Nick) -> 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, _} = jid:tolower(JID), - LUS = {LUser, LServer}, - case catch mnesia:dirty_select(muc_registered, - [{#muc_registered{us_host = '$1', - nick = Nick, _ = '_'}, - [{'==', {element, 2, '$1'}, Host}], - ['$_']}]) - of - {'EXIT', _Reason} -> true; - [] -> true; - [#muc_registered{us_host = {U, _Host}}] -> U == LUS - end; -can_use_nick(LServer, Host, JID, Nick, riak) -> - {LUser, LServer, _} = jid:tolower(JID), - LUS = {LUser, LServer}, - case ejabberd_riak:get_by_index(muc_registered, - muc_registered_schema(), - <<"nick_host">>, {Nick, Host}) of - {ok, []} -> - true; - {ok, [#muc_registered{us_host = {U, _Host}}]} -> - U == LUS; - {error, _} -> - true - end; -can_use_nick(LServer, Host, JID, Nick, odbc) -> - SJID = - 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, - [<<"select jid from muc_registered ">>, - <<"where nick='">>, SNick, - <<"' and host='">>, SHost, <<"';">>]) - of - {selected, [<<"jid">>], [[SJID1]]} -> SJID == SJID1; - _ -> true - end. + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:can_use_nick(LServer, Host, JID, Nick). %%==================================================================== %% gen_server callbacks @@ -283,21 +183,8 @@ can_use_nick(LServer, Host, JID, Nick, odbc) -> init([Host, Opts]) -> MyHost = gen_mod:get_opt_host(Host, Opts, <<"conference.@HOST@">>), - case gen_mod:db_type(Host, Opts) of - mnesia -> - mnesia:create_table(muc_room, - [{disc_copies, [node()]}, - {attributes, - record_info(fields, muc_room)}]), - mnesia:create_table(muc_registered, - [{disc_copies, [node()]}, - {attributes, - record_info(fields, muc_registered)}]), - update_tables(MyHost), - mnesia:add_table_index(muc_registered, nick); - _ -> - ok - end, + Mod = gen_mod:db_mod(Host, Opts, ?MODULE), + Mod:init(Host, [{host, MyHost}|Opts]), mnesia:create_table(muc_online_room, [{ram_copies, [node()]}, {attributes, record_info(fields, muc_online_room)}]), @@ -373,7 +260,7 @@ init([Host, Opts]) -> RoomShaper = gen_mod:get_opt(room_shaper, Opts, fun(A) when is_atom(A) -> A end, none), - ejabberd_router:register_route(MyHost), + ejabberd_router:register_route(MyHost, Host), load_permanent_rooms(MyHost, Host, {Access, AccessCreate, AccessAdmin, AccessPersistent}, HistorySize, RoomShaper), @@ -451,7 +338,7 @@ do_route(Host, ServerHost, Access, HistorySize, RoomShaper, 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)), @@ -557,18 +444,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">>, @@ -581,7 +468,7 @@ 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; _ -> @@ -593,11 +480,12 @@ do_route1(Host, ServerHost, Access, HistorySize, RoomShaper, _ -> 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, @@ -606,14 +494,14 @@ do_route1(Host, ServerHost, Access, HistorySize, RoomShaper, 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), + 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)), @@ -628,56 +516,26 @@ do_route1(Host, ServerHost, Access, HistorySize, RoomShaper, 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 = jid:nameprep(ServerHost), - get_rooms(LServer, Host, - gen_mod:db_type(LServer, ?MODULE)). - -get_rooms(_LServer, Host, mnesia) -> - case catch mnesia:dirty_select(muc_room, - [{#muc_room{name_host = {'_', Host}, - _ = '_'}, - [], ['$_']}]) - of - {'EXIT', Reason} -> ?ERROR_MSG("~p", [Reason]), []; - Rs -> Rs - end; -get_rooms(_LServer, Host, riak) -> - case ejabberd_riak:get(muc_room, muc_room_schema()) of - {ok, Rs} -> - lists:filter( - fun(#muc_room{name_host = {_, H}}) -> - Host == H - end, Rs); - _Err -> - [] - end; -get_rooms(LServer, Host, odbc) -> - SHost = ejabberd_odbc:escape(Host), - case catch ejabberd_odbc:sql_query(LServer, - [<<"select name, opts from muc_room ">>, - <<"where host='">>, SHost, <<"';">>]) - of - {selected, [<<"name">>, <<"opts">>], RoomOpts} -> - lists:map(fun ([Room, Opts]) -> - #muc_room{name_host = {Room, Host}, - opts = opts_to_binary( - ejabberd_odbc:decode_term(Opts))} - end, - RoomOpts); - Err -> ?ERROR_MSG("failed to get rooms: ~p", [Err]), [] - end. + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:get_rooms(LServer, Host). load_permanent_rooms(Host, ServerHost, Access, HistorySize, RoomShaper) -> @@ -867,41 +725,8 @@ iq_get_unique(From) -> get_nick(ServerHost, Host, From) -> LServer = jid:nameprep(ServerHost), - get_nick(LServer, Host, From, - gen_mod:db_type(LServer, ?MODULE)). - -get_nick(_LServer, Host, From, mnesia) -> - {LUser, LServer, _} = jid:tolower(From), - LUS = {LUser, LServer}, - case catch mnesia:dirty_read(muc_registered, - {LUS, Host}) - of - {'EXIT', _Reason} -> error; - [] -> error; - [#muc_registered{nick = Nick}] -> Nick - end; -get_nick(LServer, Host, From, riak) -> - {LUser, LServer, _} = jid:tolower(From), - US = {LUser, LServer}, - case ejabberd_riak:get(muc_registered, - muc_registered_schema(), - {US, Host}) of - {ok, #muc_registered{nick = Nick}} -> Nick; - {error, _} -> error - end; -get_nick(LServer, Host, From, odbc) -> - SJID = - 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 " - "jid='">>, - SJID, <<"' and host='">>, SHost, - <<"';">>]) - of - {selected, [<<"nick">>], [[Nick]]} -> Nick; - _ -> error - end. + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:get_nick(LServer, Host, From). iq_get_register_info(ServerHost, Host, From, Lang) -> {Nick, Registered} = case get_nick(ServerHost, Host, @@ -940,107 +765,8 @@ iq_get_register_info(ServerHost, Host, From, Lang) -> set_nick(ServerHost, Host, From, Nick) -> 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, _} = jid:tolower(From), - LUS = {LUser, LServer}, - F = fun () -> - case Nick of - <<"">> -> - mnesia:delete({muc_registered, {LUS, Host}}), ok; - _ -> - Allow = case mnesia:select(muc_registered, - [{#muc_registered{us_host = - '$1', - nick = Nick, - _ = '_'}, - [{'==', {element, 2, '$1'}, - Host}], - ['$_']}]) - of - [] -> true; - [#muc_registered{us_host = {U, _Host}}] -> - U == LUS - end, - if Allow -> - mnesia:write(#muc_registered{us_host = {LUS, Host}, - nick = Nick}), - ok; - true -> false - end - end - end, - mnesia:transaction(F); -set_nick(LServer, Host, From, Nick, riak) -> - {LUser, LServer, _} = jid:tolower(From), - LUS = {LUser, LServer}, - {atomic, - case Nick of - <<"">> -> - ejabberd_riak:delete(muc_registered, {LUS, Host}); - _ -> - Allow = case ejabberd_riak:get_by_index( - muc_registered, - muc_registered_schema(), - <<"nick_host">>, {Nick, Host}) of - {ok, []} -> - true; - {ok, [#muc_registered{us_host = {U, _Host}}]} -> - U == LUS; - {error, _} -> - false - end, - if Allow -> - ejabberd_riak:put(#muc_registered{us_host = {LUS, Host}, - nick = Nick}, - muc_registered_schema(), - [{'2i', [{<<"nick_host">>, - {Nick, Host}}]}]); - true -> - false - end - end}; -set_nick(LServer, Host, From, Nick, odbc) -> - JID = - jid:to_string(jid:tolower(jid:remove_resource(From))), - SJID = ejabberd_odbc:escape(JID), - SNick = ejabberd_odbc:escape(Nick), - SHost = ejabberd_odbc:escape(Host), - F = fun () -> - case Nick of - <<"">> -> - ejabberd_odbc:sql_query_t([<<"delete from muc_registered where ">>, - <<"jid='">>, SJID, - <<"' and host='">>, Host, - <<"';">>]), - ok; - _ -> - Allow = case - ejabberd_odbc:sql_query_t([<<"select jid from muc_registered ">>, - <<"where nick='">>, - SNick, - <<"' and host='">>, - SHost, <<"';">>]) - of - {selected, [<<"jid">>], [[J]]} -> J == JID; - _ -> true - end, - if Allow -> - odbc_queries:update_t(<<"muc_registered">>, - [<<"jid">>, <<"host">>, - <<"nick">>], - [SJID, SHost, SNick], - [<<"jid='">>, SJID, - <<"' and host='">>, SHost, - <<"'">>]), - ok; - true -> false - end - end - end, - ejabberd_odbc:sql_transaction(LServer, F). + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:set_nick(LServer, Host, From, Nick). iq_set_register_info(ServerHost, Host, From, Nick, Lang) -> @@ -1050,24 +776,28 @@ iq_set_register_info(ServerHost, Host, From, Nick, ErrText = <<"That nickname is registered by another " "person">>, {error, ?ERRT_CONFLICT(Lang, ErrText)}; - _ -> {error, ?ERR_INTERNAL_SERVER_ERROR} + _ -> + Txt = <<"Database failure">>, + {error, ?ERRT_INTERNAL_SERVER_ERROR(Lang, Txt)} end. 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">>} -> XData = jlib:parse_xdata_submit(XEl), case XData of - invalid -> {error, ?ERR_BAD_REQUEST}; + invalid -> + Txt = <<"Incorrect data form">>, + {error, ?ERRT_BAD_REQUEST(Lang, Txt)}; _ -> case lists:keysearch(<<"nick">>, 1, XData) of {value, {_, [Nick]}} when Nick /= <<"">> -> @@ -1182,118 +912,17 @@ opts_to_binary(Opts) -> Opt end, Opts). -update_tables(Host) -> - update_muc_room_table(Host), - update_muc_registered_table(Host). - -muc_room_schema() -> - {record_info(fields, muc_room), #muc_room{}}. - -muc_registered_schema() -> - {record_info(fields, muc_registered), #muc_registered{}}. - -update_muc_room_table(_Host) -> - Fields = record_info(fields, muc_room), - case mnesia:table_info(muc_room, attributes) of - Fields -> - ejabberd_config:convert_table_to_binary( - muc_room, Fields, set, - fun(#muc_room{name_host = {N, _}}) -> N end, - fun(#muc_room{name_host = {N, H}, - opts = Opts} = R) -> - R#muc_room{name_host = {iolist_to_binary(N), - iolist_to_binary(H)}, - opts = opts_to_binary(Opts)} - end); - _ -> - ?INFO_MSG("Recreating muc_room table", []), - mnesia:transform_table(muc_room, ignore, Fields) - end. +export(LServer) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:export(LServer). -update_muc_registered_table(_Host) -> - Fields = record_info(fields, muc_registered), - case mnesia:table_info(muc_registered, attributes) of - Fields -> - ejabberd_config:convert_table_to_binary( - muc_registered, Fields, set, - fun(#muc_registered{us_host = {_, H}}) -> H end, - fun(#muc_registered{us_host = {{U, S}, H}, - nick = Nick} = R) -> - R#muc_registered{us_host = {{iolist_to_binary(U), - iolist_to_binary(S)}, - iolist_to_binary(H)}, - nick = iolist_to_binary(Nick)} - end); - _ -> - ?INFO_MSG("Recreating muc_registered table", []), - mnesia:transform_table(muc_registered, ignore, Fields) - end. +import(LServer) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:import(LServer). -export(_Server) -> - [{muc_room, - fun(Host, #muc_room{name_host = {Name, RoomHost}, opts = Opts}) -> - case str:suffix(Host, RoomHost) of - true -> - SName = ejabberd_odbc:escape(Name), - SRoomHost = ejabberd_odbc:escape(RoomHost), - SOpts = ejabberd_odbc:encode_term(Opts), - [[<<"delete from muc_room where name='">>, SName, - <<"' and host='">>, SRoomHost, <<"';">>], - [<<"insert into muc_room(name, host, opts) ", - "values (">>, - <<"'">>, SName, <<"', '">>, SRoomHost, - <<"', '">>, SOpts, <<"');">>]]; - false -> - [] - end - end}, - {muc_registered, - fun(Host, #muc_registered{us_host = {{U, S}, RoomHost}, - nick = Nick}) -> - case str:suffix(Host, RoomHost) of - true -> - SJID = ejabberd_odbc:escape( - jid:to_string( - jid:make(U, S, <<"">>))), - SNick = ejabberd_odbc:escape(Nick), - SRoomHost = ejabberd_odbc:escape(RoomHost), - [[<<"delete from muc_registered where jid='">>, - SJID, <<"' and host='">>, SRoomHost, <<"';">>], - [<<"insert into muc_registered(jid, host, " - "nick) values ('">>, - SJID, <<"', '">>, SRoomHost, <<"', '">>, SNick, - <<"');">>]]; - false -> - [] - end - end}]. - -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} - end}, - {<<"select jid, host, nick from muc_registered;">>, - fun([J, RoomHost, Nick]) -> - #jid{user = U, server = S} = - jid:from_string(J), - #muc_registered{us_host = {{U, S}, RoomHost}, - nick = Nick} - end}]. - -import(_LServer, mnesia, #muc_room{} = R) -> - mnesia:dirty_write(R); -import(_LServer, mnesia, #muc_registered{} = R) -> - mnesia:dirty_write(R); -import(_LServer, riak, #muc_room{} = R) -> - ejabberd_riak:put(R, muc_room_schema()); -import(_LServer, riak, - #muc_registered{us_host = {_, Host}, nick = Nick} = R) -> - ejabberd_riak:put(R, muc_registered_schema(), - [{'2i', [{<<"nick_host">>, {Nick, Host}}]}]); -import(_, _, _) -> - pass. +import(LServer, DBType, Data) -> + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:import(LServer, Data). mod_opt_type(access) -> fun (A) when is_atom(A) -> A end; @@ -1317,6 +946,8 @@ 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 @@ -1342,7 +973,7 @@ mod_opt_type(user_presence_shaper) -> 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, + 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, diff --git a/src/mod_muc_admin.erl b/src/mod_muc_admin.erl index c08757375..5fbda4f28 100644 --- a/src/mod_muc_admin.erl +++ b/src/mod_muc_admin.erl @@ -178,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]; @@ -279,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)}} @@ -395,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, @@ -471,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), @@ -499,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, "."), - {list_to_binary(Name), list_to_binary(Host), list_to_binary(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]). - + [Name, Host] = binary:split(RoomJID, <<"@">>), + [_MUC_service_name, ServerHost] = binary:split(Host, <<".">>), + {Name, Host, ServerHost}. %%---------------------------- %% 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), @@ -692,29 +688,32 @@ 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, jid:from_string(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 = [jid:from_string(JidString) || {JidString, _Nick, _} <- OccupantsTuples], - lists:filter( - fun(UserString) -> - UserJid = jid:from_string(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 @@ -825,8 +824,12 @@ get_room_options(Pid) -> get_options(Config). get_options(Config) -> - Fields = record_info(fields, config), - [config | Values] = tuple_to_list(Config), + Fields = [jlib:atom_to_binary(Field) || Field <- record_info(fields, config)], + [config | ValuesRaw] = tuple_to_list(Config), + Values = lists:map(fun(V) when is_atom(V) -> jlib:atom_to_binary(V); + (V) when is_integer(V) -> jlib:integer_to_binary(V); + (V) when is_tuple(V); is_list(V) -> list_to_binary(hd(io_lib:format("~w", [V]))); + (V) -> V end, ValuesRaw), lists:zip(Fields, Values). %%---------------------------- @@ -882,12 +885,19 @@ make_opts(StateData) -> Config = StateData#state.config, [ {title, Config#config.title}, + {vcard, Config#config.vcard}, + {voice_request_min_interval, Config#config.voice_request_min_interval}, {allow_change_subj, Config#config.allow_change_subj}, {allow_query_users, Config#config.allow_query_users}, {allow_private_messages, Config#config.allow_private_messages}, + {allow_private_messages_from_visitors, Config#config.allow_private_messages_from_visitors}, + {allow_visitor_status, Config#config.allow_visitor_status}, + {allow_visitor_nickchange, Config#config.allow_visitor_nickchange}, + {allow_voice_requests, Config#config.allow_voice_requests}, {public, Config#config.public}, {public_list, Config#config.public_list}, {persistent, Config#config.persistent}, + {mam, Config#config.mam}, {moderated, Config#config.moderated}, {members_by_default, Config#config.members_by_default}, {members_only, Config#config.members_only}, @@ -895,6 +905,8 @@ make_opts(StateData) -> {password_protected, Config#config.password_protected}, {password, Config#config.password}, {anonymous, Config#config.anonymous}, + {captcha_protected, Config#config.captcha_protected}, + {description, Config#config.description}, {logging, Config#config.logging}, {max_users, Config#config.max_users}, {affiliations, ?DICT:to_list(StateData#state.affiliations)}, diff --git a/src/mod_muc_log.erl b/src/mod_muc_log.erl index 4d8e3965c..1f32f6f9b 100644 --- a/src/mod_muc_log.erl +++ b/src/mod_muc_log.erl @@ -193,15 +193,15 @@ code_change(_OldVsn, State, _Extra) -> {ok, State}. add_to_log2(text, {Nick, Packet}, Room, Opts, State) -> case has_no_permanent_store_hint(Packet) of false -> - case {xml:get_subtag(Packet, <<"subject">>), - xml:get_subtag(Packet, <<"body">>)} + case {fxml:get_subtag(Packet, <<"subject">>), + fxml:get_subtag(Packet, <<"body">>)} of {false, false} -> ok; {false, SubEl} -> - Message = {body, xml:get_tag_cdata(SubEl)}, + Message = {body, fxml:get_tag_cdata(SubEl)}, add_message_to_log(Nick, Message, Room, Opts, State); {SubEl, _} -> - Message = {subject, xml:get_tag_cdata(SubEl)}, + Message = {subject, fxml:get_tag_cdata(SubEl)}, add_message_to_log(Nick, Message, Room, Opts, State) end; true -> ok @@ -1201,13 +1201,13 @@ fjoin(FileList) -> list_to_binary(filename:join([binary_to_list(File) || File <- FileList])). has_no_permanent_store_hint(Packet) -> - xml:get_subtag_with_xmlns(Packet, <<"no-store">>, ?NS_HINTS) + fxml:get_subtag_with_xmlns(Packet, <<"no-store">>, ?NS_HINTS) =/= false orelse - xml:get_subtag_with_xmlns(Packet, <<"no-storage">>, ?NS_HINTS) + fxml:get_subtag_with_xmlns(Packet, <<"no-storage">>, ?NS_HINTS) =/= false orelse - xml:get_subtag_with_xmlns(Packet, <<"no-permanent-store">>, ?NS_HINTS) + fxml:get_subtag_with_xmlns(Packet, <<"no-permanent-store">>, ?NS_HINTS) =/= false orelse - xml:get_subtag_with_xmlns(Packet, <<"no-permanent-storage">>, ?NS_HINTS) + fxml:get_subtag_with_xmlns(Packet, <<"no-permanent-storage">>, ?NS_HINTS) =/= false. mod_opt_type(access_log) -> diff --git a/src/mod_muc_mnesia.erl b/src/mod_muc_mnesia.erl new file mode 100644 index 000000000..e3ae36978 --- /dev/null +++ b/src/mod_muc_mnesia.erl @@ -0,0 +1,163 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 13 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_muc_mnesia). + +-behaviour(mod_muc). + +%% API +-export([init/2, import/2, store_room/4, restore_room/3, forget_room/3, + can_use_nick/4, get_rooms/2, get_nick/3, set_nick/4]). + +-include("mod_muc.hrl"). +-include("logger.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, Opts) -> + MyHost = proplists:get_value(host, Opts), + mnesia:create_table(muc_room, + [{disc_copies, [node()]}, + {attributes, + record_info(fields, muc_room)}]), + mnesia:create_table(muc_registered, + [{disc_copies, [node()]}, + {attributes, + record_info(fields, muc_registered)}]), + update_tables(MyHost), + mnesia:add_table_index(muc_registered, nick). + +store_room(_LServer, Host, Name, Opts) -> + F = fun () -> + mnesia:write(#muc_room{name_host = {Name, Host}, + opts = Opts}) + end, + mnesia:transaction(F). + +restore_room(_LServer, Host, Name) -> + case catch mnesia:dirty_read(muc_room, {Name, Host}) of + [#muc_room{opts = Opts}] -> Opts; + _ -> error + end. + +forget_room(_LServer, Host, Name) -> + F = fun () -> mnesia:delete({muc_room, {Name, Host}}) + end, + mnesia:transaction(F). + +can_use_nick(_LServer, Host, JID, Nick) -> + {LUser, LServer, _} = jid:tolower(JID), + LUS = {LUser, LServer}, + case catch mnesia:dirty_select(muc_registered, + [{#muc_registered{us_host = '$1', + nick = Nick, _ = '_'}, + [{'==', {element, 2, '$1'}, Host}], + ['$_']}]) + of + {'EXIT', _Reason} -> true; + [] -> true; + [#muc_registered{us_host = {U, _Host}}] -> U == LUS + end. + +get_rooms(_LServer, Host) -> + mnesia:dirty_select(muc_room, + [{#muc_room{name_host = {'_', Host}, + _ = '_'}, + [], ['$_']}]). + +get_nick(_LServer, Host, From) -> + {LUser, LServer, _} = jid:tolower(From), + LUS = {LUser, LServer}, + case mnesia:dirty_read(muc_registered, {LUS, Host}) of + [] -> error; + [#muc_registered{nick = Nick}] -> Nick + end. + +set_nick(_LServer, Host, From, Nick) -> + {LUser, LServer, _} = jid:tolower(From), + LUS = {LUser, LServer}, + F = fun () -> + case Nick of + <<"">> -> + mnesia:delete({muc_registered, {LUS, Host}}), + ok; + _ -> + Allow = case mnesia:select( + muc_registered, + [{#muc_registered{us_host = + '$1', + nick = Nick, + _ = '_'}, + [{'==', {element, 2, '$1'}, + Host}], + ['$_']}]) of + [] -> true; + [#muc_registered{us_host = {U, _Host}}] -> + U == LUS + end, + if Allow -> + mnesia:write(#muc_registered{ + us_host = {LUS, Host}, + nick = Nick}), + ok; + true -> + false + end + end + end, + mnesia:transaction(F). + +import(_LServer, #muc_room{} = R) -> + mnesia:dirty_write(R); +import(_LServer, #muc_registered{} = R) -> + mnesia:dirty_write(R). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +update_tables(Host) -> + update_muc_room_table(Host), + update_muc_registered_table(Host). + +update_muc_room_table(_Host) -> + Fields = record_info(fields, muc_room), + case mnesia:table_info(muc_room, attributes) of + Fields -> + ejabberd_config:convert_table_to_binary( + muc_room, Fields, set, + fun(#muc_room{name_host = {N, _}}) -> N end, + fun(#muc_room{name_host = {N, H}, + opts = Opts} = R) -> + R#muc_room{name_host = {iolist_to_binary(N), + iolist_to_binary(H)}, + opts = mod_muc:opts_to_binary(Opts)} + end); + _ -> + ?INFO_MSG("Recreating muc_room table", []), + mnesia:transform_table(muc_room, ignore, Fields) + end. + +update_muc_registered_table(_Host) -> + Fields = record_info(fields, muc_registered), + case mnesia:table_info(muc_registered, attributes) of + Fields -> + ejabberd_config:convert_table_to_binary( + muc_registered, Fields, set, + fun(#muc_registered{us_host = {_, H}}) -> H end, + fun(#muc_registered{us_host = {{U, S}, H}, + nick = Nick} = R) -> + R#muc_registered{us_host = {{iolist_to_binary(U), + iolist_to_binary(S)}, + iolist_to_binary(H)}, + nick = iolist_to_binary(Nick)} + end); + _ -> + ?INFO_MSG("Recreating muc_registered table", []), + mnesia:transform_table(muc_registered, ignore, Fields) + end. diff --git a/src/mod_muc_riak.erl b/src/mod_muc_riak.erl new file mode 100644 index 000000000..bc6e5959a --- /dev/null +++ b/src/mod_muc_riak.erl @@ -0,0 +1,117 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 13 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_muc_riak). + +-behaviour(mod_muc). + +%% API +-export([init/2, import/2, store_room/4, restore_room/3, forget_room/3, + can_use_nick/4, get_rooms/2, get_nick/3, set_nick/4]). + +-include("mod_muc.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ok. + +store_room(_LServer, Host, Name, Opts) -> + {atomic, ejabberd_riak:put(#muc_room{name_host = {Name, Host}, + opts = Opts}, + muc_room_schema())}. + +restore_room(_LServer, Host, Name) -> + case ejabberd_riak:get(muc_room, muc_room_schema(), {Name, Host}) of + {ok, #muc_room{opts = Opts}} -> Opts; + _ -> error + end. + +forget_room(_LServer, Host, Name) -> + {atomic, ejabberd_riak:delete(muc_room, {Name, Host})}. + +can_use_nick(LServer, Host, JID, Nick) -> + {LUser, LServer, _} = jid:tolower(JID), + LUS = {LUser, LServer}, + case ejabberd_riak:get_by_index(muc_registered, + muc_registered_schema(), + <<"nick_host">>, {Nick, Host}) of + {ok, []} -> + true; + {ok, [#muc_registered{us_host = {U, _Host}}]} -> + U == LUS; + {error, _} -> + true + end. + +get_rooms(_LServer, Host) -> + case ejabberd_riak:get(muc_room, muc_room_schema()) of + {ok, Rs} -> + lists:filter( + fun(#muc_room{name_host = {_, H}}) -> + Host == H + end, Rs); + _Err -> + [] + end. + +get_nick(LServer, Host, From) -> + {LUser, LServer, _} = jid:tolower(From), + US = {LUser, LServer}, + case ejabberd_riak:get(muc_registered, + muc_registered_schema(), + {US, Host}) of + {ok, #muc_registered{nick = Nick}} -> Nick; + {error, _} -> error + end. + +set_nick(LServer, Host, From, Nick) -> + {LUser, LServer, _} = jid:tolower(From), + LUS = {LUser, LServer}, + {atomic, + case Nick of + <<"">> -> + ejabberd_riak:delete(muc_registered, {LUS, Host}); + _ -> + Allow = case ejabberd_riak:get_by_index( + muc_registered, + muc_registered_schema(), + <<"nick_host">>, {Nick, Host}) of + {ok, []} -> + true; + {ok, [#muc_registered{us_host = {U, _Host}}]} -> + U == LUS; + {error, _} -> + false + end, + if Allow -> + ejabberd_riak:put(#muc_registered{us_host = {LUS, Host}, + nick = Nick}, + muc_registered_schema(), + [{'2i', [{<<"nick_host">>, + {Nick, Host}}]}]); + true -> + false + end + end}. + +import(_LServer, #muc_room{} = R) -> + ejabberd_riak:put(R, muc_room_schema()); +import(_LServer, #muc_registered{us_host = {_, Host}, nick = Nick} = R) -> + ejabberd_riak:put(R, muc_registered_schema(), + [{'2i', [{<<"nick_host">>, {Nick, Host}}]}]). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +muc_room_schema() -> + {record_info(fields, muc_room), #muc_room{}}. + +muc_registered_schema() -> + {record_info(fields, muc_registered), #muc_registered{}}. diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl index d19bc5f3d..c9c785757 100644 --- a/src/mod_muc_room.erl +++ b/src/mod_muc_room.erl @@ -143,12 +143,12 @@ normal_state({route, From, <<"">>, 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 = p1_time_compat:system_time(micro_seconds), @@ -252,7 +252,7 @@ normal_state({route, From, <<"">>, IsVoiceApprovement = is_voice_approvement(Els) and not is_visitor(From, StateData), if IsInvitation -> - case catch check_invitation(From, Els, Lang, StateData) + case catch check_invitation(From, Packet, Lang, StateData) of {error, Error} -> Err = jlib:make_error_reply(Packet, Error), @@ -394,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, @@ -432,10 +432,12 @@ normal_state({route, From, <<"">>, ?NS_MUC_OWNER -> process_iq_owner(From, Type, Lang, SubEl, StateData); ?NS_DISCO_INFO -> - case xml:get_attr(<<"node">>, Attrs) of - false -> process_iq_disco_info(From, Type, Lang, StateData); - {value, _} -> {error, ?ERR_SERVICE_UNAVAILABLE} - end; + case fxml:get_attr(<<"node">>, Attrs) of + false -> process_iq_disco_info(From, Type, Lang, StateData); + {value, _} -> + Txt = <<"Disco info is not available for this node">>, + {error, ?ERRT_SERVICE_UNAVAILABLE(Lang, Txt)} + end; ?NS_DISCO_ITEMS -> process_iq_disco_items(From, Type, Lang, StateData); ?NS_VCARD -> @@ -509,8 +511,8 @@ 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} -> @@ -568,7 +570,7 @@ normal_state({route, From, ToNick, 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 -> @@ -607,8 +609,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 @@ -817,8 +819,9 @@ handle_info({captcha_failed, From}, normal_state, of {ok, {Nick, Packet}} -> Robots = (?DICT):erase(From, StateData#state.robots), - Err = jlib:make_error_reply(Packet, - ?ERR_NOT_AUTHORIZED), + Txt = <<"The CAPTCHA verification has failed">>, + Err = jlib:make_error_reply( + Packet, ?ERRT_NOT_AUTHORIZED(?MYLANG, Txt)), ejabberd_router:route % TODO: s/Nick/""/ (jid:replace_resource(StateData#state.jid, Nick), @@ -884,7 +887,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 @@ -936,7 +939,7 @@ process_groupchat_message(From, drop -> {next_state, normal_state, StateData}; NewPacket1 -> - NewPacket = xml:remove_subtags(NewPacket1, <<"nick">>, {<<"xmlns">>, ?NS_NICK}), + NewPacket = fxml:remove_subtags(NewPacket1, <<"nick">>, {<<"xmlns">>, ?NS_NICK}), send_multiple(jid:replace_resource(StateData#state.jid, FromNick), StateData#state.server_host, @@ -1016,7 +1019,7 @@ get_participant_data(From, StateData) -> process_presence(From, Nick, #xmlel{name = <<"presence">>, attrs = Attrs0} = Packet0, StateData) -> - Type0 = xml:get_attr_s(<<"type">>, Attrs0), + Type0 = fxml:get_attr_s(<<"type">>, Attrs0), IsOnline = is_user_online(From, StateData), if Type0 == <<"">>; IsOnline and ((Type0 == <<"unavailable">>) or (Type0 == <<"error">>)) -> @@ -1029,8 +1032,8 @@ process_presence(From, Nick, drop -> {next_state, normal_state, StateData}; #xmlel{attrs = Attrs} = Packet -> - 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), StateData1 = case Type of <<"unavailable">> -> NewPacket = case @@ -1047,12 +1050,12 @@ process_presence(From, Nick, {ok, [_, _ | _]} -> ok; _ -> send_new_presence(From, NewState, StateData) end, - Reason = case xml:get_subtag(NewPacket, + Reason = case fxml:get_subtag(NewPacket, <<"status">>) of false -> <<"">>; Status_el -> - xml:get_tag_cdata(Status_el) + fxml:get_tag_cdata(Status_el) end, remove_online_user(From, NewState, Reason); <<"error">> -> @@ -1087,7 +1090,7 @@ process_presence(From, Nick, From, Err), StateData; {true, _, _} -> - Lang = xml:get_attr_s(<<"xml:lang">>, + Lang = fxml:get_attr_s(<<"xml:lang">>, Attrs), ErrText = <<"That nickname is already in use by another " @@ -1304,7 +1307,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, @@ -1655,7 +1658,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), @@ -1743,11 +1746,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 @@ -1781,7 +1784,7 @@ nick_collision(User, Nick, StateData) -> 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,6 +1810,22 @@ add_new_user(From, Nick, StateData#state.host, From, Nick), get_default_role(Affiliation, StateData)} of + {false, _, _, _} when NUsers >= MaxUsers orelse NUsers >= MaxAdminUsers -> + Txt = <<"Too many users in this conference">>, + Err = jlib:make_error_reply(Packet, + ?ERRT_RESOURCE_CONSTRAINT(Lang, Txt)), + ejabberd_router:route % TODO: s/Nick/""/ + (jid:replace_resource(StateData#state.jid, Nick), + From, Err), + StateData; + {false, _, _, _} when NConferences >= MaxConferences -> + Txt = <<"You have joined too many conferences">>, + Err = jlib:make_error_reply(Packet, + ?ERRT_RESOURCE_CONSTRAINT(Lang, Txt)), + ejabberd_router:route % TODO: s/Nick/""/ + (jid:replace_resource(StateData#state.jid, Nick), + From, Err), + StateData; {false, _, _, _} -> Err = jlib:make_error_reply(Packet, ?ERR_SERVICE_UNAVAILABLE), @@ -1856,7 +1875,7 @@ add_new_user(From, Nick, add_online_user(From, Nick, Role, StateData)), send_existing_presences(From, NewState), - send_new_presence(From, NewState, StateData), + send_initial_presence(From, NewState, StateData), Shift = count_stanza_shift(Nick, Els, NewState), case send_history(From, Shift, NewState) of true -> ok; @@ -1879,7 +1898,7 @@ add_new_user(From, 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 = jid:replace_resource(RoomJID, Nick), Limiter = {From#jid.luser, From#jid.lserver}, @@ -1979,11 +1998,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; @@ -2057,9 +2076,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">> -> @@ -2090,6 +2109,9 @@ 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, StateData, OldStateData) -> send_update_presence(JID, <<"">>, StateData, OldStateData). @@ -2117,20 +2139,25 @@ send_update_presence1(JID, Reason, StateData, OldStateData) -> end end, lists:foreach(fun (J) -> - send_new_presence(J, Reason, StateData, OldStateData) + send_new_presence1(J, Reason, false, StateData, + OldStateData) end, LJIDs). send_new_presence(NJID, StateData, OldStateData) -> - send_new_presence(NJID, <<"">>, StateData, OldStateData). + send_new_presence(NJID, <<"">>, false, StateData, OldStateData). 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, StateData, OldStateData) + false -> send_new_presence1(NJID, Reason, IsInitialPresence, StateData, + OldStateData) end. -send_new_presence1(NJID, Reason, StateData, OldStateData) -> +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), @@ -2187,46 +2214,9 @@ send_new_presence1(NJID, Reason, StateData, OldStateData) -> 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, - Status4 = case (StateData#state.config)#config.logging of - true -> - [#xmlel{name = <<"status">>, - attrs = - [{<<"code">>, <<"170">>}], - children = []} - | Status3]; - false -> Status3 - end, - Packet = xml:append_subtags(Presence, + StatusEls = status_els(IsInitialPresence, NJID, Info, + StateData), + Packet = fxml:append_subtags(Presence, [#xmlel{name = <<"x">>, attrs = [{<<"xmlns">>, @@ -2240,7 +2230,7 @@ send_new_presence1(NJID, Reason, StateData, OldStateData) -> children = ItemEls} - | Status4]}]), + | StatusEls]}]), ejabberd_router:route(jid:replace_resource(StateData#state.jid, Nick), Info#user.jid, Packet) @@ -2289,7 +2279,7 @@ send_existing_presences1(ToJID, StateData) -> {<<"role">>, role_to_list(FromRole)}] end, - Packet = xml:append_subtags(Presence, + Packet = fxml:append_subtags(Presence, [#xmlel{name = <<"x">>, attrs = @@ -2420,7 +2410,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">>, @@ -2450,6 +2440,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}. @@ -2474,7 +2498,7 @@ 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 @@ -2491,7 +2515,7 @@ add_message_to_history(FromNick, FromJID, Packet, StateData) -> 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 = @@ -2530,9 +2554,9 @@ send_subject(JID, #state{subject_author = Nick} = StateData) -> 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) -> @@ -2549,22 +2573,27 @@ 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 - false -> {error, ?ERR_BAD_REQUEST}; + case fxml:get_subtag(SubEl, <<"item">>) of + false -> + Txt = <<"No 'item' element found">>, + {error, ?ERRT_BAD_REQUEST(Lang, Txt)}; 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 - false -> {error, ?ERR_BAD_REQUEST}; + case fxml:get_tag_attr(<<"affiliation">>, Item) of + false -> + Txt = <<"No 'affiliation' attribute found">>, + {error, ?ERRT_BAD_REQUEST(Lang, Txt)}; {value, StrAffiliation} -> case catch list_to_affiliation(StrAffiliation) of {'EXIT', _} -> {error, ?ERR_BAD_REQUEST}; SAffiliation -> if (FAffiliation == owner) or (FAffiliation == admin) or - ((FAffiliation == member) and (SAffiliation == member)) -> + ((FAffiliation == member) and not + (StateData#state.config)#config.anonymous) -> Items = items_with_affiliation(SAffiliation, StateData), {result, Items, StateData}; @@ -2577,7 +2606,9 @@ process_iq_admin(From, get, Lang, SubEl, StateData) -> end; {value, StrRole} -> case catch list_to_role(StrRole) of - {'EXIT', _} -> {error, ?ERR_BAD_REQUEST}; + {'EXIT', _} -> + Txt = <<"Incorrect value of 'role' attribute">>, + {error, ?ERRT_BAD_REQUEST(Lang, Txt)}; SRole -> if FRole == moderator -> Items = items_with_role(SRole, StateData), @@ -2748,7 +2779,7 @@ 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 jid:from_string(S) of error -> @@ -2761,7 +2792,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 -> @@ -2774,17 +2805,21 @@ find_changed_items(UJID, UAffiliation, URole, {error, ?ERRT_NOT_ACCEPTABLE(Lang, ErrText)}; J -> {value, J} end; - _ -> {error, ?ERR_BAD_REQUEST} + _ -> + Txt1 = <<"No 'nick' attribute found">>, + {error, ?ERRT_BAD_REQUEST(Lang, Txt1)} end end, case TJID of {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 - false -> {error, ?ERR_BAD_REQUEST}; + case fxml:get_attr(<<"affiliation">>, Attrs) of + false -> + Txt2 = <<"No 'affiliation' attribute found">>, + {error, ?ERRT_BAD_REQUEST(Lang, Txt2)}; {value, StrAffiliation} -> case catch list_to_affiliation(StrAffiliation) of {'EXIT', _} -> @@ -2824,7 +2859,7 @@ 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 = [{jid:remove_resource(Jidx), @@ -2833,7 +2868,9 @@ find_changed_items(UJID, UAffiliation, URole, find_changed_items(UJID, UAffiliation, URole, Items, Lang, StateData, [MoreRes | Res]); - false -> {error, ?ERR_NOT_ALLOWED} + false -> + Txt3 = <<"Changing role/affiliation is not allowed">>, + {error, ?ERRT_NOT_ALLOWED(Lang, Txt3)} end end end; @@ -2871,7 +2908,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} @@ -2879,7 +2916,9 @@ find_changed_items(UJID, UAffiliation, URole, find_changed_items(UJID, UAffiliation, URole, Items, Lang, StateData, [MoreRes | Res]); - _ -> {error, ?ERR_NOT_ALLOWED} + _ -> + Txt4 = <<"Changing role/affiliation is not allowed">>, + {error, ?ERRT_NOT_ALLOWED(Lang, Txt4)} end end end; @@ -3123,10 +3162,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">>} -> @@ -3137,10 +3176,12 @@ process_iq_owner(From, set, Lang, SubEl, StateData) -> andalso is_password_settings_correct(XEl, StateData) of - true -> set_config(XEl, StateData); + true -> set_config(XEl, StateData, Lang); false -> {error, ?ERR_NOT_ACCEPTABLE} end; - _ -> {error, ?ERR_BAD_REQUEST} + _ -> + Txt = <<"Incorrect data form">>, + {error, ?ERRT_BAD_REQUEST(Lang, Txt)} end; [#xmlel{name = <<"destroy">>} = SubEl1] -> ?INFO_MSG("Destroyed MUC room ~s by the owner ~s", @@ -3160,11 +3201,13 @@ 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 - false -> {error, ?ERR_BAD_REQUEST}; + case fxml:get_tag_attr(<<"affiliation">>, Item) of + false -> + Txt = <<"No 'affiliation' attribute found">>, + {error, ?ERRT_BAD_REQUEST(Lang, Txt)}; {value, StrAffiliation} -> case catch list_to_affiliation(StrAffiliation) of {'EXIT', _} -> @@ -3636,10 +3679,10 @@ get_config(Lang, StateData, From) -> children = Res}], StateData}. -set_config(XEl, StateData) -> +set_config(XEl, StateData, Lang) -> XData = jlib:parse_xdata_submit(XEl), case XData of - invalid -> {error, ?ERR_BAD_REQUEST}; + invalid -> {error, ?ERRT_BAD_REQUEST(Lang, <<"Incorrect data form">>)}; _ -> case set_xoption(XData, StateData#state.config) of #config{} = Config -> @@ -3669,14 +3712,20 @@ set_config(XEl, StateData) -> <<"1">> -> set_xoption(Opts, Config#config{Opt = true}); <<"true">> -> set_xoption(Opts, Config#config{Opt = true}); - _ -> {error, ?ERR_BAD_REQUEST} + _ -> + Txt = <<"Value of '~s' should be boolean">>, + ErrTxt = iolist_to_binary(io_lib:format(Txt, [Opt])), + {error, ?ERRT_BAD_REQUEST(?MYLANG, ErrTxt)} end). -define(SET_NAT_XOPT(Opt, Val), case catch jlib:binary_to_integer(Val) of I when is_integer(I), I > 0 -> set_xoption(Opts, Config#config{Opt = I}); - _ -> {error, ?ERR_BAD_REQUEST} + _ -> + Txt = <<"Value of '~s' should be integer">>, + ErrTxt = iolist_to_binary(io_lib:format(Txt, [Opt])), + {error, ?ERRT_BAD_REQUEST(?MYLANG, ErrTxt)} end). -define(SET_STRING_XOPT(Opt, Val), @@ -3729,7 +3778,10 @@ set_xoption([{<<"allow_private_messages_from_visitors">>, <<"nobody">> -> ?SET_STRING_XOPT(allow_private_messages_from_visitors, nobody); - _ -> {error, ?ERR_BAD_REQUEST} + _ -> + Txt = <<"Value of 'allow_private_messages_from_visitors' " + "should be anyone|moderators|nobody">>, + {error, ?ERRT_BAD_REQUEST(?MYLANG, Txt)} end; set_xoption([{<<"muc#roomconfig_allowvisitorstatus">>, [Val]} @@ -3797,7 +3849,10 @@ set_xoption([{<<"muc#roomconfig_presencebroadcast">>, Vals} | Opts], end end, {false, false, false}, Vals), case Roles of - error -> {error, ?ERR_BAD_REQUEST}; + error -> + Txt = <<"Value of 'muc#roomconfig_presencebroadcast' should " + "be moderator|participant|visitor">>, + {error, ?ERRT_BAD_REQUEST(?MYLANG, Txt)}; {M, P, V} -> Res = if M -> [moderator]; true -> [] end ++ @@ -3825,7 +3880,10 @@ set_xoption([{<<"muc#roomconfig_whois">>, [Val]} <<"anyone">> -> ?SET_BOOL_XOPT(anonymous, (iolist_to_binary(integer_to_list(0)))); - _ -> {error, ?ERR_BAD_REQUEST} + _ -> + Txt = <<"Value of 'muc#roomconfig_whois' should be " + "moderators|anyone">>, + {error, ?ERRT_BAD_REQUEST(?MYLANG, Txt)} end; set_xoption([{<<"muc#roomconfig_maxusers">>, [Val]} | Opts], @@ -3848,8 +3906,10 @@ set_xoption([{<<"muc#roomconfig_captcha_whitelist">>, ?SET_JIDMULTI_XOPT(captcha_whitelist, JIDs); set_xoption([{<<"FORM_TYPE">>, _} | Opts], Config) -> set_xoption(Opts, Config); -set_xoption([_ | _Opts], _Config) -> - {error, ?ERR_BAD_REQUEST}. +set_xoption([{Opt, _Vals} | _Opts], _Config) -> + Txt = <<"Unknown option '~s'">>, + ErrTxt = iolist_to_binary(io_lib:format(Txt, [Opt])), + {error, ?ERRT_BAD_REQUEST(?MYLANG, ErrTxt)}. change_config(Config, StateData) -> send_config_change_info(Config, StateData), @@ -4122,8 +4182,9 @@ destroy_room(DEl, StateData) -> false -> ?FEATURE(Fiffalse) end). -process_iq_disco_info(_From, set, _Lang, _StateData) -> - {error, ?ERR_NOT_ALLOWED}; +process_iq_disco_info(_From, set, Lang, _StateData) -> + Txt = <<"Value 'set' of 'type' attribute is not allowed">>, + {error, ?ERRT_NOT_ALLOWED(Lang, Txt)}; process_iq_disco_info(_From, get, Lang, StateData) -> Config = StateData#state.config, {result, @@ -4193,9 +4254,10 @@ iq_disco_info_extras(Lang, StateData) -> <<"muc#roominfo_occupants">>, (iolist_to_binary(integer_to_list(Len))))]}]. -process_iq_disco_items(_From, set, _Lang, _StateData) -> - {error, ?ERR_NOT_ALLOWED}; -process_iq_disco_items(From, get, _Lang, StateData) -> +process_iq_disco_items(_From, set, Lang, _StateData) -> + Txt = <<"Value 'set' of 'type' attribute is not allowed">>, + {error, ?ERRT_NOT_ALLOWED(Lang, Txt)}; +process_iq_disco_items(From, get, Lang, StateData) -> case (StateData#state.config)#config.public_list of true -> {result, get_mucroom_disco_items(StateData), StateData}; @@ -4203,23 +4265,31 @@ process_iq_disco_items(From, get, _Lang, StateData) -> case is_occupant_or_admin(From, StateData) of true -> {result, get_mucroom_disco_items(StateData), StateData}; - _ -> {error, ?ERR_FORBIDDEN} + _ -> + Txt = <<"Only occupants or administrators can perform this query">>, + {error, ?ERRT_FORBIDDEN(Lang, Txt)} end end. -process_iq_captcha(_From, get, _Lang, _SubEl, +process_iq_captcha(_From, get, Lang, _SubEl, _StateData) -> - {error, ?ERR_NOT_ALLOWED}; -process_iq_captcha(_From, set, _Lang, SubEl, + Txt = <<"Value 'get' of 'type' attribute is not allowed">>, + {error, ?ERRT_NOT_ALLOWED(Lang, Txt)}; +process_iq_captcha(_From, set, Lang, SubEl, StateData) -> case ejabberd_captcha:process_reply(SubEl) of ok -> {result, [], StateData}; - _ -> {error, ?ERR_NOT_ACCEPTABLE} + {error, malformed} -> + Txt = <<"Incorrect CAPTCHA submit">>, + {error, ?ERRT_BAD_REQUEST(Lang, Txt)}; + _ -> + Txt = <<"The CAPTCHA verification has failed">>, + {error, ?ERRT_NOT_ALLOWED(Lang, Txt)} end. 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, _} -> @@ -4228,7 +4298,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); @@ -4287,7 +4357,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 -> @@ -4370,7 +4440,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 -> @@ -4424,9 +4494,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; @@ -4436,37 +4506,42 @@ is_invitation(Els) -> end, false, Els). -check_invitation(From, Els, Lang, StateData) -> +check_invitation(From, Packet, Lang, StateData) -> FAffiliation = get_affiliation(From, StateData), CanInvite = (StateData#state.config)#config.allow_user_invites orelse FAffiliation == admin orelse FAffiliation == owner, - InviteEl = case xml:remove_cdata(Els) of - [#xmlel{name = <<"x">>, children = Els1} = XEl] -> - case xml:get_tag_attr_s(<<"xmlns">>, XEl) of - ?NS_MUC_USER -> ok; - _ -> throw({error, ?ERR_BAD_REQUEST}) - end, - case xml:remove_cdata(Els1) of - [#xmlel{name = <<"invite">>} = InviteEl1] -> InviteEl1; - _ -> throw({error, ?ERR_BAD_REQUEST}) - end; - _ -> throw({error, ?ERR_BAD_REQUEST}) + InviteEl = case fxml:get_subtag_with_xmlns(Packet, <<"x">>, ?NS_MUC_USER) of + false -> + Txt1 = <<"No 'x' element found">>, + throw({error, ?ERRT_BAD_REQUEST(Lang, Txt1)}); + XEl -> + case fxml:get_subtag(XEl, <<"invite">>) of + false -> + Txt2 = <<"No 'invite' element found">>, + throw({error, ?ERRT_BAD_REQUEST(Lang, Txt2)}); + InviteEl1 -> + InviteEl1 + end end, JID = case - jid:from_string(xml:get_tag_attr_s(<<"to">>, + jid:from_string(fxml:get_tag_attr_s(<<"to">>, InviteEl)) of - error -> throw({error, ?ERR_JID_MALFORMED}); + error -> + Txt = <<"Incorrect value of 'to' attribute">>, + throw({error, ?ERRT_JID_MALFORMED(Lang, Txt)}); JID1 -> JID1 end, case CanInvite of - false -> throw({error, ?ERR_NOT_ALLOWED}); + false -> + Txt3 = <<"Invitations are not allowed in this conference">>, + throw({error, ?ERRT_NOT_ALLOWED(Lang, Txt3)}); 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 <<>> -> []; @@ -4556,10 +4631,10 @@ handle_roommessage_from_nonparticipant(Packet, Lang, %% 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), + 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}}. @@ -4646,7 +4721,7 @@ tab_count_user(JID) -> end. element_size(El) -> - byte_size(xml:element_to_binary(El)). + byte_size(fxml:element_to_binary(El)). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Multicast diff --git a/src/mod_muc_sql.erl b/src/mod_muc_sql.erl new file mode 100644 index 000000000..55628d43e --- /dev/null +++ b/src/mod_muc_sql.erl @@ -0,0 +1,202 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 13 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_muc_sql). + +-behaviour(mod_muc). + +%% API +-export([init/2, store_room/4, restore_room/3, forget_room/3, + can_use_nick/4, get_rooms/2, get_nick/3, set_nick/4, + import/1, import/2, export/1]). + +-include("jlib.hrl"). +-include("mod_muc.hrl"). +-include("logger.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ok. + +store_room(LServer, Host, Name, Opts) -> + SName = ejabberd_sql:escape(Name), + SHost = ejabberd_sql:escape(Host), + SOpts = ejabberd_sql:encode_term(Opts), + F = fun () -> + sql_queries:update_t(<<"muc_room">>, + [<<"name">>, <<"host">>, <<"opts">>], + [SName, SHost, SOpts], + [<<"name='">>, SName, <<"' and host='">>, + SHost, <<"'">>]) + end, + ejabberd_sql:sql_transaction(LServer, F). + +restore_room(LServer, Host, Name) -> + SName = ejabberd_sql:escape(Name), + SHost = ejabberd_sql:escape(Host), + case catch ejabberd_sql:sql_query(LServer, + [<<"select opts from muc_room where name='">>, + SName, <<"' and host='">>, SHost, + <<"';">>]) of + {selected, [<<"opts">>], [[Opts]]} -> + mod_muc:opts_to_binary(ejabberd_sql:decode_term(Opts)); + _ -> + error + end. + +forget_room(LServer, Host, Name) -> + SName = ejabberd_sql:escape(Name), + SHost = ejabberd_sql:escape(Host), + F = fun () -> + ejabberd_sql:sql_query_t([<<"delete from muc_room where name='">>, + SName, <<"' and host='">>, SHost, + <<"';">>]) + end, + ejabberd_sql:sql_transaction(LServer, F). + +can_use_nick(LServer, Host, JID, Nick) -> + SJID = jid:to_string(jid:tolower(jid:remove_resource(JID))), + SNick = ejabberd_sql:escape(Nick), + SHost = ejabberd_sql:escape(Host), + case catch ejabberd_sql:sql_query(LServer, + [<<"select jid from muc_registered ">>, + <<"where nick='">>, SNick, + <<"' and host='">>, SHost, <<"';">>]) of + {selected, [<<"jid">>], [[SJID1]]} -> SJID == SJID1; + _ -> true + end. + +get_rooms(LServer, Host) -> + SHost = ejabberd_sql:escape(Host), + case catch ejabberd_sql:sql_query(LServer, + [<<"select name, opts from muc_room ">>, + <<"where host='">>, SHost, <<"';">>]) of + {selected, [<<"name">>, <<"opts">>], RoomOpts} -> + lists:map( + fun([Room, Opts]) -> + #muc_room{name_host = {Room, Host}, + opts = mod_muc:opts_to_binary( + ejabberd_sql:decode_term(Opts))} + end, RoomOpts); + Err -> + ?ERROR_MSG("failed to get rooms: ~p", [Err]), + [] + end. + +get_nick(LServer, Host, From) -> + SJID = ejabberd_sql:escape(jid:to_string(jid:tolower(jid:remove_resource(From)))), + SHost = ejabberd_sql:escape(Host), + case catch ejabberd_sql:sql_query(LServer, + [<<"select nick from muc_registered where " + "jid='">>, + SJID, <<"' and host='">>, SHost, + <<"';">>]) of + {selected, [<<"nick">>], [[Nick]]} -> Nick; + _ -> error + end. + +set_nick(LServer, Host, From, Nick) -> + JID = jid:to_string(jid:tolower(jid:remove_resource(From))), + SJID = ejabberd_sql:escape(JID), + SNick = ejabberd_sql:escape(Nick), + SHost = ejabberd_sql:escape(Host), + F = fun () -> + case Nick of + <<"">> -> + ejabberd_sql:sql_query_t( + [<<"delete from muc_registered where ">>, + <<"jid='">>, SJID, + <<"' and host='">>, Host, + <<"';">>]), + ok; + _ -> + Allow = case ejabberd_sql:sql_query_t( + [<<"select jid from muc_registered ">>, + <<"where nick='">>, + SNick, + <<"' and host='">>, + SHost, <<"';">>]) of + {selected, [<<"jid">>], [[J]]} -> J == JID; + _ -> true + end, + if Allow -> + sql_queries:update_t(<<"muc_registered">>, + [<<"jid">>, <<"host">>, + <<"nick">>], + [SJID, SHost, SNick], + [<<"jid='">>, SJID, + <<"' and host='">>, SHost, + <<"'">>]), + ok; + true -> + false + end + end + end, + ejabberd_sql:sql_transaction(LServer, F). + +export(_Server) -> + [{muc_room, + fun(Host, #muc_room{name_host = {Name, RoomHost}, opts = Opts}) -> + case str:suffix(Host, RoomHost) of + true -> + SName = ejabberd_sql:escape(Name), + SRoomHost = ejabberd_sql:escape(RoomHost), + SOpts = ejabberd_sql:encode_term(Opts), + [[<<"delete from muc_room where name='">>, SName, + <<"' and host='">>, SRoomHost, <<"';">>], + [<<"insert into muc_room(name, host, opts) ", + "values (">>, + <<"'">>, SName, <<"', '">>, SRoomHost, + <<"', '">>, SOpts, <<"');">>]]; + false -> + [] + end + end}, + {muc_registered, + fun(Host, #muc_registered{us_host = {{U, S}, RoomHost}, + nick = Nick}) -> + case str:suffix(Host, RoomHost) of + true -> + SJID = ejabberd_sql:escape( + jid:to_string( + jid:make(U, S, <<"">>))), + SNick = ejabberd_sql:escape(Nick), + SRoomHost = ejabberd_sql:escape(RoomHost), + [[<<"delete from muc_registered where jid='">>, + SJID, <<"' and host='">>, SRoomHost, <<"';">>], + [<<"insert into muc_registered(jid, host, " + "nick) values ('">>, + SJID, <<"', '">>, SRoomHost, <<"', '">>, SNick, + <<"');">>]]; + false -> + [] + end + end}]. + +import(_LServer) -> + [{<<"select name, host, opts from muc_room;">>, + fun([Name, RoomHost, SOpts]) -> + Opts = mod_muc:opts_to_binary(ejabberd_sql:decode_term(SOpts)), + #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} = jid:from_string(J), + #muc_registered{us_host = {{U, S}, RoomHost}, + nick = Nick} + end}]. + +import(_, _) -> + pass. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== diff --git a/src/mod_multicast.erl b/src/mod_multicast.erl index 96ebcb6f2..83520c0be 100644 --- a/src/mod_multicast.erl +++ b/src/mod_multicast.erl @@ -151,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}}. @@ -233,7 +233,7 @@ handle_iq(From, To, #xmlel{attrs = Attrs} = Packet, State) -> ejabberd_router:route(To, From, Err); reply -> LServiceS = jts(To), - case xml:get_attr_s(<<"type">>, Attrs) of + case fxml:get_attr_s(<<"type">>, Attrs) of <<"result">> -> process_iqreply_result(From, LServiceS, Packet, State); <<"error">> -> @@ -395,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}) -> @@ -436,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, xml:remove_cdata(Addresses)}; + {ok, Packet_stripped, AAttrs, fxml:remove_cdata(Addresses)}; _ -> throw(ewxmlns) end; _ -> throw(eadsele) @@ -460,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; @@ -499,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} @@ -525,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: ", @@ -634,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) -> @@ -666,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]). @@ -734,8 +736,8 @@ process_iqreply_error(From, LServiceS, _Packet) -> 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 -> @@ -763,7 +765,7 @@ process_discoinfo_result2(From, FromS, LServiceS, Els, fun(XML) -> case XML of #xmlel{name = <<"feature">>, attrs = Attrs} -> - (?NS_ADDRESS) == xml:get_attr_s(<<"var">>, Attrs); + (?NS_ADDRESS) == fxml:get_attr_s(<<"var">>, Attrs); _ -> false end end, @@ -807,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 @@ -826,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 @@ -848,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]; @@ -870,7 +872,7 @@ process_discoitems_result(From, LServiceS, Els) -> fun(XML, Res) -> case XML of #xmlel{name = <<"item">>, attrs = Attrs} -> - SJID = xml:get_attr_s(<<"jid">>, Attrs), + SJID = fxml:get_attr_s(<<"jid">>, Attrs), case jid:from_string(SJID) of #jid{luser = <<"">>, lresource = <<"">>} -> @@ -912,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), @@ -1196,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). diff --git a/src/mod_offline.erl b/src/mod_offline.erl index 1b1627e87..356d89a67 100644 --- a/src/mod_offline.erl +++ b/src/mod_offline.erl @@ -27,6 +27,7 @@ -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'}). @@ -37,15 +38,18 @@ -behaviour(gen_mod). --export([count_offline_messages/2]). - -export([start/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, @@ -53,7 +57,9 @@ import/3, export/1, get_queue_length/2, + count_offline_messages/2, get_offline_els/2, + find_x_expire/2, webadmin_page/3, webadmin_user/4, webadmin_user_parse_query/5]). @@ -62,6 +68,8 @@ handle_info/2, terminate/2, code_change/3, mod_opt_type/1]). +-deprecated({get_queue_length,2}). + -include("ejabberd.hrl"). -include("logger.hrl"). @@ -80,6 +88,25 @@ %% default value for the maximum number of user messages -define(MAX_USER_MESSAGES, infinity). +-type us() :: {binary(), binary()}. +-callback init(binary(), gen_mod:opts()) -> any(). +-callback import(binary(), #offline_msg{}) -> ok | pass. +-callback store_messages(binary(), us(), [#offline_msg{}], + non_neg_integer(), non_neg_integer()) -> + {atomic, any()}. +-callback pop_messages(binary(), binary()) -> + {atomic, [#offline_msg{}]} | {aborted, any()}. +-callback remove_expired_messages(binary()) -> {atomic, any()}. +-callback remove_old_messages(non_neg_integer(), binary()) -> {atomic, any()}. +-callback remove_user(binary(), binary()) -> {atomic, any()}. +-callback read_message_headers(binary(), binary()) -> any(). +-callback read_message(binary(), binary(), non_neg_integer()) -> + {ok, #offline_msg{}} | error. +-callback remove_message(binary(), binary(), non_neg_integer()) -> ok. +-callback read_all_messages(binary(), binary()) -> [#offline_msg{}]. +-callback remove_all_messages(binary(), binary()) -> {atomic, any()}. +-callback count_messages(binary(), binary()) -> non_neg_integer(). + start_link(Host, Opts) -> Proc = gen_mod:get_module_proc(Host, ?PROCNAME), ?GEN_SERVER:start_link({local, Proc}, ?MODULE, @@ -104,14 +131,10 @@ stop(Host) -> %%==================================================================== init([Host, Opts]) -> - case gen_mod:db_type(Host, Opts) of - mnesia -> - mnesia:create_table(offline_msg, - [{disc_only_copies, [node()]}, {type, bag}, - {attributes, record_info(fields, offline_msg)}]), - update_table(); - _ -> ok - end, + Mod = gen_mod:db_mod(Host, Opts, ?MODULE), + Mod:init(Host, Opts), + 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, @@ -124,12 +147,19 @@ 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), + 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, @@ -154,7 +184,7 @@ handle_info(#offline_msg{us = UserServer} = Msg, State) -> Len = length(Msgs), MaxOfflineMsgs = get_max_user_messages(AccessMaxOfflineMsgs, UserServer, Host), - store_offline_msg(Host, UserServer, Msgs, Len, MaxOfflineMsgs, DBType), + store_offline_msg(Host, UserServer, Msgs, Len, MaxOfflineMsgs), {noreply, State}; handle_info(_Info, State) -> @@ -174,77 +204,28 @@ 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, - mnesia) -> - F = fun () -> - Count = if MaxOfflineMsgs =/= infinity -> - Len + count_mnesia_records(US); - true -> 0 - end, - if Count > MaxOfflineMsgs -> discard_warn_sender(Msgs); - true -> - if Len >= (?OFFLINE_TABLE_LOCK_THRESHOLD) -> - mnesia:write_lock_table(offline_msg); - true -> ok - end, - lists:foreach(fun (M) -> mnesia:write(M) end, Msgs) - end - end, - mnesia:transaction(F); -store_offline_msg(Host, {User, _Server}, Msgs, Len, MaxOfflineMsgs, odbc) -> - Count = if MaxOfflineMsgs =/= infinity -> - Len + count_offline_messages(User, Host); - true -> 0 - end, - if Count > MaxOfflineMsgs -> discard_warn_sender(Msgs); - true -> - Query = lists:map(fun (M) -> - Username = - ejabberd_odbc:escape((M#offline_msg.to)#jid.luser), - From = M#offline_msg.from, - To = M#offline_msg.to, - Packet = - jlib:replace_from_to(From, To, - M#offline_msg.packet), - NewPacket = - jlib:add_delay_info(Packet, Host, - M#offline_msg.timestamp, - <<"Offline Storage">>), - XML = - ejabberd_odbc:escape(xml:element_to_binary(NewPacket)), - odbc_queries:add_spool_sql(Username, XML) - end, - Msgs), - odbc_queries:add_spool(Host, Query) - end; -store_offline_msg(Host, {User, _}, Msgs, Len, MaxOfflineMsgs, - riak) -> - Count = if MaxOfflineMsgs =/= infinity -> - Len + count_offline_messages(User, Host); - true -> 0 - end, - if - Count > MaxOfflineMsgs -> - discard_warn_sender(Msgs); - true -> - lists:foreach( - fun(#offline_msg{us = US, - timestamp = TS} = M) -> - ejabberd_riak:put(M, offline_msg_schema(), - [{i, TS}, {'2i', [{<<"us">>, US}]}]) - end, Msgs) +store_offline_msg(Host, US, Msgs, Len, MaxOfflineMsgs) -> + Mod = gen_mod:db_mod(Host, ?MODULE), + case Mod:store_messages(Host, US, Msgs, Len, MaxOfflineMsgs) of + {atomic, discard} -> + discard_warn_sender(Msgs); + _ -> + ok end. get_max_user_messages(AccessRule, {User, Server}, Host) -> @@ -262,7 +243,7 @@ receive_all(US, Msgs, DBType) -> after 0 -> case DBType of mnesia -> Msgs; - odbc -> lists:reverse(Msgs); + sql -> lists:reverse(Msgs); riak -> Msgs end end. @@ -272,38 +253,215 @@ 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, _To, _El}) -> + #xmlel{name = <<"item">>, + attrs = [{<<"jid">>, BareJID}, + {<<"node">>, Node}, + {<<"name">>, jid:to_string(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, lang = Lang} = IQ) -> + Txt = <<"Query to another users is forbidden">>, + IQ#iq{type = error, sub_el = [SubEl, ?ERRT_FORBIDDEN(Lang, Txt)]}. + +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, From, To, El}) -> + NewEl = set_offline_tag(El, Node), + Pid ! {route, From, To, NewEl} + end, read_message_headers(U, S)) + end. + +fetch_msg_by_node(To, Seq) -> + case catch binary_to_integer(Seq) of + I when is_integer(I), I >= 0 -> + LUser = To#jid.luser, + LServer = To#jid.lserver, + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:read_message(LUser, LServer, I); + _ -> + error + end. + +remove_msg_by_node(To, Seq) -> + case catch binary_to_integer(Seq) of + I when is_integer(I), I>= 0 -> + LUser = To#jid.luser, + LServer = To#jid.lserver, + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:remove_message(LUser, LServer, I); + _ -> + 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 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 -> - xml:get_subtag(Packet, <<"body">>) /= false; - unless_chat_state -> - not jlib:is_standalone_chat_state(Packet); - true -> - true - end + case has_offline_tag(Packet) of + 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 -> + false end; true -> false @@ -342,12 +500,15 @@ check_store_hint(Packet) -> end. has_store_hint(Packet) -> - xml:get_subtag_with_xmlns(Packet, <<"store">>, ?NS_HINTS) =/= false. + fxml:get_subtag_with_xmlns(Packet, <<"store">>, ?NS_HINTS) =/= false. has_no_store_hint(Packet) -> - xml:get_subtag_with_xmlns(Packet, <<"no-store">>, ?NS_HINTS) =/= false + fxml:get_subtag_with_xmlns(Packet, <<"no-store">>, ?NS_HINTS) =/= false orelse - xml:get_subtag_with_xmlns(Packet, <<"no-storage">>, ?NS_HINTS) =/= false. + 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) -> @@ -356,12 +517,12 @@ check_event(From, To, 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 = []}; @@ -398,7 +559,7 @@ 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. @@ -407,9 +568,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 -> @@ -426,21 +587,11 @@ find_x_expire(TimeStamp, [El | Els]) -> resend_offline_messages(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), - US = {LUser, LServer}, - F = fun () -> - Rs = mnesia:wread({offline_msg, US}), - mnesia:delete({offline_msg, US}), - Rs - end, - case mnesia:transaction(F) of - {atomic, Rs} -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + case Mod:pop_messages(LUser, LServer) of + {ok, Rs} -> lists:foreach(fun (R) -> - ejabberd_sm ! - {route, R#offline_msg.from, R#offline_msg.to, - jlib:add_delay_info(R#offline_msg.packet, - LServer, - R#offline_msg.timestamp, - <<"Offline Storage">>)} + ejabberd_sm ! offline_msg_to_route(LServer, R) end, lists:keysort(#offline_msg.timestamp, Rs)); _ -> ok @@ -449,194 +600,47 @@ resend_offline_messages(User, Server) -> pop_offline_messages(Ls, User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), - pop_offline_messages(Ls, LUser, LServer, - gen_mod:db_type(LServer, ?MODULE)). - -pop_offline_messages(Ls, LUser, LServer, mnesia) -> - US = {LUser, LServer}, - F = fun () -> - Rs = mnesia:wread({offline_msg, US}), - mnesia:delete({offline_msg, US}), - Rs - end, - case mnesia:transaction(F) of - {atomic, Rs} -> - TS = p1_time_compat:timestamp(), - Ls ++ - lists:map(fun (R) -> - offline_msg_to_route(LServer, R) - end, - lists:filter(fun (R) -> - case R#offline_msg.expire of - never -> true; - TimeStamp -> TS < TimeStamp - end - end, - lists:keysort(#offline_msg.timestamp, Rs))); - _ -> 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}} -> - Ls ++ - lists:flatmap(fun ([_, XML]) -> - case xml_stream:parse_element(XML) of - {error, _Reason} -> - []; - El -> - case offline_msg_to_route(LServer, El) of - error -> - []; - RouteMsg -> - [RouteMsg] - end - end + Mod = gen_mod:db_mod(LServer, ?MODULE), + case Mod:pop_messages(LUser, LServer) of + {ok, Rs} -> + TS = p1_time_compat:timestamp(), + Ls ++ + lists:map(fun (R) -> + offline_msg_to_route(LServer, R) end, - Rs); - _ -> Ls - end; -pop_offline_messages(Ls, LUser, LServer, riak) -> - case ejabberd_riak:get_by_index(offline_msg, offline_msg_schema(), - <<"us">>, {LUser, LServer}) of - {ok, Rs} -> - try - lists:foreach( - fun(#offline_msg{timestamp = T}) -> - ok = ejabberd_riak:delete(offline_msg, T) - end, Rs), - TS = p1_time_compat:timestamp(), - Ls ++ lists:map( - fun (R) -> - offline_msg_to_route(LServer, R) - end, - lists:filter( - fun(R) -> - case R#offline_msg.expire of - never -> true; - TimeStamp -> TS < TimeStamp - end - end, - lists:keysort(#offline_msg.timestamp, Rs))) - catch _:{badmatch, _} -> - Ls - end; + lists:filter( + fun(#offline_msg{packet = Pkt} = R) -> + #xmlel{children = Els} = Pkt, + Expire = case R#offline_msg.expire of + undefined -> + find_x_expire(TS, Els); + Exp -> + Exp + end, + case Expire of + never -> true; + TimeStamp -> TS < TimeStamp + end + end, Rs)); _ -> Ls end. remove_expired_messages(Server) -> LServer = jid:nameprep(Server), - remove_expired_messages(LServer, - gen_mod:db_type(LServer, ?MODULE)). - -remove_expired_messages(_LServer, mnesia) -> - TimeStamp = p1_time_compat:timestamp(), - F = fun () -> - mnesia:write_lock_table(offline_msg), - mnesia:foldl(fun (Rec, _Acc) -> - case Rec#offline_msg.expire of - never -> ok; - TS -> - if TS < TimeStamp -> - mnesia:delete_object(Rec); - true -> ok - end - end - end, - ok, offline_msg) - end, - mnesia:transaction(F); -remove_expired_messages(_LServer, odbc) -> {atomic, ok}; -remove_expired_messages(_LServer, riak) -> {atomic, ok}. + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:remove_expired_messages(LServer). remove_old_messages(Days, Server) -> LServer = jid:nameprep(Server), - remove_old_messages(Days, LServer, - gen_mod:db_type(LServer, ?MODULE)). - -remove_old_messages(Days, _LServer, mnesia) -> - S = p1_time_compat:system_time(seconds) - 60 * 60 * 24 * Days, - MegaSecs1 = S div 1000000, - Secs1 = S rem 1000000, - TimeStamp = {MegaSecs1, Secs1, 0}, - F = fun () -> - mnesia:write_lock_table(offline_msg), - mnesia:foldl(fun (#offline_msg{timestamp = TS} = Rec, - _Acc) - when TS < TimeStamp -> - mnesia:delete_object(Rec); - (_Rec, _Acc) -> ok - end, - ok, offline_msg) - end, - mnesia:transaction(F); - -remove_old_messages(Days, LServer, odbc) -> - case catch ejabberd_odbc:sql_query( - LServer, - [<<"DELETE FROM spool" - " WHERE created_at < " - "DATE_SUB(CURDATE(), INTERVAL ">>, - integer_to_list(Days), <<" DAY);">>]) of - {updated, N} -> - ?INFO_MSG("~p message(s) deleted from offline spool", [N]); - _Error -> - ?ERROR_MSG("Cannot delete message in offline spool: ~p", [_Error]) - end, - {atomic, ok}; -remove_old_messages(_Days, _LServer, riak) -> - {atomic, ok}. + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:remove_old_messages(Days, LServer). 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({offline_msg, US}) end, - mnesia:transaction(F); -remove_user(LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - odbc_queries:del_spool_msg(LServer, Username); -remove_user(LUser, LServer, riak) -> - {atomic, ejabberd_riak:delete_by_index(offline_msg, - <<"us">>, {LUser, LServer})}. - -jid_to_binary(#jid{user = U, server = S, resource = R, - luser = LU, lserver = LS, lresource = LR}) -> - #jid{user = iolist_to_binary(U), - server = iolist_to_binary(S), - resource = iolist_to_binary(R), - luser = iolist_to_binary(LU), - lserver = iolist_to_binary(LS), - lresource = iolist_to_binary(LR)}. - -update_table() -> - Fields = record_info(fields, offline_msg), - case mnesia:table_info(offline_msg, attributes) of - Fields -> - ejabberd_config:convert_table_to_binary( - offline_msg, Fields, bag, - fun(#offline_msg{us = {U, _}}) -> U end, - fun(#offline_msg{us = {U, S}, - from = From, - to = To, - packet = El} = R) -> - R#offline_msg{us = {iolist_to_binary(U), - iolist_to_binary(S)}, - from = jid_to_binary(From), - to = jid_to_binary(To), - packet = xml:to_xmlel(El)} - end); - _ -> - ?INFO_MSG("Recreating offline_msg table", []), - mnesia:transform_table(offline_msg, ignore, Fields) - end. + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:remove_user(LUser, LServer). %% Helper functions: @@ -646,7 +650,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)), @@ -662,138 +666,71 @@ webadmin_page(_, Host, webadmin_page(Acc, _, _) -> Acc. get_offline_els(LUser, LServer) -> - get_offline_els(LUser, LServer, gen_mod:db_type(LServer, ?MODULE)). - -get_offline_els(LUser, LServer, DBType) - when DBType == mnesia; DBType == riak -> - Msgs = read_all_msgs(LUser, LServer, DBType), + Mod = gen_mod:db_mod(LServer, ?MODULE), + Hdrs = Mod:read_message_headers(LUser, LServer), lists:map( - fun(Msg) -> - {route, From, To, Packet} = offline_msg_to_route(LServer, Msg), - 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} -> - lists:flatmap( - fun([XML]) -> - case xml_stream:parse_element(XML) of - #xmlel{} = El -> - case offline_msg_to_route(LServer, El) of - {route, _, _, NewEl} -> - [NewEl]; - error -> - [] - end; - _ -> - [] - end - end, Rs); - _ -> - [] - end. + fun({_Seq, From, To, Packet}) -> + jlib:replace_from_to(From, To, Packet) + end, Hdrs). offline_msg_to_route(LServer, #offline_msg{} = R) -> - {route, R#offline_msg.from, R#offline_msg.to, - jlib:add_delay_info(R#offline_msg.packet, LServer, R#offline_msg.timestamp, - <<"Offline Storage">>)}; -offline_msg_to_route(_LServer, #xmlel{} = El) -> - To = jid:from_string(xml:get_tag_attr_s(<<"to">>, El)), - From = jid:from_string(xml:get_tag_attr_s(<<"from">>, El)), - if (To /= error) and (From /= error) -> - {route, From, To, El}; - true -> - error - end. - -read_all_msgs(LUser, LServer, mnesia) -> - US = {LUser, LServer}, - lists:keysort(#offline_msg.timestamp, - mnesia:dirty_read({offline_msg, US})); -read_all_msgs(LUser, LServer, riak) -> - case ejabberd_riak:get_by_index( - offline_msg, offline_msg_schema(), - <<"us">>, {LUser, LServer}) of - {ok, Rs} -> - lists:keysort(#offline_msg.timestamp, Rs); - _Err -> - [] - 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); - _ -> [] - end. + El = case R#offline_msg.timestamp of + undefined -> + R#offline_msg.packet; + TS -> + jlib:add_delay_info(R#offline_msg.packet, LServer, TS, + <<"Offline Storage">>) + end, + {route, R#offline_msg.from, R#offline_msg.to, El}. + +read_message_headers(LUser, LServer) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + lists:map( + fun({Seq, From, To, El}) -> + Node = integer_to_binary(Seq), + {Node, From, To, El} + end, Mod:read_message_headers(LUser, LServer)). -format_user_queue(Msgs, DBType) when DBType == mnesia; DBType == riak -> - lists:map(fun (#offline_msg{timestamp = TimeStamp, - from = From, to = To, - packet = - #xmlel{name = Name, attrs = Attrs, - children = Els}} = - Msg) -> - ID = jlib:encode_base64((term_to_binary(Msg))), - {{Year, Month, Day}, {Hour, Minute, Second}} = - calendar:now_to_local_time(TimeStamp), - Time = - iolist_to_binary(io_lib:format("~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w", - [Year, Month, Day, - Hour, Minute, - Second])), - 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}, - FPacket = ejabberd_web_admin:pretty_print_xml(Packet), - ?XE(<<"tr">>, - [?XAE(<<"td">>, [{<<"class">>, <<"valign">>}], - [?INPUT(<<"checkbox">>, <<"selected">>, ID)]), - ?XAC(<<"td">>, [{<<"class">>, <<"valign">>}], Time), - ?XAC(<<"td">>, [{<<"class">>, <<"valign">>}], SFrom), - ?XAC(<<"td">>, [{<<"class">>, <<"valign">>}], STo), - ?XAE(<<"td">>, [{<<"class">>, <<"valign">>}], - [?XC(<<"pre">>, FPacket)])]) - end, - Msgs); -format_user_queue(Msgs, odbc) -> - lists:map(fun (#xmlel{} = Msg) -> - ID = jlib:encode_base64((term_to_binary(Msg))), - Packet = Msg, - FPacket = ejabberd_web_admin:pretty_print_xml(Packet), - ?XE(<<"tr">>, - [?XAE(<<"td">>, [{<<"class">>, <<"valign">>}], - [?INPUT(<<"checkbox">>, <<"selected">>, ID)]), - ?XAE(<<"td">>, [{<<"class">>, <<"valign">>}], - [?XC(<<"pre">>, FPacket)])]) - end, - Msgs). +format_user_queue(Hdrs) -> + lists:map( + fun({Seq, From, To, El}) -> + ID = integer_to_binary(Seq), + FPacket = ejabberd_web_admin:pretty_print_xml(El), + SFrom = jid:to_string(From), + STo = jid:to_string(To), + Stamp = fxml:get_path_s(El, [{elem, <<"delay">>}, + {attr, <<"stamp">>}]), + Time = case jlib:datetime_string_to_timestamp(Stamp) of + {_, _, _} = Now -> + {{Year, Month, Day}, {Hour, Minute, Second}} = + calendar:now_to_local_time(Now), + iolist_to_binary( + io_lib:format( + "~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w", + [Year, Month, Day, Hour, Minute, + Second])); + _ -> + <<"">> + end, + ?XE(<<"tr">>, + [?XAE(<<"td">>, [{<<"class">>, <<"valign">>}], + [?INPUT(<<"checkbox">>, <<"selected">>, ID)]), + ?XAC(<<"td">>, [{<<"class">>, <<"valign">>}], Time), + ?XAC(<<"td">>, [{<<"class">>, <<"valign">>}], SFrom), + ?XAC(<<"td">>, [{<<"class">>, <<"valign">>}], STo), + ?XAE(<<"td">>, [{<<"class">>, <<"valign">>}], + [?XC(<<"pre">>, FPacket)])]) + end, Hdrs). user_queue(User, Server, Query, Lang) -> 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, - DBType), - MsgsAll = read_all_msgs(LUser, LServer, DBType), - Msgs = get_messages_subset(US, Server, MsgsAll, - DBType), - FMsgs = format_user_queue(Msgs, DBType), + Mod = gen_mod:db_mod(LServer, ?MODULE), + Res = user_queue_parse_query(LUser, LServer, Query), + HdrsAll = Mod:read_message_headers(LUser, LServer), + Hdrs = get_messages_subset(US, Server, HdrsAll), + FMsgs = format_user_queue(Hdrs), [?XC(<<"h1">>, list_to_binary(io_lib:format(?T(<<"~s's Offline Messages Queue">>), [us_to_list(US)])))] @@ -823,128 +760,33 @@ user_queue(User, Server, Query, Lang) -> ?INPUTT(<<"submit">>, <<"delete">>, <<"Delete Selected">>)])]. -user_queue_parse_query(LUser, LServer, Query, mnesia) -> - US = {LUser, LServer}, - case lists:keysearch(<<"delete">>, 1, Query) of - {value, _} -> - Msgs = lists:keysort(#offline_msg.timestamp, - mnesia:dirty_read({offline_msg, US})), - F = fun () -> - lists:foreach(fun (Msg) -> - ID = - jlib:encode_base64((term_to_binary(Msg))), - case lists:member({<<"selected">>, - ID}, - Query) - of - true -> mnesia:delete_object(Msg); - false -> ok - end - end, - Msgs) - end, - mnesia:transaction(F), - ok; - false -> nothing - end; -user_queue_parse_query(LUser, LServer, Query, riak) -> - case lists:keysearch(<<"delete">>, 1, Query) of - {value, _} -> - Msgs = read_all_msgs(LUser, LServer, riak), - lists:foreach( - fun (Msg) -> - ID = jlib:encode_base64((term_to_binary(Msg))), - case lists:member({<<"selected">>, ID}, Query) of - true -> - ejabberd_riak:delete(offline_msg, - Msg#offline_msg.timestamp); - false -> - ok - end - end, - Msgs), - ok; - false -> - nothing - end; -user_queue_parse_query(LUser, LServer, Query, odbc) -> - Username = ejabberd_odbc:escape(LUser), +user_queue_parse_query(LUser, LServer, Query) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), case lists:keysearch(<<"delete">>, 1, Query) of - {value, _} -> - Msgs = case catch ejabberd_odbc:sql_query(LServer, - [<<"select xml, seq from spool where username='">>, - Username, - <<"' order by seq;">>]) - of - {selected, [<<"xml">>, <<"seq">>], Rs} -> - lists:flatmap(fun ([XML, Seq]) -> - case xml_stream:parse_element(XML) - of - {error, _Reason} -> []; - El -> [{El, Seq}] - end - end, - Rs); - _ -> [] - end, - F = fun () -> - lists:foreach(fun ({Msg, Seq}) -> - ID = - jlib:encode_base64((term_to_binary(Msg))), - case lists:member({<<"selected">>, - ID}, - Query) - of - true -> - SSeq = - ejabberd_odbc:escape(Seq), - catch - ejabberd_odbc:sql_query(LServer, - [<<"delete from spool where username='">>, - Username, - <<"' and seq='">>, - SSeq, - <<"';">>]); - false -> ok - end - end, - Msgs) - end, - mnesia:transaction(F), - ok; - false -> nothing + {value, _} -> + case lists:keyfind(<<"selected">>, 1, Query) of + {_, Seq} -> + case catch binary_to_integer(Seq) of + I when is_integer(I), I>=0 -> + Mod:remove_message(LUser, LServer, I), + ok; + _ -> + nothing + end; + false -> + nothing + end; + _ -> + nothing end. us_to_list({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) -> +get_messages_subset(User, Host, MsgsAll) -> Access = gen_mod:get_module_opt(Host, ?MODULE, access_max_user_messages, fun(A) when is_atom(A) -> A end, max_user_offline_messages), @@ -955,36 +797,23 @@ get_messages_subset(User, Host, MsgsAll, DBType) -> _ -> 100 end, Length = length(MsgsAll), - get_messages_subset2(MaxOfflineMsgs, Length, MsgsAll, - DBType). + get_messages_subset2(MaxOfflineMsgs, Length, MsgsAll). -get_messages_subset2(Max, Length, MsgsAll, _DBType) - when Length =< Max * 2 -> +get_messages_subset2(Max, Length, MsgsAll) when Length =< Max * 2 -> MsgsAll; -get_messages_subset2(Max, Length, MsgsAll, DBType) - when DBType == mnesia; DBType == riak -> +get_messages_subset2(Max, Length, MsgsAll) -> FirstN = Max, {MsgsFirstN, Msgs2} = lists:split(FirstN, MsgsAll), MsgsLastN = lists:nthtail(Length - FirstN - FirstN, Msgs2), NoJID = jid:make(<<"...">>, <<"...">>, <<"">>), - IntermediateMsg = #offline_msg{timestamp = p1_time_compat:timestamp(), - from = NoJID, to = NoJID, - packet = - #xmlel{name = <<"...">>, attrs = [], - children = []}}, - MsgsFirstN ++ [IntermediateMsg] ++ MsgsLastN; -get_messages_subset2(Max, Length, MsgsAll, odbc) -> - FirstN = Max, - {MsgsFirstN, Msgs2} = lists:split(FirstN, MsgsAll), - MsgsLastN = lists:nthtail(Length - FirstN - FirstN, - Msgs2), + Seq = <<"0">>, IntermediateMsg = #xmlel{name = <<"...">>, attrs = [], children = []}, - MsgsFirstN ++ [IntermediateMsg] ++ MsgsLastN. + MsgsFirstN ++ [{Seq, NoJID, NoJID, IntermediateMsg}] ++ MsgsLastN. webadmin_user(Acc, User, Server, Lang) -> - QueueLen = get_queue_length(jid:nodeprep(User), + QueueLen = count_offline_messages(jid:nodeprep(User), jid:nameprep(Server)), FQueueLen = [?AC(<<"queue/">>, (iolist_to_binary(integer_to_list(QueueLen))))], @@ -998,26 +827,8 @@ webadmin_user(Acc, User, Server, Lang) -> delete_all_msgs(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), - delete_all_msgs(LUser, LServer, - gen_mod:db_type(LServer, ?MODULE)). - -delete_all_msgs(LUser, LServer, mnesia) -> - US = {LUser, LServer}, - F = fun () -> - mnesia:write_lock_table(offline_msg), - lists:foreach(fun (Msg) -> mnesia:delete_object(Msg) - end, - mnesia:dirty_read({offline_msg, US})) - end, - mnesia:transaction(F); -delete_all_msgs(LUser, LServer, riak) -> - Res = ejabberd_riak:delete_by_index(offline_msg, - <<"us">>, {LUser, LServer}), - {atomic, Res}; -delete_all_msgs(LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - odbc_queries:del_spool_msg(LServer, Username), - {atomic, ok}. + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:remove_all_messages(LUser, LServer). webadmin_user_parse_query(_, <<"removealloffline">>, User, Server, _Query) -> @@ -1039,114 +850,20 @@ webadmin_user_parse_query(Acc, _Action, _User, _Server, count_offline_messages(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), - DBType = gen_mod:db_type(LServer, ?MODULE), - count_offline_messages(LUser, LServer, DBType). + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:count_messages(LUser, LServer). -count_offline_messages(LUser, LServer, mnesia) -> - US = {LUser, LServer}, - F = fun () -> - count_mnesia_records(US) - end, - case catch mnesia:async_dirty(F) of - I when is_integer(I) -> I; - _ -> 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 - end; -count_offline_messages(LUser, LServer, riak) -> - case ejabberd_riak:count_by_index( - offline_msg, <<"us">>, {LUser, LServer}) of - {ok, Res} -> - Res; - _ -> - 0 - end. - -%% Return the number of records matching a given match expression. -%% This function is intended to be used inside a Mnesia transaction. -%% The count has been written to use the fewest possible memory by -%% getting the record by small increment and by using continuation. --define(BATCHSIZE, 100). - -count_mnesia_records(US) -> - MatchExpression = #offline_msg{us = US, _ = '_'}, - case mnesia:select(offline_msg, [{MatchExpression, [], [[]]}], - ?BATCHSIZE, read) of - {Result, Cont} -> - Count = length(Result), - count_records_cont(Cont, Count); - '$end_of_table' -> - 0 - end. - -count_records_cont(Cont, Count) -> - case mnesia:select(Cont) of - {Result, Cont} -> - NewCount = Count + length(Result), - count_records_cont(Cont, NewCount); - '$end_of_table' -> - Count - end. - -offline_msg_schema() -> - {record_info(fields, offline_msg), #offline_msg{}}. - -export(_Server) -> - [{offline_msg, - fun(Host, #offline_msg{us = {LUser, LServer}, - timestamp = TimeStamp, from = From, to = To, - packet = Packet}) - when LServer == Host -> - Username = ejabberd_odbc:escape(LUser), - 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)), - [[<<"delete from spool where username='">>, Username, <<"';">>], - [<<"insert into spool(username, xml) values ('">>, - Username, <<"', '">>, XML, <<"');">>]]; - (_Host, _R) -> - [] - end}]. +export(LServer) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:export(LServer). import(LServer) -> - [{<<"select username, xml from spool;">>, - fun([LUser, XML]) -> - El = #xmlel{} = xml_stream:parse_element(XML), - From = #jid{} = jid:from_string( - xml:get_attr_s(<<"from">>, El#xmlel.attrs)), - To = #jid{} = jid:from_string( - xml:get_attr_s(<<"to">>, El#xmlel.attrs)), - Stamp = xml:get_path_s(El, [{elem, <<"delay">>}, - {attr, <<"stamp">>}]), - TS = case jlib:datetime_string_to_timestamp(Stamp) of - {_, _, _} = Now -> - Now; - undefined -> - p1_time_compat:timestamp() - end, - Expire = find_x_expire(TS, El#xmlel.children), - #offline_msg{us = {LUser, LServer}, - from = From, to = To, - timestamp = TS, expire = Expire} - end}]. - -import(_LServer, mnesia, #offline_msg{} = Msg) -> - mnesia:dirty_write(Msg); -import(_LServer, riak, #offline_msg{us = US, timestamp = TS} = M) -> - ejabberd_riak:put(M, offline_msg_schema(), - [{i, TS}, {'2i', [{<<"us">>, US}]}]); -import(_, _, _) -> - pass. + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:import(LServer). + +import(LServer, DBType, Data) -> + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:import(LServer, Data). mod_opt_type(access_max_user_messages) -> fun (A) -> A end; diff --git a/src/mod_offline_mnesia.erl b/src/mod_offline_mnesia.erl new file mode 100644 index 000000000..6a1d9e309 --- /dev/null +++ b/src/mod_offline_mnesia.erl @@ -0,0 +1,232 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 15 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_offline_mnesia). + +-behaviour(mod_offline). + +-export([init/2, store_messages/5, pop_messages/2, remove_expired_messages/1, + remove_old_messages/2, remove_user/2, read_message_headers/2, + read_message/3, remove_message/3, read_all_messages/2, + remove_all_messages/2, count_messages/2, import/2]). + +-include("jlib.hrl"). +-include("mod_offline.hrl"). +-include("logger.hrl"). + +-define(OFFLINE_TABLE_LOCK_THRESHOLD, 1000). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + mnesia:create_table(offline_msg, + [{disc_only_copies, [node()]}, {type, bag}, + {attributes, record_info(fields, offline_msg)}]), + update_table(). + +store_messages(_Host, US, Msgs, Len, MaxOfflineMsgs) -> + F = fun () -> + Count = if MaxOfflineMsgs =/= infinity -> + Len + count_mnesia_records(US); + true -> 0 + end, + if Count > MaxOfflineMsgs -> discard; + true -> + if Len >= (?OFFLINE_TABLE_LOCK_THRESHOLD) -> + mnesia:write_lock_table(offline_msg); + true -> ok + end, + lists:foreach(fun (M) -> mnesia:write(M) end, Msgs) + end + end, + mnesia:transaction(F). + +pop_messages(LUser, LServer) -> + US = {LUser, LServer}, + F = fun () -> + Rs = mnesia:wread({offline_msg, US}), + mnesia:delete({offline_msg, US}), + Rs + end, + case mnesia:transaction(F) of + {atomic, L} -> + {ok, lists:keysort(#offline_msg.timestamp, L)}; + {aborted, Reason} -> + {error, Reason} + end. + +remove_expired_messages(_LServer) -> + TimeStamp = p1_time_compat:timestamp(), + F = fun () -> + mnesia:write_lock_table(offline_msg), + mnesia:foldl(fun (Rec, _Acc) -> + case Rec#offline_msg.expire of + never -> ok; + TS -> + if TS < TimeStamp -> + mnesia:delete_object(Rec); + true -> ok + end + end + end, + ok, offline_msg) + end, + mnesia:transaction(F). + +remove_old_messages(Days, _LServer) -> + S = p1_time_compat:system_time(seconds) - 60 * 60 * 24 * Days, + MegaSecs1 = S div 1000000, + Secs1 = S rem 1000000, + TimeStamp = {MegaSecs1, Secs1, 0}, + F = fun () -> + mnesia:write_lock_table(offline_msg), + mnesia:foldl(fun (#offline_msg{timestamp = TS} = Rec, + _Acc) + when TS < TimeStamp -> + mnesia:delete_object(Rec); + (_Rec, _Acc) -> ok + end, + ok, offline_msg) + end, + mnesia:transaction(F). + +remove_user(LUser, LServer) -> + US = {LUser, LServer}, + F = fun () -> mnesia:delete({offline_msg, US}) end, + mnesia:transaction(F). + +read_message_headers(LUser, LServer) -> + Msgs = mnesia:dirty_read({offline_msg, {LUser, LServer}}), + Hdrs = lists:map( + fun(#offline_msg{from = From, to = To, packet = Pkt, + timestamp = TS}) -> + Seq = now_to_integer(TS), + NewPkt = jlib:add_delay_info(Pkt, LServer, TS, + <<"Offline Storage">>), + {Seq, From, To, NewPkt} + end, Msgs), + lists:keysort(1, Hdrs). + +read_message(LUser, LServer, I) -> + US = {LUser, LServer}, + TS = integer_to_now(I), + case mnesia:dirty_match_object( + offline_msg, #offline_msg{us = US, timestamp = TS, _ = '_'}) of + [Msg|_] -> + {ok, Msg}; + _ -> + error + end. + +remove_message(LUser, LServer, I) -> + US = {LUser, LServer}, + TS = integer_to_now(I), + Msgs = mnesia:dirty_match_object( + offline_msg, #offline_msg{us = US, timestamp = TS, _ = '_'}), + lists:foreach( + fun(Msg) -> + mnesia:dirty_delete_object(Msg) + end, Msgs). + +read_all_messages(LUser, LServer) -> + US = {LUser, LServer}, + lists:keysort(#offline_msg.timestamp, + mnesia:dirty_read({offline_msg, US})). + +remove_all_messages(LUser, LServer) -> + US = {LUser, LServer}, + F = fun () -> + mnesia:write_lock_table(offline_msg), + lists:foreach(fun (Msg) -> mnesia:delete_object(Msg) end, + mnesia:dirty_read({offline_msg, US})) + end, + mnesia:transaction(F). + +count_messages(LUser, LServer) -> + US = {LUser, LServer}, + F = fun () -> + count_mnesia_records(US) + end, + case catch mnesia:async_dirty(F) of + I when is_integer(I) -> I; + _ -> 0 + end. + +import(_LServer, #offline_msg{} = Msg) -> + mnesia:dirty_write(Msg). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +%% Return the number of records matching a given match expression. +%% This function is intended to be used inside a Mnesia transaction. +%% The count has been written to use the fewest possible memory by +%% getting the record by small increment and by using continuation. +-define(BATCHSIZE, 100). + +count_mnesia_records(US) -> + MatchExpression = #offline_msg{us = US, _ = '_'}, + case mnesia:select(offline_msg, [{MatchExpression, [], [[]]}], + ?BATCHSIZE, read) of + {Result, Cont} -> + Count = length(Result), + count_records_cont(Cont, Count); + '$end_of_table' -> + 0 + end. + +count_records_cont(Cont, Count) -> + case mnesia:select(Cont) of + {Result, Cont} -> + NewCount = Count + length(Result), + count_records_cont(Cont, NewCount); + '$end_of_table' -> + Count + end. + +jid_to_binary(#jid{user = U, server = S, resource = R, + luser = LU, lserver = LS, lresource = LR}) -> + #jid{user = iolist_to_binary(U), + server = iolist_to_binary(S), + resource = iolist_to_binary(R), + luser = iolist_to_binary(LU), + lserver = iolist_to_binary(LS), + lresource = iolist_to_binary(LR)}. + +now_to_integer({MS, S, US}) -> + (MS * 1000000 + S) * 1000000 + US. + +integer_to_now(Int) -> + Secs = Int div 1000000, + USec = Int rem 1000000, + MSec = Secs div 1000000, + Sec = Secs rem 1000000, + {MSec, Sec, USec}. + +update_table() -> + Fields = record_info(fields, offline_msg), + case mnesia:table_info(offline_msg, attributes) of + Fields -> + ejabberd_config:convert_table_to_binary( + offline_msg, Fields, bag, + fun(#offline_msg{us = {U, _}}) -> U end, + fun(#offline_msg{us = {U, S}, + from = From, + to = To, + packet = El} = R) -> + R#offline_msg{us = {iolist_to_binary(U), + iolist_to_binary(S)}, + from = jid_to_binary(From), + to = jid_to_binary(To), + packet = fxml:to_xmlel(El)} + end); + _ -> + ?INFO_MSG("Recreating offline_msg table", []), + mnesia:transform_table(offline_msg, ignore, Fields) + end. diff --git a/src/mod_offline_riak.erl b/src/mod_offline_riak.erl new file mode 100644 index 000000000..217e8f828 --- /dev/null +++ b/src/mod_offline_riak.erl @@ -0,0 +1,153 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 15 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_offline_riak). + +-behaviour(mod_offline). + +-export([init/2, store_messages/5, pop_messages/2, remove_expired_messages/1, + remove_old_messages/2, remove_user/2, read_message_headers/2, + read_message/3, remove_message/3, read_all_messages/2, + remove_all_messages/2, count_messages/2, import/2]). + +-include("jlib.hrl"). +-include("mod_offline.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ok. + +store_messages(Host, {User, _}, Msgs, Len, MaxOfflineMsgs) -> + Count = if MaxOfflineMsgs =/= infinity -> + Len + count_messages(User, Host); + true -> 0 + end, + if + Count > MaxOfflineMsgs -> + {atomic, discard}; + true -> + try + lists:foreach( + fun(#offline_msg{us = US, + timestamp = TS} = M) -> + ok = ejabberd_riak:put( + M, offline_msg_schema(), + [{i, TS}, {'2i', [{<<"us">>, US}]}]) + end, Msgs), + {atomic, ok} + catch _:{badmatch, Err} -> + {atomic, Err} + end + end. + +pop_messages(LUser, LServer) -> + case ejabberd_riak:get_by_index(offline_msg, offline_msg_schema(), + <<"us">>, {LUser, LServer}) of + {ok, Rs} -> + try + lists:foreach( + fun(#offline_msg{timestamp = T}) -> + ok = ejabberd_riak:delete(offline_msg, T) + end, Rs), + {ok, lists:keysort(#offline_msg.timestamp, Rs)} + catch _:{badmatch, Err} -> + Err + end; + Err -> + Err + end. + +remove_expired_messages(_LServer) -> + %% TODO + {atomic, ok}. + +remove_old_messages(_Days, _LServer) -> + %% TODO + {atomic, ok}. + +remove_user(LUser, LServer) -> + {atomic, ejabberd_riak:delete_by_index(offline_msg, + <<"us">>, {LUser, LServer})}. + +read_message_headers(LUser, LServer) -> + case ejabberd_riak:get_by_index( + offline_msg, offline_msg_schema(), + <<"us">>, {LUser, LServer}) of + {ok, Rs} -> + Hdrs = lists:map( + fun(#offline_msg{from = From, to = To, packet = Pkt, + timestamp = TS}) -> + Seq = now_to_integer(TS), + NewPkt = jlib:add_delay_info( + Pkt, LServer, TS, <<"Offline Storage">>), + {Seq, From, To, NewPkt} + end, Rs), + lists:keysort(1, Hdrs); + _Err -> + [] + end. + +read_message(_LUser, _LServer, I) -> + TS = integer_to_now(I), + case ejabberd_riak:get(offline_msg, offline_msg_schema(), TS) of + {ok, Msg} -> + {ok, Msg}; + _ -> + error + end. + +remove_message(_LUser, _LServer, I) -> + TS = integer_to_now(I), + ejabberd_riak:delete(offline_msg, TS), + ok. + +read_all_messages(LUser, LServer) -> + case ejabberd_riak:get_by_index( + offline_msg, offline_msg_schema(), + <<"us">>, {LUser, LServer}) of + {ok, Rs} -> + lists:keysort(#offline_msg.timestamp, Rs); + _Err -> + [] + end. + +remove_all_messages(LUser, LServer) -> + Res = ejabberd_riak:delete_by_index(offline_msg, + <<"us">>, {LUser, LServer}), + {atomic, Res}. + +count_messages(LUser, LServer) -> + case ejabberd_riak:count_by_index( + offline_msg, <<"us">>, {LUser, LServer}) of + {ok, Res} -> + Res; + _ -> + 0 + end. + +import(_LServer, #offline_msg{us = US, timestamp = TS} = M) -> + ejabberd_riak:put(M, offline_msg_schema(), + [{i, TS}, {'2i', [{<<"us">>, US}]}]). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +offline_msg_schema() -> + {record_info(fields, offline_msg), #offline_msg{}}. + +now_to_integer({MS, S, US}) -> + (MS * 1000000 + S) * 1000000 + US. + +integer_to_now(Int) -> + Secs = Int div 1000000, + USec = Int rem 1000000, + MSec = Secs div 1000000, + Sec = Secs rem 1000000, + {MSec, Sec, USec}. diff --git a/src/mod_offline_sql.erl b/src/mod_offline_sql.erl new file mode 100644 index 000000000..4d9455570 --- /dev/null +++ b/src/mod_offline_sql.erl @@ -0,0 +1,252 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 15 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_offline_sql). + +-compile([{parse_transform, ejabberd_sql_pt}]). + +-behaviour(mod_offline). + +-export([init/2, store_messages/5, pop_messages/2, remove_expired_messages/1, + remove_old_messages/2, remove_user/2, read_message_headers/2, + read_message/3, remove_message/3, read_all_messages/2, + remove_all_messages/2, count_messages/2, import/1, import/2, + export/1]). + +-include("jlib.hrl"). +-include("mod_offline.hrl"). +-include("logger.hrl"). +-include("ejabberd_sql_pt.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ok. + +store_messages(Host, {User, _Server}, Msgs, Len, MaxOfflineMsgs) -> + Count = if MaxOfflineMsgs =/= infinity -> + Len + count_messages(User, Host); + true -> 0 + end, + if Count > MaxOfflineMsgs -> {atomic, discard}; + true -> + Query = lists:map( + fun(M) -> + Username = + ejabberd_sql:escape((M#offline_msg.to)#jid.luser), + From = M#offline_msg.from, + To = M#offline_msg.to, + Packet = + jlib:replace_from_to(From, To, + M#offline_msg.packet), + NewPacket = + jlib:add_delay_info(Packet, Host, + M#offline_msg.timestamp, + <<"Offline Storage">>), + XML = + ejabberd_sql:escape(fxml:element_to_binary(NewPacket)), + sql_queries:add_spool_sql(Username, XML) + end, + Msgs), + sql_queries:add_spool(Host, Query) + end. + +pop_messages(LUser, LServer) -> + case sql_queries:get_and_del_spool_msg_t(LServer, LUser) of + {atomic, {selected, Rs}} -> + {ok, lists:flatmap( + fun({_, XML}) -> + case xml_to_offline_msg(XML) of + {ok, Msg} -> + [Msg]; + _Err -> + [] + end + end, Rs)}; + Err -> + {error, Err} + end. + +remove_expired_messages(_LServer) -> + %% TODO + {atomic, ok}. + +remove_old_messages(Days, LServer) -> + case catch ejabberd_sql:sql_query( + LServer, + [<<"DELETE FROM spool" + " WHERE created_at < " + "DATE_SUB(CURDATE(), INTERVAL ">>, + integer_to_list(Days), <<" DAY);">>]) of + {updated, N} -> + ?INFO_MSG("~p message(s) deleted from offline spool", [N]); + _Error -> + ?ERROR_MSG("Cannot delete message in offline spool: ~p", [_Error]) + end, + {atomic, ok}. + +remove_user(LUser, LServer) -> + sql_queries:del_spool_msg(LServer, LUser). + +read_message_headers(LUser, LServer) -> + Username = ejabberd_sql:escape(LUser), + case catch ejabberd_sql:sql_query( + LServer, [<<"select xml, seq from spool where username ='">>, + Username, <<"' order by seq;">>]) of + {selected, [<<"xml">>, <<"seq">>], Rows} -> + lists:flatmap( + fun([XML, Seq]) -> + case xml_to_offline_msg(XML) of + {ok, #offline_msg{from = From, + to = To, + packet = El}} -> + Seq0 = binary_to_integer(Seq), + [{Seq0, From, To, El}]; + _ -> + [] + end + end, Rows); + _Err -> + [] + end. + +read_message(LUser, LServer, Seq) -> + Username = ejabberd_sql:escape(LUser), + SSeq = ejabberd_sql:escape(integer_to_binary(Seq)), + case ejabberd_sql:sql_query( + LServer, + [<<"select xml from spool where username='">>, Username, + <<"' and seq='">>, SSeq, <<"';">>]) of + {selected, [<<"xml">>], [[RawXML]|_]} -> + case xml_to_offline_msg(RawXML) of + {ok, Msg} -> + {ok, Msg}; + _ -> + error + end; + _ -> + error + end. + +remove_message(LUser, LServer, Seq) -> + Username = ejabberd_sql:escape(LUser), + SSeq = ejabberd_sql:escape(integer_to_binary(Seq)), + ejabberd_sql:sql_query( + LServer, + [<<"delete from spool where username='">>, Username, + <<"' and seq='">>, SSeq, <<"';">>]), + ok. + +read_all_messages(LUser, LServer) -> + case catch ejabberd_sql: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_to_offline_msg(XML) of + {ok, Msg} -> [Msg]; + _ -> [] + end + end, Rs); + _ -> + [] + end. + +remove_all_messages(LUser, LServer) -> + sql_queries:del_spool_msg(LServer, LUser), + {atomic, ok}. + +count_messages(LUser, LServer) -> + case catch ejabberd_sql:sql_query( + LServer, + ?SQL("select @(count(*))d from spool " + "where username=%(LUser)s")) of + {selected, [{Res}]} -> + Res; + _ -> 0 + end. + +export(_Server) -> + [{offline_msg, + fun(Host, #offline_msg{us = {LUser, LServer}, + timestamp = TimeStamp, from = From, to = To, + packet = Packet}) + when LServer == Host -> + Username = ejabberd_sql:escape(LUser), + Packet1 = jlib:replace_from_to(From, To, Packet), + Packet2 = jlib:add_delay_info(Packet1, LServer, TimeStamp, + <<"Offline Storage">>), + XML = ejabberd_sql:escape(fxml:element_to_binary(Packet2)), + [[<<"delete from spool where username='">>, Username, <<"';">>], + [<<"insert into spool(username, xml) values ('">>, + Username, <<"', '">>, XML, <<"');">>]]; + (_Host, _R) -> + [] + end}]. + +import(LServer) -> + [{<<"select username, xml from spool;">>, + fun([LUser, XML]) -> + 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 -> + p1_time_compat:timestamp() + end, + Expire = mod_offline:find_x_expire(TS, El#xmlel.children), + #offline_msg{us = {LUser, LServer}, + from = From, to = To, + packet = El, + timestamp = TS, expire = Expire} + end}]. + +import(_, _) -> + pass. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +xml_to_offline_msg(XML) -> + case fxml_stream:parse_element(XML) of + #xmlel{} = El -> + el_to_offline_msg(El); + Err -> + ?ERROR_MSG("got ~p when parsing XML packet ~s", + [Err, XML]), + Err + end. + +el_to_offline_msg(El) -> + To_s = fxml:get_tag_attr_s(<<"to">>, El), + From_s = fxml:get_tag_attr_s(<<"from">>, El), + To = jid:from_string(To_s), + From = jid:from_string(From_s), + if To == error -> + ?ERROR_MSG("failed to get 'to' JID from offline XML ~p", [El]), + {error, bad_jid_to}; + From == error -> + ?ERROR_MSG("failed to get 'from' JID from offline XML ~p", [El]), + {error, bad_jid_from}; + true -> + {ok, #offline_msg{us = {To#jid.luser, To#jid.lserver}, + from = From, + to = To, + timestamp = undefined, + expire = undefined, + packet = El}} + end. diff --git a/src/mod_ping.erl b/src/mod_ping.erl index 85ff770e3..f1c175a91 100644 --- a/src/mod_ping.erl +++ b/src/mod_ping.erl @@ -202,13 +202,14 @@ code_change(_OldVsn, State, _Extra) -> {ok, State}. %% Hook callbacks %%==================================================================== iq_ping(_From, _To, - #iq{type = Type, sub_el = SubEl} = IQ) -> + #iq{type = Type, sub_el = SubEl, lang = Lang} = IQ) -> case {Type, SubEl} of {get, #xmlel{name = <<"ping">>}} -> IQ#iq{type = result, sub_el = []}; _ -> + Txt = <<"Ping query is incorrect">>, IQ#iq{type = error, - sub_el = [SubEl, ?ERR_FEATURE_NOT_IMPLEMENTED]} + sub_el = [SubEl, ?ERRT_BAD_REQUEST(Lang, Txt)]} end. user_online(_SID, JID, _Info) -> diff --git a/src/mod_pres_counter.erl b/src/mod_pres_counter.erl index 34fdcdb75..1118b7bbc 100644 --- a/src/mod_pres_counter.erl +++ b/src/mod_pres_counter.erl @@ -52,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; diff --git a/src/mod_privacy.erl b/src/mod_privacy.erl index 9c2af037b..413dcb52a 100644 --- a/src/mod_privacy.erl +++ b/src/mod_privacy.erl @@ -33,18 +33,10 @@ -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]). - --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, mod_opt_type/1]). + check_packet/6, remove_user/2, + is_list_needdb/1, updated_list/3, + item_to_xml/1, get_user_lists/2, import/3, + set_privacy_list/1, mod_opt_type/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -53,20 +45,24 @@ -include("mod_privacy.hrl"). -privacy_schema() -> - {record_info(fields, privacy), #privacy{}}. +-callback init(binary(), gen_mod:opts()) -> any(). +-callback import(binary(), #privacy{}) -> ok | pass. +-callback process_lists_get(binary(), binary()) -> {none | binary(), [xmlel()]} | error. +-callback process_list_get(binary(), binary(), binary()) -> [listitem()] | error | not_found. +-callback process_default_set(binary(), binary(), {value, binary()} | false) -> {atomic, any()}. +-callback process_active_set(binary(), binary(), binary()) -> [listitem()] | error. +-callback remove_privacy_list(binary(), binary(), binary()) -> {atomic, any()}. +-callback set_privacy_list(#privacy{}) -> any(). +-callback set_privacy_list(binary(), binary(), binary(), [listitem()]) -> {atomic, any()}. +-callback get_user_list(binary(), binary()) -> {none | binary(), [listitem()]}. +-callback get_user_lists(binary(), binary()) -> {ok, #privacy{}} | error. +-callback remove_user(binary(), binary()) -> {atomic, any()}. start(Host, Opts) -> IQDisc = gen_mod:get_opt(iqdisc, Opts, fun gen_iq_handler:check_type/1, one_queue), - case gen_mod:db_type(Host, Opts) of - mnesia -> - mnesia:create_table(privacy, - [{disc_copies, [node()]}, - {attributes, record_info(fields, privacy)}]), - update_table(); - _ -> ok - end, + Mod = gen_mod:db_mod(Host, Opts, ?MODULE), + Mod:init(Host, Opts), mod_disco:register_feature(Host, ?NS_PRIVACY), ejabberd_hooks:add(privacy_iq_get, Host, ?MODULE, process_iq_get, 50), @@ -104,27 +100,28 @@ process_iq(_From, _To, IQ) -> SubEl = IQ#iq.sub_el, IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}. -process_iq_get(_, From, _To, #iq{sub_el = SubEl}, +process_iq_get(_, From, _To, #iq{lang = Lang, sub_el = SubEl}, #userlist{name = Active}) -> #jid{luser = LUser, lserver = LServer} = From, #xmlel{children = Els} = SubEl, - case xml:remove_cdata(Els) of - [] -> process_lists_get(LUser, LServer, Active); + case fxml:remove_cdata(Els) of + [] -> process_lists_get(LUser, LServer, Active, Lang); [#xmlel{name = Name, attrs = Attrs}] -> case Name of <<"list">> -> - ListName = xml:get_attr(<<"name">>, Attrs), - process_list_get(LUser, LServer, ListName); - _ -> {error, ?ERR_BAD_REQUEST} + ListName = fxml:get_attr(<<"name">>, Attrs), + process_list_get(LUser, LServer, ListName, Lang); + _ -> + Txt = <<"Unsupported tag name">>, + {error, ?ERRT_BAD_REQUEST(Lang, Txt)} end; - _ -> {error, ?ERR_BAD_REQUEST} + _ -> {error, ?ERRT_BAD_REQUEST(Lang, <<"Too many elements">>)} end. -process_lists_get(LUser, LServer, Active) -> - case process_lists_get(LUser, LServer, Active, - gen_mod:db_type(LServer, ?MODULE)) - of - error -> {error, ?ERR_INTERNAL_SERVER_ERROR}; +process_lists_get(LUser, LServer, Active, Lang) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + case Mod:process_lists_get(LUser, LServer) of + error -> {error, ?ERRT_INTERNAL_SERVER_ERROR(Lang, <<"Database failure">>)}; {_Default, []} -> {result, [#xmlel{name = <<"query">>, @@ -150,60 +147,10 @@ process_lists_get(LUser, LServer, Active) -> children = ADItems}]} end. -process_lists_get(LUser, LServer, _Active, mnesia) -> - case catch mnesia:dirty_read(privacy, {LUser, LServer}) - of - {'EXIT', _Reason} -> error; - [] -> {none, []}; - [#privacy{default = Default, lists = Lists}] -> - LItems = lists:map(fun ({N, _}) -> - #xmlel{name = <<"list">>, - attrs = [{<<"name">>, N}], - children = []} - end, - Lists), - {Default, LItems} - end; -process_lists_get(LUser, LServer, _Active, riak) -> - case ejabberd_riak:get(privacy, privacy_schema(), {LUser, LServer}) of - {ok, #privacy{default = Default, lists = Lists}} -> - LItems = lists:map(fun ({N, _}) -> - #xmlel{name = <<"list">>, - attrs = [{<<"name">>, N}], - children = []} - end, - Lists), - {Default, LItems}; - {error, notfound} -> - {none, []}; - {error, _} -> - 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; - _ -> none - end, - case catch sql_get_privacy_list_names(LUser, LServer) of - {selected, [<<"name">>], Names} -> - LItems = lists:map(fun ([N]) -> - #xmlel{name = <<"list">>, - attrs = [{<<"name">>, N}], - children = []} - end, - Names), - {Default, LItems}; - _ -> error - end. - -process_list_get(LUser, LServer, {value, Name}) -> - case process_list_get(LUser, LServer, Name, - gen_mod:db_type(LServer, ?MODULE)) - of - error -> {error, ?ERR_INTERNAL_SERVER_ERROR}; +process_list_get(LUser, LServer, {value, Name}, Lang) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + case Mod:process_list_get(LUser, LServer, Name) of + error -> {error, ?ERRT_INTERNAL_SERVER_ERROR(Lang, <<"Database failure">>)}; not_found -> {error, ?ERR_ITEM_NOT_FOUND}; Items -> LItems = lists:map(fun item_to_xml/1, Items), @@ -214,50 +161,9 @@ process_list_get(LUser, LServer, {value, Name}) -> [#xmlel{name = <<"list">>, attrs = [{<<"name">>, Name}], children = LItems}]}]} end; -process_list_get(_LUser, _LServer, false) -> +process_list_get(_LUser, _LServer, false, _Lang) -> {error, ?ERR_BAD_REQUEST}. -process_list_get(LUser, LServer, Name, mnesia) -> - case catch mnesia:dirty_read(privacy, {LUser, LServer}) - of - {'EXIT', _Reason} -> error; - [] -> not_found; - [#privacy{lists = Lists}] -> - case lists:keysearch(Name, 1, Lists) of - {value, {_, List}} -> List; - _ -> not_found - end - end; -process_list_get(LUser, LServer, Name, riak) -> - case ejabberd_riak:get(privacy, privacy_schema(), {LUser, LServer}) of - {ok, #privacy{lists = Lists}} -> - case lists:keysearch(Name, 1, Lists) of - {value, {_, List}} -> List; - _ -> not_found - end; - {error, notfound} -> - not_found; - {error, _} -> - 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} -> - lists:flatmap(fun raw_to_item/1, RItems); - _ -> error - end; - _ -> error - end. - item_to_xml(Item) -> Attrs1 = [{<<"action">>, action_to_list(Item#listitem.action)}, @@ -339,109 +245,41 @@ list_to_action(S) -> <<"deny">> -> deny end. -process_iq_set(_, From, _To, #iq{sub_el = SubEl}) -> +process_iq_set(_, From, _To, #iq{lang = Lang, 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), Lang); <<"active">> -> process_active_set(LUser, LServer, ListName); <<"default">> -> - process_default_set(LUser, LServer, ListName); - _ -> {error, ?ERR_BAD_REQUEST} + process_default_set(LUser, LServer, ListName, Lang); + _ -> + Txt = <<"Unsupported tag name">>, + {error, ?ERRT_BAD_REQUEST(Lang, Txt)} end; _ -> {error, ?ERR_BAD_REQUEST} end. -process_default_set(LUser, LServer, Value) -> - case process_default_set(LUser, LServer, Value, - gen_mod:db_type(LServer, ?MODULE)) - of - {atomic, error} -> {error, ?ERR_INTERNAL_SERVER_ERROR}; +process_default_set(LUser, LServer, Value, Lang) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + case Mod:process_default_set(LUser, LServer, Value) of + {atomic, error} -> + {error, ?ERRT_INTERNAL_SERVER_ERROR(Lang, <<"Database failure">>)}; {atomic, not_found} -> {error, ?ERR_ITEM_NOT_FOUND}; {atomic, ok} -> {result, []}; _ -> {error, ?ERR_INTERNAL_SERVER_ERROR} end. -process_default_set(LUser, LServer, {value, Name}, - mnesia) -> - F = fun () -> - case mnesia:read({privacy, {LUser, LServer}}) of - [] -> not_found; - [#privacy{lists = Lists} = P] -> - case lists:keymember(Name, 1, Lists) of - true -> - mnesia:write(P#privacy{default = Name, - lists = Lists}), - ok; - false -> not_found - end - end - end, - mnesia:transaction(F); -process_default_set(LUser, LServer, {value, Name}, riak) -> - {atomic, - case ejabberd_riak:get(privacy, privacy_schema(), {LUser, LServer}) of - {ok, #privacy{lists = Lists} = P} -> - case lists:keymember(Name, 1, Lists) of - true -> - ejabberd_riak:put(P#privacy{default = Name, - lists = Lists}, - privacy_schema()); - false -> - not_found - end; - {error, _} -> - not_found - end}; -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 - true -> sql_set_default_privacy_list(LUser, Name), ok; - false -> not_found - end - end - end, - odbc_queries:sql_transaction(LServer, F); -process_default_set(LUser, LServer, false, mnesia) -> - F = fun () -> - case mnesia:read({privacy, {LUser, LServer}}) of - [] -> ok; - [R] -> mnesia:write(R#privacy{default = none}) - end - end, - mnesia:transaction(F); -process_default_set(LUser, LServer, false, riak) -> - {atomic, - case ejabberd_riak:get(privacy, privacy_schema(), {LUser, LServer}) of - {ok, R} -> - ejabberd_riak:put(R#privacy{default = none}, privacy_schema()); - {error, _} -> - ok - end}; -process_default_set(LUser, LServer, false, odbc) -> - case catch sql_unset_default_privacy_list(LUser, - LServer) - of - {'EXIT', _Reason} -> {atomic, error}; - {error, _Reason} -> {atomic, error}; - _ -> {atomic, ok} - end. - process_active_set(LUser, LServer, {value, Name}) -> - case process_active_set(LUser, LServer, Name, - gen_mod:db_type(LServer, ?MODULE)) - of + Mod = gen_mod:db_mod(LServer, ?MODULE), + case Mod:process_active_set(LUser, LServer, Name) of error -> {error, ?ERR_ITEM_NOT_FOUND}; Items -> NeedDb = is_list_needdb(Items), @@ -451,135 +289,19 @@ process_active_set(LUser, LServer, {value, Name}) -> process_active_set(_LUser, _LServer, false) -> {result, [], #userlist{}}. -process_active_set(LUser, LServer, Name, mnesia) -> - case catch mnesia:dirty_read(privacy, {LUser, LServer}) - of - [] -> error; - [#privacy{lists = Lists}] -> - case lists:keysearch(Name, 1, Lists) of - {value, {_, List}} -> List; - false -> error - end - end; -process_active_set(LUser, LServer, Name, riak) -> - case ejabberd_riak:get(privacy, privacy_schema(), {LUser, LServer}) of - {ok, #privacy{lists = Lists}} -> - case lists:keysearch(Name, 1, Lists) of - {value, {_, List}} -> List; - false -> error - end; - {error, _} -> - 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} -> - lists:flatmap(fun raw_to_item/1, RItems); - _ -> error - end; - _ -> error - end. +set_privacy_list(#privacy{us = {_, LServer}} = Privacy) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:set_privacy_list(Privacy). -remove_privacy_list(LUser, LServer, Name, mnesia) -> - F = fun () -> - case mnesia:read({privacy, {LUser, LServer}}) of - [] -> ok; - [#privacy{default = Default, lists = Lists} = P] -> - if Name == Default -> conflict; - true -> - NewLists = lists:keydelete(Name, 1, Lists), - mnesia:write(P#privacy{lists = NewLists}) - end - end - end, - mnesia:transaction(F); -remove_privacy_list(LUser, LServer, Name, riak) -> - {atomic, - case ejabberd_riak:get(privacy, privacy_schema(), {LUser, LServer}) of - {ok, #privacy{default = Default, lists = Lists} = P} -> - if Name == Default -> - conflict; - true -> - NewLists = lists:keydelete(Name, 1, Lists), - ejabberd_riak:put(P#privacy{lists = NewLists}, - privacy_schema()) - end; - {error, _} -> - ok - end}; -remove_privacy_list(LUser, LServer, Name, odbc) -> - F = fun () -> - case sql_get_default_privacy_list_t(LUser) of - {selected, [<<"name">>], []} -> - sql_remove_privacy_list(LUser, Name), ok; - {selected, [<<"name">>], [[Default]]} -> - if Name == Default -> conflict; - true -> sql_remove_privacy_list(LUser, Name), ok - end - end - end, - odbc_queries:sql_transaction(LServer, F). - -set_privacy_list(LUser, LServer, Name, List, mnesia) -> - F = fun () -> - case mnesia:wread({privacy, {LUser, LServer}}) of - [] -> - NewLists = [{Name, List}], - mnesia:write(#privacy{us = {LUser, LServer}, - lists = NewLists}); - [#privacy{lists = Lists} = P] -> - NewLists1 = lists:keydelete(Name, 1, Lists), - NewLists = [{Name, List} | NewLists1], - mnesia:write(P#privacy{lists = NewLists}) - end - end, - mnesia:transaction(F); -set_privacy_list(LUser, LServer, Name, List, riak) -> - {atomic, - case ejabberd_riak:get(privacy, privacy_schema(), {LUser, LServer}) of - {ok, #privacy{lists = Lists} = P} -> - NewLists1 = lists:keydelete(Name, 1, Lists), - NewLists = [{Name, List} | NewLists1], - ejabberd_riak:put(P#privacy{lists = NewLists}, privacy_schema()); - {error, _} -> - NewLists = [{Name, List}], - ejabberd_riak:put(#privacy{us = {LUser, LServer}, - lists = NewLists}, - privacy_schema()) - end}; -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">>], []} -> - sql_add_privacy_list(LUser, Name), - {selected, [<<"id">>], [[I]]} = - sql_get_privacy_list_id_t(LUser, Name), - I; - {selected, [<<"id">>], [[I]]} -> I - end, - sql_set_privacy_list(ID, RItems), - ok - end, - odbc_queries:sql_transaction(LServer, F). - -process_list_set(LUser, LServer, {value, Name}, Els) -> +process_list_set(LUser, LServer, {value, Name}, Els, Lang) -> case parse_items(Els) of false -> {error, ?ERR_BAD_REQUEST}; remove -> - case remove_privacy_list(LUser, LServer, Name, - gen_mod:db_type(LServer, ?MODULE)) - of - {atomic, conflict} -> {error, ?ERR_CONFLICT}; + Mod = gen_mod:db_mod(LServer, ?MODULE), + case Mod:remove_privacy_list(LUser, LServer, Name) of + {atomic, conflict} -> + Txt = <<"Cannot remove default list">>, + {error, ?ERRT_CONFLICT(Lang, Txt)}; {atomic, ok} -> ejabberd_sm:route(jid:make(LUser, LServer, <<"">>), @@ -589,12 +311,11 @@ process_list_set(LUser, LServer, {value, Name}, Els) -> list = []}, Name}}), {result, []}; - _ -> {error, ?ERR_INTERNAL_SERVER_ERROR} + _ -> {error, ?ERRT_INTERNAL_SERVER_ERROR(Lang, <<"Database failure">>)} end; List -> - case set_privacy_list(LUser, LServer, Name, List, - gen_mod:db_type(LServer, ?MODULE)) - of + Mod = gen_mod:db_mod(LServer, ?MODULE), + case Mod:set_privacy_list(LUser, LServer, Name, List) of {atomic, ok} -> NeedDb = is_list_needdb(List), ejabberd_sm:route(jid:make(LUser, LServer, @@ -606,10 +327,10 @@ process_list_set(LUser, LServer, {value, Name}, Els) -> needdb = NeedDb}, Name}}), {result, []}; - _ -> {error, ?ERR_INTERNAL_SERVER_ERROR} + _ -> {error, ?ERRT_INTERNAL_SERVER_ERROR(Lang, <<"Database failure">>)} end end; -process_list_set(_LUser, _LServer, false, _Els) -> +process_list_set(_LUser, _LServer, false, _Els, _Lang) -> {error, ?ERR_BAD_REQUEST}. parse_items([]) -> remove; @@ -621,10 +342,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; @@ -674,7 +395,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 @@ -714,115 +435,20 @@ is_list_needdb(Items) -> end, Items). -get_user_list(Acc, User, Server) -> +get_user_list(_Acc, User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), - {Default, Items} = get_user_list(Acc, LUser, LServer, - gen_mod:db_type(LServer, ?MODULE)), + Mod = gen_mod:db_mod(LServer, ?MODULE), + {Default, Items} = Mod:get_user_list(LUser, LServer), NeedDb = is_list_needdb(Items), #userlist{name = Default, list = Items, needdb = NeedDb}. -get_user_list(_, LUser, LServer, mnesia) -> - case catch mnesia:dirty_read(privacy, {LUser, LServer}) - of - [] -> {none, []}; - [#privacy{default = Default, lists = Lists}] -> - case Default of - none -> {none, []}; - _ -> - case lists:keysearch(Default, 1, Lists) of - {value, {_, List}} -> {Default, List}; - _ -> {none, []} - end - end; - _ -> {none, []} - end; -get_user_list(_, LUser, LServer, riak) -> - case ejabberd_riak:get(privacy, privacy_schema(), {LUser, LServer}) of - {ok, #privacy{default = Default, lists = Lists}} -> - case Default of - none -> {none, []}; - _ -> - case lists:keysearch(Default, 1, Lists) of - {value, {_, List}} -> {Default, List}; - _ -> {none, []} - end - end; - {error, _} -> - {none, []} - end; -get_user_list(_, LUser, LServer, odbc) -> - case catch sql_get_default_privacy_list(LUser, LServer) - of - {selected, [<<"name">>], []} -> {none, []}; - {selected, [<<"name">>], [[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, lists:flatmap(fun raw_to_item/1, RItems)}; - _ -> {none, []} - end; - _ -> {none, []} - end. - get_user_lists(User, 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) -> - case catch mnesia:dirty_read(privacy, {LUser, LServer}) of - [#privacy{} = P] -> - {ok, P}; - _ -> - error - end; -get_user_lists(LUser, LServer, riak) -> - case ejabberd_riak:get(privacy, privacy_schema(), {LUser, LServer}) of - {ok, #privacy{} = P} -> - {ok, P}; - {error, _} -> - error - end; -get_user_lists(LUser, LServer, odbc) -> - Default = case catch sql_get_default_privacy_list(LUser, LServer) of - {selected, [<<"name">>], []} -> - none; - {selected, [<<"name">>], [[DefName]]} -> - DefName; - _ -> - none - end, - case catch sql_get_privacy_list_names(LUser, LServer) of - {selected, [<<"name">>], Names} -> - Lists = - lists:flatmap( - 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} -> - [{Name, lists:flatmap(fun raw_to_item/1, RItems)}]; - _ -> - [] - end - end, Names), - {ok, #privacy{default = Default, - us = {LUser, LServer}, - lists = Lists}}; - _ -> - error - end. + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:get_user_lists(LUser, LServer). %% From is the sender, To is the destination. %% If Dir = out, User@Server is the sender account (From). @@ -852,7 +478,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; @@ -946,17 +572,8 @@ is_type_match(Type, Value, JID, Subscription, Groups) -> 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) -> - F = fun () -> mnesia:delete({privacy, {LUser, LServer}}) - end, - mnesia:transaction(F); -remove_user(LUser, LServer, riak) -> - {atomic, ejabberd_riak:delete(privacy, {LUser, LServer})}; -remove_user(LUser, LServer, odbc) -> - sql_del_privacy_lists(LUser, LServer). + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:remove_user(LUser, LServer). updated_list(_, #userlist{name = OldName} = Old, #userlist{name = NewName} = New) -> @@ -964,296 +581,17 @@ updated_list(_, #userlist{name = OldName} = Old, true -> Old end. -raw_to_item([SType, SValue, SAction, SOrder, SMatchAll, - SMatchIQ, SMatchMessage, SMatchPresenceIn, - SMatchPresenceOut] = Row) -> - try - {Type, Value} = case SType of - <<"n">> -> {none, none}; - <<"j">> -> - case jid:from_string(SValue) of - #jid{} = JID -> - {jid, jid:tolower(JID)} - end; - <<"g">> -> {group, SValue}; - <<"s">> -> - case SValue of - <<"none">> -> {subscription, none}; - <<"both">> -> {subscription, both}; - <<"from">> -> {subscription, from}; - <<"to">> -> {subscription, to} - end - end, - Action = case SAction of - <<"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, - match_presence_in = MatchPresenceIn, - match_presence_out = MatchPresenceOut}] - catch _:_ -> - ?WARNING_MSG("failed to parse row: ~p", [Row]), - [] - end. - -item_to_raw(#listitem{type = Type, value = Value, - action = Action, order = Order, match_all = MatchAll, - match_iq = MatchIQ, match_message = MatchMessage, - match_presence_in = MatchPresenceIn, - match_presence_out = MatchPresenceOut}) -> - {SType, SValue} = case Type of - none -> {<<"n">>, <<"">>}; - jid -> - {<<"j">>, - ejabberd_odbc:escape(jid:to_string(Value))}; - group -> {<<"g">>, ejabberd_odbc:escape(Value)}; - subscription -> - case Value of - none -> {<<"s">>, <<"none">>}; - both -> {<<"s">>, <<"both">>}; - from -> {<<"s">>, <<"from">>}; - to -> {<<"s">>, <<"to">>} - end - end, - SAction = case Action of - 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]. - -sql_get_default_privacy_list(LUser, LServer) -> - Username = ejabberd_odbc:escape(LUser), - odbc_queries:get_default_privacy_list(LServer, - Username). - -sql_get_default_privacy_list_t(LUser) -> - Username = ejabberd_odbc:escape(LUser), - odbc_queries:get_default_privacy_list_t(Username). - -sql_get_privacy_list_names(LUser, LServer) -> - Username = ejabberd_odbc:escape(LUser), - odbc_queries:get_privacy_list_names(LServer, Username). - -sql_get_privacy_list_names_t(LUser) -> - Username = ejabberd_odbc:escape(LUser), - odbc_queries:get_privacy_list_names_t(Username). - -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). - -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). - -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). - -sql_get_privacy_list_data_t(LUser, Name) -> - Username = ejabberd_odbc:escape(LUser), - SName = ejabberd_odbc:escape(Name), - odbc_queries:get_privacy_list_data_t(Username, SName). - -sql_get_privacy_list_data_by_id(ID, LServer) -> - odbc_queries:get_privacy_list_data_by_id(LServer, ID). - -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). - -sql_unset_default_privacy_list(LUser, LServer) -> - Username = ejabberd_odbc:escape(LUser), - odbc_queries:unset_default_privacy_list(LServer, - Username). - -sql_remove_privacy_list(LUser, Name) -> - Username = ejabberd_odbc:escape(LUser), - SName = ejabberd_odbc:escape(Name), - odbc_queries:remove_privacy_list(Username, SName). - -sql_add_privacy_list(LUser, Name) -> - Username = ejabberd_odbc:escape(LUser), - SName = ejabberd_odbc:escape(Name), - odbc_queries:add_privacy_list(Username, SName). - -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). - -update_table() -> - Fields = record_info(fields, privacy), - case mnesia:table_info(privacy, attributes) of - Fields -> - ejabberd_config:convert_table_to_binary( - privacy, Fields, set, - fun(#privacy{us = {U, _}}) -> U end, - fun(#privacy{us = {U, S}, default = Def, lists = Lists} = R) -> - NewLists = - lists:map( - fun({Name, Ls}) -> - NewLs = - lists:map( - fun(#listitem{value = Val} = L) -> - NewVal = - case Val of - {LU, LS, LR} -> - {iolist_to_binary(LU), - iolist_to_binary(LS), - iolist_to_binary(LR)}; - none -> none; - both -> both; - from -> from; - to -> to; - _ -> iolist_to_binary(Val) - end, - L#listitem{value = NewVal} - end, Ls), - {iolist_to_binary(Name), NewLs} - end, Lists), - NewDef = case Def of - none -> none; - _ -> iolist_to_binary(Def) - end, - NewUS = {iolist_to_binary(U), iolist_to_binary(S)}, - R#privacy{us = NewUS, default = NewDef, - lists = NewLists} - end); - _ -> - ?INFO_MSG("Recreating privacy table", []), - mnesia:transform_table(privacy, ignore, Fields) - end. - -export(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]]} -> - put(id, jlib:binary_to_integer(I)); - _ -> - put(id, 0) - end, - [{privacy, - fun(Host, #privacy{us = {LUser, LServer}, lists = Lists, - default = Default}) - when LServer == Host -> - Username = ejabberd_odbc:escape(LUser), - if Default /= none -> - SDefault = ejabberd_odbc:escape(Default), - [[<<"delete from privacy_default_list where ">>, - <<"username='">>, Username, <<"';">>], - [<<"insert into privacy_default_list(username, " - "name) ">>, - <<"values ('">>, Username, <<"', '">>, - SDefault, <<"');">>]]; - true -> - [] - end ++ - lists:flatmap( - fun({Name, List}) -> - SName = ejabberd_odbc:escape(Name), - RItems = lists:map(fun item_to_raw/1, List), - ID = jlib:integer_to_binary(get_id()), - [[<<"delete from privacy_list where username='">>, - Username, <<"' and name='">>, - SName, <<"';">>], - [<<"insert into privacy_list(username, " - "name, id) values ('">>, - Username, <<"', '">>, SName, - <<"', '">>, ID, <<"');">>], - [<<"delete from privacy_list_data where " - "id='">>, ID, <<"';">>]] ++ - [[<<"insert into privacy_list_data(id, t, " - "value, action, ord, match_all, match_iq, " - "match_message, match_presence_in, " - "match_presence_out) values ('">>, - ID, <<"', '">>, str:join(Items, <<"', '">>), - <<"');">>] || Items <- RItems] - end, - Lists); - (_Host, _R) -> - [] - end}]. - -get_id() -> - ID = get(id), - put(id, ID + 1), - ID + 1. +export(LServer) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:export(LServer). import(LServer) -> - [{<<"select username from privacy_list;">>, - fun([LUser]) -> - Default = case sql_get_default_privacy_list_t(LUser) of - {selected, [<<"name">>], []} -> - none; - {selected, [<<"name">>], [[DefName]]} -> - DefName; - _ -> - none - end, - {selected, [<<"name">>], Names} = - sql_get_privacy_list_names_t(LUser), - Lists = lists:flatmap( - fun([Name]) -> - case sql_get_privacy_list_data_t(LUser, Name) of - {selected, _, RItems} -> - [{Name, - lists:map(fun raw_to_item/1, - RItems)}]; - _ -> - [] - end - end, Names), - #privacy{default = Default, - us = {LUser, LServer}, - lists = Lists} - end}]. + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:import(LServer). -import(_LServer, mnesia, #privacy{} = P) -> - mnesia:dirty_write(P); -import(_LServer, riak, #privacy{} = P) -> - ejabberd_riak:put(P, privacy_schema()); -import(_, _, _) -> - pass. +import(LServer, DBType, Data) -> + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:import(LServer, Data). mod_opt_type(db_type) -> fun gen_mod:v_db/1; mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; diff --git a/src/mod_privacy_mnesia.erl b/src/mod_privacy_mnesia.erl new file mode 100644 index 000000000..4026b7f64 --- /dev/null +++ b/src/mod_privacy_mnesia.erl @@ -0,0 +1,198 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 14 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_privacy_mnesia). + +-behaviour(mod_privacy). + +%% API +-export([init/2, process_lists_get/2, process_list_get/3, + process_default_set/3, process_active_set/3, + remove_privacy_list/3, set_privacy_list/1, + set_privacy_list/4, get_user_list/2, get_user_lists/2, + remove_user/2, import/2]). + +-include("jlib.hrl"). +-include("mod_privacy.hrl"). +-include("logger.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + mnesia:create_table(privacy, + [{disc_copies, [node()]}, + {attributes, record_info(fields, privacy)}]), + update_table(). + +process_lists_get(LUser, LServer) -> + case catch mnesia:dirty_read(privacy, {LUser, LServer}) of + {'EXIT', _Reason} -> error; + [] -> {none, []}; + [#privacy{default = Default, lists = Lists}] -> + LItems = lists:map(fun ({N, _}) -> + #xmlel{name = <<"list">>, + attrs = [{<<"name">>, N}], + children = []} + end, Lists), + {Default, LItems} + end. + +process_list_get(LUser, LServer, Name) -> + case catch mnesia:dirty_read(privacy, {LUser, LServer}) of + {'EXIT', _Reason} -> error; + [] -> not_found; + [#privacy{lists = Lists}] -> + case lists:keysearch(Name, 1, Lists) of + {value, {_, List}} -> List; + _ -> not_found + end + end. + +process_default_set(LUser, LServer, {value, Name}) -> + F = fun () -> + case mnesia:read({privacy, {LUser, LServer}}) of + [] -> not_found; + [#privacy{lists = Lists} = P] -> + case lists:keymember(Name, 1, Lists) of + true -> + mnesia:write(P#privacy{default = Name, + lists = Lists}), + ok; + false -> not_found + end + end + end, + mnesia:transaction(F); +process_default_set(LUser, LServer, false) -> + F = fun () -> + case mnesia:read({privacy, {LUser, LServer}}) of + [] -> ok; + [R] -> mnesia:write(R#privacy{default = none}) + end + end, + mnesia:transaction(F). + +process_active_set(LUser, LServer, Name) -> + case catch mnesia:dirty_read(privacy, {LUser, LServer}) of + [] -> error; + [#privacy{lists = Lists}] -> + case lists:keysearch(Name, 1, Lists) of + {value, {_, List}} -> List; + false -> error + end + end. + +remove_privacy_list(LUser, LServer, Name) -> + F = fun () -> + case mnesia:read({privacy, {LUser, LServer}}) of + [] -> ok; + [#privacy{default = Default, lists = Lists} = P] -> + if Name == Default -> conflict; + true -> + NewLists = lists:keydelete(Name, 1, Lists), + mnesia:write(P#privacy{lists = NewLists}) + end + end + end, + mnesia:transaction(F). + +set_privacy_list(Privacy) -> + mnesia:dirty_write(Privacy). + +set_privacy_list(LUser, LServer, Name, List) -> + F = fun () -> + case mnesia:wread({privacy, {LUser, LServer}}) of + [] -> + NewLists = [{Name, List}], + mnesia:write(#privacy{us = {LUser, LServer}, + lists = NewLists}); + [#privacy{lists = Lists} = P] -> + NewLists1 = lists:keydelete(Name, 1, Lists), + NewLists = [{Name, List} | NewLists1], + mnesia:write(P#privacy{lists = NewLists}) + end + end, + mnesia:transaction(F). + +get_user_list(LUser, LServer) -> + case catch mnesia:dirty_read(privacy, {LUser, LServer}) + of + [] -> {none, []}; + [#privacy{default = Default, lists = Lists}] -> + case Default of + none -> {none, []}; + _ -> + case lists:keysearch(Default, 1, Lists) of + {value, {_, List}} -> {Default, List}; + _ -> {none, []} + end + end; + _ -> {none, []} + end. + +get_user_lists(LUser, LServer) -> + case catch mnesia:dirty_read(privacy, {LUser, LServer}) of + [#privacy{} = P] -> + {ok, P}; + _ -> + error + end. + +remove_user(LUser, LServer) -> + F = fun () -> mnesia:delete({privacy, {LUser, LServer}}) end, + mnesia:transaction(F). + +import(_LServer, #privacy{} = P) -> + mnesia:dirty_write(P). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +update_table() -> + Fields = record_info(fields, privacy), + case mnesia:table_info(privacy, attributes) of + Fields -> + ejabberd_config:convert_table_to_binary( + privacy, Fields, set, + fun(#privacy{us = {U, _}}) -> U end, + fun(#privacy{us = {U, S}, default = Def, lists = Lists} = R) -> + NewLists = + lists:map( + fun({Name, Ls}) -> + NewLs = + lists:map( + fun(#listitem{value = Val} = L) -> + NewVal = + case Val of + {LU, LS, LR} -> + {iolist_to_binary(LU), + iolist_to_binary(LS), + iolist_to_binary(LR)}; + none -> none; + both -> both; + from -> from; + to -> to; + _ -> iolist_to_binary(Val) + end, + L#listitem{value = NewVal} + end, Ls), + {iolist_to_binary(Name), NewLs} + end, Lists), + NewDef = case Def of + none -> none; + _ -> iolist_to_binary(Def) + end, + NewUS = {iolist_to_binary(U), iolist_to_binary(S)}, + R#privacy{us = NewUS, default = NewDef, + lists = NewLists} + end); + _ -> + ?INFO_MSG("Recreating privacy table", []), + mnesia:transform_table(privacy, ignore, Fields) + end. diff --git a/src/mod_privacy_riak.erl b/src/mod_privacy_riak.erl new file mode 100644 index 000000000..0c43e74f9 --- /dev/null +++ b/src/mod_privacy_riak.erl @@ -0,0 +1,160 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 14 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_privacy_riak). + +-behaviour(mod_privacy). + +%% API +-export([init/2, process_lists_get/2, process_list_get/3, + process_default_set/3, process_active_set/3, + remove_privacy_list/3, set_privacy_list/1, + set_privacy_list/4, get_user_list/2, get_user_lists/2, + remove_user/2, import/2]). + +-export([privacy_schema/0]). + +-include("jlib.hrl"). +-include("mod_privacy.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ok. + +process_lists_get(LUser, LServer) -> + case ejabberd_riak:get(privacy, privacy_schema(), {LUser, LServer}) of + {ok, #privacy{default = Default, lists = Lists}} -> + LItems = lists:map(fun ({N, _}) -> + #xmlel{name = <<"list">>, + attrs = [{<<"name">>, N}], + children = []} + end, + Lists), + {Default, LItems}; + {error, notfound} -> + {none, []}; + {error, _} -> + error + end. + +process_list_get(LUser, LServer, Name) -> + case ejabberd_riak:get(privacy, privacy_schema(), {LUser, LServer}) of + {ok, #privacy{lists = Lists}} -> + case lists:keysearch(Name, 1, Lists) of + {value, {_, List}} -> List; + _ -> not_found + end; + {error, notfound} -> + not_found; + {error, _} -> + error + end. + +process_default_set(LUser, LServer, {value, Name}) -> + {atomic, + case ejabberd_riak:get(privacy, privacy_schema(), {LUser, LServer}) of + {ok, #privacy{lists = Lists} = P} -> + case lists:keymember(Name, 1, Lists) of + true -> + ejabberd_riak:put(P#privacy{default = Name, + lists = Lists}, + privacy_schema()); + false -> + not_found + end; + {error, _} -> + not_found + end}; +process_default_set(LUser, LServer, false) -> + {atomic, + case ejabberd_riak:get(privacy, privacy_schema(), {LUser, LServer}) of + {ok, R} -> + ejabberd_riak:put(R#privacy{default = none}, privacy_schema()); + {error, _} -> + ok + end}. + +process_active_set(LUser, LServer, Name) -> + case ejabberd_riak:get(privacy, privacy_schema(), {LUser, LServer}) of + {ok, #privacy{lists = Lists}} -> + case lists:keysearch(Name, 1, Lists) of + {value, {_, List}} -> List; + false -> error + end; + {error, _} -> + error + end. + +remove_privacy_list(LUser, LServer, Name) -> + {atomic, + case ejabberd_riak:get(privacy, privacy_schema(), {LUser, LServer}) of + {ok, #privacy{default = Default, lists = Lists} = P} -> + if Name == Default -> + conflict; + true -> + NewLists = lists:keydelete(Name, 1, Lists), + ejabberd_riak:put(P#privacy{lists = NewLists}, + privacy_schema()) + end; + {error, _} -> + ok + end}. + +set_privacy_list(Privacy) -> + ejabberd_riak:put(Privacy, privacy_schema()). + +set_privacy_list(LUser, LServer, Name, List) -> + {atomic, + case ejabberd_riak:get(privacy, privacy_schema(), {LUser, LServer}) of + {ok, #privacy{lists = Lists} = P} -> + NewLists1 = lists:keydelete(Name, 1, Lists), + NewLists = [{Name, List} | NewLists1], + ejabberd_riak:put(P#privacy{lists = NewLists}, privacy_schema()); + {error, _} -> + NewLists = [{Name, List}], + ejabberd_riak:put(#privacy{us = {LUser, LServer}, + lists = NewLists}, + privacy_schema()) + end}. + +get_user_list(LUser, LServer) -> + case ejabberd_riak:get(privacy, privacy_schema(), {LUser, LServer}) of + {ok, #privacy{default = Default, lists = Lists}} -> + case Default of + none -> {none, []}; + _ -> + case lists:keysearch(Default, 1, Lists) of + {value, {_, List}} -> {Default, List}; + _ -> {none, []} + end + end; + {error, _} -> + {none, []} + end. + +get_user_lists(LUser, LServer) -> + case ejabberd_riak:get(privacy, privacy_schema(), {LUser, LServer}) of + {ok, #privacy{} = P} -> + {ok, P}; + {error, _} -> + error + end. + +remove_user(LUser, LServer) -> + {atomic, ejabberd_riak:delete(privacy, {LUser, LServer})}. + +import(_LServer, #privacy{} = P) -> + ejabberd_riak:put(P, privacy_schema()). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +privacy_schema() -> + {record_info(fields, privacy), #privacy{}}. diff --git a/src/mod_privacy_sql.erl b/src/mod_privacy_sql.erl new file mode 100644 index 000000000..6b996fa8b --- /dev/null +++ b/src/mod_privacy_sql.erl @@ -0,0 +1,397 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 14 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_privacy_sql). + +-behaviour(mod_privacy). + +%% API +-export([init/2, process_lists_get/2, process_list_get/3, + process_default_set/3, process_active_set/3, + remove_privacy_list/3, set_privacy_list/1, + set_privacy_list/4, get_user_list/2, get_user_lists/2, + remove_user/2, import/1, import/2, export/1]). + +-export([item_to_raw/1, raw_to_item/1, + 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]). + +-include("jlib.hrl"). +-include("mod_privacy.hrl"). +-include("logger.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ok. + +process_lists_get(LUser, LServer) -> + 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, Names} -> + LItems = lists:map(fun ({N}) -> + #xmlel{name = <<"list">>, + attrs = [{<<"name">>, N}], + children = []} + end, + Names), + {Default, LItems}; + _ -> error + end. + +process_list_get(LUser, LServer, Name) -> + 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; + _ -> error + end. + +process_default_set(LUser, LServer, {value, Name}) -> + F = fun () -> + case sql_get_privacy_list_names_t(LUser) 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 + end + end, + sql_queries:sql_transaction(LServer, F); +process_default_set(LUser, LServer, false) -> + case catch sql_unset_default_privacy_list(LUser, + LServer) + of + {'EXIT', _Reason} -> {atomic, error}; + {error, _Reason} -> {atomic, error}; + _ -> {atomic, ok} + end. + +process_active_set(LUser, LServer, Name) -> + 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; + _ -> error + end. + +remove_privacy_list(LUser, LServer, Name) -> + F = fun () -> + case sql_get_default_privacy_list_t(LUser) of + {selected, []} -> + sql_remove_privacy_list(LUser, Name), ok; + {selected, [{Default}]} -> + if Name == Default -> conflict; + true -> sql_remove_privacy_list(LUser, Name), ok + end + end + end, + sql_queries:sql_transaction(LServer, F). + +set_privacy_list(#privacy{us = {LUser, LServer}, + default = Default, + lists = Lists}) -> + 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, + sql_queries:sql_transaction(LServer, F). + +set_privacy_list(LUser, LServer, Name, List) -> + RItems = lists:map(fun item_to_raw/1, List), + F = fun () -> + ID = case sql_get_privacy_list_id_t(LUser, Name) of + {selected, []} -> + sql_add_privacy_list(LUser, Name), + {selected, [{I}]} = + sql_get_privacy_list_id_t(LUser, Name), + I; + {selected, [{I}]} -> I + end, + sql_set_privacy_list(ID, RItems), + ok + end, + sql_queries:sql_transaction(LServer, F). + +get_user_list(LUser, LServer) -> + case catch sql_get_default_privacy_list(LUser, LServer) + of + {selected, []} -> {none, []}; + {selected, [{Default}]} -> + case catch sql_get_privacy_list_data(LUser, LServer, + Default) of + {selected, RItems} -> + {Default, lists:flatmap(fun raw_to_item/1, RItems)}; + _ -> {none, []} + end; + _ -> {none, []} + end. + +get_user_lists(LUser, LServer) -> + 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, Names} -> + Lists = + lists:flatmap( + fun({Name}) -> + case catch sql_get_privacy_list_data( + LUser, LServer, Name) of + {selected, RItems} -> + [{Name, lists:flatmap(fun raw_to_item/1, RItems)}]; + _ -> + [] + end + end, Names), + {ok, #privacy{default = Default, + us = {LUser, LServer}, + lists = Lists}}; + _ -> + error + end. + +remove_user(LUser, LServer) -> + sql_del_privacy_lists(LUser, LServer). + +export(Server) -> + case catch ejabberd_sql:sql_query(jid:nameprep(Server), + [<<"select id from privacy_list order by " + "id desc limit 1;">>]) of + {selected, [<<"id">>], [[I]]} -> + put(id, jlib:binary_to_integer(I)); + _ -> + put(id, 0) + end, + [{privacy, + fun(Host, #privacy{us = {LUser, LServer}, lists = Lists, + default = Default}) + when LServer == Host -> + Username = ejabberd_sql:escape(LUser), + if Default /= none -> + SDefault = ejabberd_sql:escape(Default), + [[<<"delete from privacy_default_list where ">>, + <<"username='">>, Username, <<"';">>], + [<<"insert into privacy_default_list(username, " + "name) ">>, + <<"values ('">>, Username, <<"', '">>, + SDefault, <<"');">>]]; + true -> + [] + end ++ + lists:flatmap( + fun({Name, List}) -> + SName = ejabberd_sql:escape(Name), + RItems = lists:map(fun item_to_raw/1, List), + ID = jlib:integer_to_binary(get_id()), + [[<<"delete from privacy_list where username='">>, + Username, <<"' and name='">>, + SName, <<"';">>], + [<<"insert into privacy_list(username, " + "name, id) values ('">>, + Username, <<"', '">>, SName, + <<"', '">>, ID, <<"');">>], + [<<"delete from privacy_list_data where " + "id='">>, ID, <<"';">>]] ++ + [[<<"insert into privacy_list_data(id, t, " + "value, action, ord, match_all, match_iq, " + "match_message, match_presence_in, " + "match_presence_out) values ('">>, + ID, <<"', '">>, str:join(Items, <<"', '">>), + <<"');">>] || Items <- RItems] + end, + Lists); + (_Host, _R) -> + [] + end}]. + +get_id() -> + ID = get(id), + put(id, ID + 1), + ID + 1. + +import(LServer) -> + [{<<"select username from privacy_list;">>, + fun([LUser]) -> + Default = case sql_get_default_privacy_list_t(LUser) of + {selected, [<<"name">>], []} -> + none; + {selected, [<<"name">>], [[DefName]]} -> + DefName; + _ -> + none + end, + {selected, [<<"name">>], Names} = + sql_get_privacy_list_names_t(LUser), + Lists = lists:flatmap( + fun([Name]) -> + case sql_get_privacy_list_data_t(LUser, Name) of + {selected, _, RItems} -> + [{Name, + lists:map(fun raw_to_item/1, + RItems)}]; + _ -> + [] + end + end, Names), + #privacy{default = Default, + us = {LUser, LServer}, + lists = Lists} + end}]. + +import(_, _) -> + pass. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +raw_to_item({SType, SValue, SAction, Order, MatchAll, + MatchIQ, MatchMessage, MatchPresenceIn, + MatchPresenceOut} = Row) -> + try + {Type, Value} = case SType of + <<"n">> -> {none, none}; + <<"j">> -> + case jid:from_string(SValue) of + #jid{} = JID -> + {jid, jid:tolower(JID)} + end; + <<"g">> -> {group, SValue}; + <<"s">> -> + case SValue of + <<"none">> -> {subscription, none}; + <<"both">> -> {subscription, both}; + <<"from">> -> {subscription, from}; + <<"to">> -> {subscription, to} + end + end, + Action = case SAction of + <<"a">> -> allow; + <<"d">> -> deny + end, + [#listitem{type = Type, value = Value, action = Action, + order = Order, match_all = MatchAll, match_iq = MatchIQ, + match_message = MatchMessage, + match_presence_in = MatchPresenceIn, + match_presence_out = MatchPresenceOut}] + catch _:_ -> + ?WARNING_MSG("failed to parse row: ~p", [Row]), + [] + end. + +item_to_raw(#listitem{type = Type, value = Value, + action = Action, order = Order, match_all = MatchAll, + match_iq = MatchIQ, match_message = MatchMessage, + match_presence_in = MatchPresenceIn, + match_presence_out = MatchPresenceOut}) -> + {SType, SValue} = case Type of + none -> {<<"n">>, <<"">>}; + jid -> + {<<"j">>, + ejabberd_sql:escape(jid:to_string(Value))}; + group -> {<<"g">>, ejabberd_sql:escape(Value)}; + subscription -> + case Value of + none -> {<<"s">>, <<"none">>}; + both -> {<<"s">>, <<"both">>}; + from -> {<<"s">>, <<"from">>}; + to -> {<<"s">>, <<"to">>} + end + end, + SAction = case Action of + allow -> <<"a">>; + deny -> <<"d">> + end, + {SType, SValue, SAction, Order, MatchAll, MatchIQ, + MatchMessage, MatchPresenceIn, MatchPresenceOut}. + +sql_get_default_privacy_list(LUser, LServer) -> + sql_queries:get_default_privacy_list(LServer, LUser). + +sql_get_default_privacy_list_t(LUser) -> + sql_queries:get_default_privacy_list_t(LUser). + +sql_get_privacy_list_names(LUser, LServer) -> + sql_queries:get_privacy_list_names(LServer, LUser). + +sql_get_privacy_list_names_t(LUser) -> + sql_queries:get_privacy_list_names_t(LUser). + +sql_get_privacy_list_id(LUser, LServer, Name) -> + sql_queries:get_privacy_list_id(LServer, LUser, Name). + +sql_get_privacy_list_id_t(LUser, Name) -> + sql_queries:get_privacy_list_id_t(LUser, Name). + +sql_get_privacy_list_data(LUser, LServer, Name) -> + sql_queries:get_privacy_list_data(LServer, LUser, Name). + +sql_get_privacy_list_data_t(LUser, Name) -> + Username = ejabberd_sql:escape(LUser), + SName = ejabberd_sql:escape(Name), + sql_queries:get_privacy_list_data_t(Username, SName). + +sql_get_privacy_list_data_by_id(ID, LServer) -> + sql_queries:get_privacy_list_data_by_id(LServer, ID). + +sql_get_privacy_list_data_by_id_t(ID) -> + sql_queries:get_privacy_list_data_by_id_t(ID). + +sql_set_default_privacy_list(LUser, Name) -> + sql_queries:set_default_privacy_list(LUser, Name). + +sql_unset_default_privacy_list(LUser, LServer) -> + sql_queries:unset_default_privacy_list(LServer, LUser). + +sql_remove_privacy_list(LUser, Name) -> + sql_queries:remove_privacy_list(LUser, Name). + +sql_add_privacy_list(LUser, Name) -> + sql_queries:add_privacy_list(LUser, Name). + +sql_set_privacy_list(ID, RItems) -> + sql_queries:set_privacy_list(ID, RItems). + +sql_del_privacy_lists(LUser, LServer) -> + sql_queries:del_privacy_lists(LServer, LUser). diff --git a/src/mod_private.erl b/src/mod_private.erl index a03d83e5a..029789e63 100644 --- a/src/mod_private.erl +++ b/src/mod_private.erl @@ -33,18 +33,20 @@ -export([start/2, stop/1, process_sm_iq/3, import/3, remove_user/2, get_data/2, export/1, import/1, - mod_opt_type/1]). + mod_opt_type/1, set_data/3]). -include("ejabberd.hrl"). -include("logger.hrl"). -include("jlib.hrl"). - --record(private_storage, - {usns = {<<"">>, <<"">>, <<"">>} :: {binary(), binary(), binary() | - '$1' | '_'}, - xml = #xmlel{} :: xmlel() | '_' | '$1'}). - +-include("mod_private.hrl"). + +-callback init(binary(), gen_mod:opts()) -> any(). +-callback import(binary(), #private_storage{}) -> ok | pass. +-callback set_data(binary(), binary(), [{binary(), xmlel()}]) -> {atomic, any()}. +-callback get_data(binary(), binary(), binary()) -> {ok, xmlel()} | error. +-callback get_all_data(binary(), binary()) -> [xmlel()]. + -define(Xmlel_Query(Attrs, Children), #xmlel{name = <<"query">>, attrs = Attrs, children = Children}). @@ -52,15 +54,8 @@ start(Host, Opts) -> IQDisc = gen_mod:get_opt(iqdisc, Opts, fun gen_iq_handler:check_type/1, one_queue), - case gen_mod:db_type(Host, Opts) of - mnesia -> - mnesia:create_table(private_storage, - [{disc_only_copies, [node()]}, - {attributes, - record_info(fields, private_storage)}]), - update_table(); - _ -> ok - end, + Mod = gen_mod:db_mod(Host, Opts, ?MODULE), + Mod:init(Host, Opts), ejabberd_hooks:add(remove_user, Host, ?MODULE, remove_user, 50), gen_iq_handler:add_iq_handler(ejabberd_sm, Host, @@ -73,255 +68,110 @@ stop(Host) -> ?NS_PRIVATE). process_sm_iq(#jid{luser = LUser, lserver = LServer}, - #jid{luser = LUser, lserver = LServer}, IQ) + #jid{luser = LUser, lserver = LServer}, #iq{lang = Lang} = IQ) when IQ#iq.type == set -> case IQ#iq.sub_el of #xmlel{name = <<"query">>, children = Xmlels} -> case filter_xmlels(Xmlels) of [] -> + Txt = <<"No private data found in this query">>, IQ#iq{type = error, - sub_el = [IQ#iq.sub_el, ?ERR_NOT_ACCEPTABLE]}; + sub_el = [IQ#iq.sub_el, ?ERRT_NOT_ACCEPTABLE(Lang, Txt)]}; 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; _ -> + Txt = <<"No query found">>, IQ#iq{type = error, - sub_el = [IQ#iq.sub_el, ?ERR_NOT_ACCEPTABLE]} + sub_el = [IQ#iq.sub_el, ?ERRT_NOT_ACCEPTABLE(Lang, Txt)]} end; %% process_sm_iq(#jid{luser = LUser, lserver = LServer}, - #jid{luser = LUser, lserver = LServer}, IQ) + #jid{luser = LUser, lserver = LServer}, #iq{lang = Lang} = IQ) when IQ#iq.type == get -> case IQ#iq.sub_el of #xmlel{name = <<"query">>, attrs = Attrs, children = Xmlels} -> case filter_xmlels(Xmlels) of [] -> + Txt = <<"No private data found in this query">>, IQ#iq{type = error, - sub_el = [IQ#iq.sub_el, ?ERR_BAD_FORMAT]}; + sub_el = [IQ#iq.sub_el, ?ERRT_BAD_FORMAT(Lang, Txt)]}; Data -> case catch get_data(LUser, LServer, Data) of {'EXIT', _Reason} -> + Txt = <<"Database failure">>, IQ#iq{type = error, sub_el = - [IQ#iq.sub_el, ?ERR_INTERNAL_SERVER_ERROR]}; + [IQ#iq.sub_el, ?ERRT_INTERNAL_SERVER_ERROR(Lang, Txt)]}; Storage_Xmlels -> IQ#iq{type = result, sub_el = [?Xmlel_Query(Attrs, Storage_Xmlels)]} end end; _ -> + Txt = <<"No query found">>, IQ#iq{type = error, - sub_el = [IQ#iq.sub_el, ?ERR_BAD_FORMAT]} + sub_el = [IQ#iq.sub_el, ?ERRT_BAD_FORMAT(Lang, Txt)]} end; %% -process_sm_iq(_From, _To, IQ) -> +process_sm_iq(_From, _To, #iq{lang = Lang} = IQ) -> + Txt = <<"Query to another users is forbidden">>, IQ#iq{type = error, - sub_el = [IQ#iq.sub_el, ?ERR_FORBIDDEN]}. + sub_el = [IQ#iq.sub_el, ?ERRT_FORBIDDEN(Lang, Txt)]}. 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, {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); -set_data(LUser, LServer, {XMLNS, El}, riak) -> - ejabberd_riak:put(#private_storage{usns = {LUser, LServer, XMLNS}, - xml = El}, - private_storage_schema(), - [{'2i', [{<<"us">>, {LUser, LServer}}]}]). +set_data(LUser, LServer, Data) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:set_data(LUser, LServer, Data). get_data(LUser, LServer, Data) -> - get_data(LUser, LServer, - gen_mod:db_type(LServer, ?MODULE), Data, []). + Mod = gen_mod:db_mod(LServer, ?MODULE), + get_data(LUser, LServer, Data, Mod, []). -get_data(_LUser, _LServer, _DBType, [], - Storage_Xmlels) -> +get_data(_LUser, _LServer, [], _Mod, Storage_Xmlels) -> lists:reverse(Storage_Xmlels); -get_data(LUser, LServer, mnesia, - [{XmlNS, Xmlel} | Data], Storage_Xmlels) -> - case mnesia:dirty_read(private_storage, - {LUser, LServer, XmlNS}) - of - [#private_storage{xml = Storage_Xmlel}] -> - get_data(LUser, LServer, mnesia, Data, - [Storage_Xmlel | Storage_Xmlels]); - _ -> - get_data(LUser, LServer, mnesia, Data, - [Xmlel | Storage_Xmlels]) - 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) - of - {selected, [<<"data">>], [[SData]]} -> - case xml_stream:parse_element(SData) of - Data when is_record(Data, xmlel) -> - get_data(LUser, LServer, odbc, Els, [Data | Res]) - end; - _ -> get_data(LUser, LServer, odbc, Els, [El | Res]) - end; -get_data(LUser, LServer, riak, [{XMLNS, El} | Els], - Res) -> - case ejabberd_riak:get(private_storage, private_storage_schema(), - {LUser, LServer, XMLNS}) of - {ok, #private_storage{xml = NewEl}} -> - get_data(LUser, LServer, riak, Els, [NewEl|Res]); - _ -> - get_data(LUser, LServer, riak, Els, [El|Res]) +get_data(LUser, LServer, [{XmlNS, Xmlel} | Data], Mod, Storage_Xmlels) -> + case Mod:get_data(LUser, LServer, XmlNS) of + {ok, Storage_Xmlel} -> + get_data(LUser, LServer, Data, Mod, [Storage_Xmlel | Storage_Xmlels]); + error -> + get_data(LUser, LServer, Data, Mod, [Xmlel | Storage_Xmlels]) end. get_data(LUser, LServer) -> - get_all_data(LUser, LServer, - gen_mod:db_type(LServer, ?MODULE)). - -get_all_data(LUser, LServer, mnesia) -> - lists:flatten( - mnesia:dirty_select(private_storage, - [{#private_storage{usns = {LUser, LServer, '_'}, - 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} -> - lists:flatmap( - fun([_, SData]) -> - case xml_stream:parse_element(SData) of - #xmlel{} = El -> - [El]; - _ -> - [] - end - end, Res); - _ -> - [] - end; -get_all_data(LUser, LServer, riak) -> - case ejabberd_riak:get_by_index( - private_storage, private_storage_schema(), - <<"us">>, {LUser, LServer}) of - {ok, Res} -> - [El || #private_storage{xml = El} <- Res]; - _ -> - [] - end. - -private_storage_schema() -> - {record_info(fields, private_storage), #private_storage{}}. + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:get_all_data(LUser, LServer). remove_user(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), - remove_user(LUser, LServer, - gen_mod:db_type(Server, ?MODULE)). + Mod = gen_mod:db_mod(Server, ?MODULE), + Mod:remove_user(LUser, LServer). -remove_user(LUser, LServer, mnesia) -> - F = fun () -> - Namespaces = mnesia:select(private_storage, - [{#private_storage{usns = - {LUser, - LServer, - '$1'}, - _ = '_'}, - [], ['$$']}]), - lists:foreach(fun ([Namespace]) -> - mnesia:delete({private_storage, - {LUser, LServer, - Namespace}}) - end, - Namespaces) - end, - mnesia:transaction(F); -remove_user(LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - odbc_queries:del_user_private_storage(LServer, - Username); -remove_user(LUser, LServer, riak) -> - {atomic, ejabberd_riak:delete_by_index(private_storage, - <<"us">>, {LUser, LServer})}. - -update_table() -> - Fields = record_info(fields, private_storage), - case mnesia:table_info(private_storage, attributes) of - Fields -> - ejabberd_config:convert_table_to_binary( - private_storage, Fields, set, - fun(#private_storage{usns = {U, _, _}}) -> U end, - fun(#private_storage{usns = {U, S, NS}, xml = El} = R) -> - R#private_storage{usns = {iolist_to_binary(U), - iolist_to_binary(S), - iolist_to_binary(NS)}, - xml = xml:to_xmlel(El)} - end); - _ -> - ?INFO_MSG("Recreating private_storage table", []), - mnesia:transform_table(private_storage, ignore, Fields) - end. - -export(_Server) -> - [{private_storage, - fun(Host, #private_storage{usns = {LUser, LServer, XMLNS}, - xml = Data}) - when LServer == Host -> - Username = ejabberd_odbc:escape(LUser), - LXMLNS = ejabberd_odbc:escape(XMLNS), - SData = - ejabberd_odbc:escape(xml:element_to_binary(Data)), - odbc_queries:set_private_data_sql(Username, LXMLNS, - SData); - (_Host, _R) -> - [] - end}]. +export(LServer) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:export(LServer). import(LServer) -> - [{<<"select username, namespace, data from private_storage;">>, - fun([LUser, XMLNS, XML]) -> - El = #xmlel{} = xml_stream:parse_element(XML), - #private_storage{usns = {LUser, LServer, XMLNS}, - xml = El} - end}]. - -import(_LServer, mnesia, #private_storage{} = PS) -> - mnesia:dirty_write(PS); + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:import(LServer). -import(_LServer, riak, #private_storage{usns = {LUser, LServer, _}} = PS) -> - ejabberd_riak:put(PS, private_storage_schema(), - [{'2i', [{<<"us">>, {LUser, LServer}}]}]); -import(_, _, _) -> - pass. +import(LServer, DBType, PD) -> + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:import(LServer, PD). mod_opt_type(db_type) -> fun gen_mod:v_db/1; mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; diff --git a/src/mod_private_mnesia.erl b/src/mod_private_mnesia.erl new file mode 100644 index 000000000..7a852c4f8 --- /dev/null +++ b/src/mod_private_mnesia.erl @@ -0,0 +1,97 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 13 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_private_mnesia). +-behaviour(mod_private). + +%% API +-export([init/2, set_data/3, get_data/3, get_all_data/2, remove_user/2, + import/2]). + +-include("jlib.hrl"). +-include("mod_private.hrl"). +-include("logger.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + mnesia:create_table(private_storage, + [{disc_only_copies, [node()]}, + {attributes, + record_info(fields, private_storage)}]), + update_table(). + +set_data(LUser, LServer, Data) -> + F = fun () -> + lists:foreach( + fun({XmlNS, Xmlel}) -> + mnesia:write( + #private_storage{ + usns = {LUser, LServer, XmlNS}, + xml = Xmlel}) + end, Data) + end, + mnesia:transaction(F). + +get_data(LUser, LServer, XmlNS) -> + case mnesia:dirty_read(private_storage, {LUser, LServer, XmlNS}) of + [#private_storage{xml = Storage_Xmlel}] -> + {ok, Storage_Xmlel}; + _ -> + error + end. + +get_all_data(LUser, LServer) -> + lists:flatten( + mnesia:dirty_select(private_storage, + [{#private_storage{usns = {LUser, LServer, '_'}, + xml = '$1'}, + [], ['$1']}])). + +remove_user(LUser, LServer) -> + F = fun () -> + Namespaces = mnesia:select(private_storage, + [{#private_storage{usns = + {LUser, + LServer, + '$1'}, + _ = '_'}, + [], ['$$']}]), + lists:foreach(fun ([Namespace]) -> + mnesia:delete({private_storage, + {LUser, LServer, + Namespace}}) + end, + Namespaces) + end, + mnesia:transaction(F). + +import(_LServer, #private_storage{} = PS) -> + mnesia:dirty_write(PS). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +update_table() -> + Fields = record_info(fields, private_storage), + case mnesia:table_info(private_storage, attributes) of + Fields -> + ejabberd_config:convert_table_to_binary( + private_storage, Fields, set, + fun(#private_storage{usns = {U, _, _}}) -> U end, + fun(#private_storage{usns = {U, S, NS}, xml = El} = R) -> + R#private_storage{usns = {iolist_to_binary(U), + iolist_to_binary(S), + iolist_to_binary(NS)}, + xml = fxml:to_xmlel(El)} + end); + _ -> + ?INFO_MSG("Recreating private_storage table", []), + mnesia:transform_table(private_storage, ignore, Fields) + end. diff --git a/src/mod_private_riak.erl b/src/mod_private_riak.erl new file mode 100644 index 000000000..11cfa4770 --- /dev/null +++ b/src/mod_private_riak.erl @@ -0,0 +1,67 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 13 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_private_riak). + +-behaviour(mod_private). + +%% API +-export([init/2, set_data/3, get_data/3, get_all_data/2, remove_user/2, + import/2]). + +-include("jlib.hrl"). +-include("mod_private.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ok. + +set_data(LUser, LServer, Data) -> + lists:foreach( + fun({XMLNS, El}) -> + ejabberd_riak:put(#private_storage{usns = {LUser, LServer, XMLNS}, + xml = El}, + private_storage_schema(), + [{'2i', [{<<"us">>, {LUser, LServer}}]}]) + end, Data), + {atomic, ok}. + +get_data(LUser, LServer, XMLNS) -> + case ejabberd_riak:get(private_storage, private_storage_schema(), + {LUser, LServer, XMLNS}) of + {ok, #private_storage{xml = El}} -> + {ok, El}; + _ -> + error + end. + +get_all_data(LUser, LServer) -> + case ejabberd_riak:get_by_index( + private_storage, private_storage_schema(), + <<"us">>, {LUser, LServer}) of + {ok, Res} -> + [El || #private_storage{xml = El} <- Res]; + _ -> + [] + end. + +remove_user(LUser, LServer) -> + {atomic, ejabberd_riak:delete_by_index(private_storage, + <<"us">>, {LUser, LServer})}. + +import(_LServer, #private_storage{usns = {LUser, LServer, _}} = PS) -> + ejabberd_riak:put(PS, private_storage_schema(), + [{'2i', [{<<"us">>, {LUser, LServer}}]}]). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +private_storage_schema() -> + {record_info(fields, private_storage), #private_storage{}}. diff --git a/src/mod_private_sql.erl b/src/mod_private_sql.erl new file mode 100644 index 000000000..6ec6c9df1 --- /dev/null +++ b/src/mod_private_sql.erl @@ -0,0 +1,97 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 13 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_private_sql). + +-behaviour(mod_private). + +%% API +-export([init/2, set_data/3, get_data/3, get_all_data/2, remove_user/2, + import/1, import/2, export/1]). + +-include("jlib.hrl"). +-include("mod_private.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ok. + +set_data(LUser, LServer, Data) -> + F = fun() -> + lists:foreach( + fun({XMLNS, El}) -> + SData = fxml:element_to_binary(El), + sql_queries:set_private_data( + LServer, LUser, XMLNS, SData) + end, Data) + end, + ejabberd_sql:sql_transaction(LServer, F). + +get_data(LUser, LServer, XMLNS) -> + case catch sql_queries:get_private_data(LServer, LUser, XMLNS) of + {selected, [{SData}]} -> + case fxml_stream:parse_element(SData) of + Data when is_record(Data, xmlel) -> + {ok, Data}; + _ -> + error + end; + _ -> + error + end. + +get_all_data(LUser, LServer) -> + case catch sql_queries:get_private_data(LServer, LUser) of + {selected, Res} -> + lists:flatmap( + fun({_, SData}) -> + case fxml_stream:parse_element(SData) of + #xmlel{} = El -> + [El]; + _ -> + [] + end + end, Res); + _ -> + [] + end. + +remove_user(LUser, LServer) -> + sql_queries:del_user_private_storage(LServer, LUser). + +export(_Server) -> + [{private_storage, + fun(Host, #private_storage{usns = {LUser, LServer, XMLNS}, + xml = Data}) + when LServer == Host -> + Username = ejabberd_sql:escape(LUser), + LXMLNS = ejabberd_sql:escape(XMLNS), + SData = + ejabberd_sql:escape(fxml:element_to_binary(Data)), + sql_queries:set_private_data_sql(Username, LXMLNS, + SData); + (_Host, _R) -> + [] + end}]. + +import(LServer) -> + [{<<"select username, namespace, data from private_storage;">>, + fun([LUser, XMLNS, XML]) -> + El = #xmlel{} = fxml_stream:parse_element(XML), + #private_storage{usns = {LUser, LServer, XMLNS}, + xml = El} + end}]. + +import(_, _) -> + pass. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== diff --git a/src/mod_proxy65_service.erl b/src/mod_proxy65_service.erl index 97d5b00af..d64afa04d 100644 --- a/src/mod_proxy65_service.erl +++ b/src/mod_proxy65_service.erl @@ -63,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}) -> @@ -150,7 +150,7 @@ process_iq(_, children = iq_vcard(Lang)}]}; %% bytestreams info request process_iq(JID, - #iq{type = get, sub_el = SubEl, + #iq{type = get, sub_el = SubEl, lang = Lang, xmlns = ?NS_BYTESTREAMS} = IQ, #state{acl = ACL, stream_addr = StreamAddr, @@ -165,21 +165,22 @@ process_iq(JID, attrs = [{<<"xmlns">>, ?NS_BYTESTREAMS}], children = StreamHostEl}]}; deny -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_FORBIDDEN]} + Txt = <<"Denied by ACL">>, + IQ#iq{type = error, sub_el = [SubEl, ?ERRT_FORBIDDEN(Lang, Txt)]} end; %% bytestream activation request process_iq(InitiatorJID, - #iq{type = set, sub_el = SubEl, + #iq{type = set, sub_el = SubEl, lang = Lang, xmlns = ?NS_BYTESTREAMS} = IQ, #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 - jid:from_string(xml:get_tag_cdata(ActivateEl)) + jid:from_string(fxml:get_tag_cdata(ActivateEl)) of TargetJID when is_record(TargetJID, jid), SID /= <<"">>, @@ -194,22 +195,27 @@ process_iq(InitiatorJID, of ok -> IQ#iq{type = result, sub_el = []}; false -> + Txt = <<"Failed to activate bytestream">>, IQ#iq{type = error, - sub_el = [SubEl, ?ERR_ITEM_NOT_FOUND]}; + sub_el = [SubEl, ?ERRT_ITEM_NOT_FOUND(Lang, Txt)]}; limit -> + Txt = <<"Too many active bytestreams">>, IQ#iq{type = error, - sub_el = [SubEl, ?ERR_RESOURCE_CONSTRAINT]}; + sub_el = [SubEl, ?ERRT_RESOURCE_CONSTRAINT(Lang, Txt)]}; conflict -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_CONFLICT]}; + Txt = <<"Bytestream already activated">>, + IQ#iq{type = error, sub_el = [SubEl, ?ERRT_CONFLICT(Lang, Txt)]}; _ -> IQ#iq{type = error, sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]} end; _ -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_BAD_REQUEST]} + Txt = <<"Malformed JID">>, + IQ#iq{type = error, sub_el = [SubEl, ?ERRT_BAD_REQUEST(Lang, Txt)]} end; deny -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_FORBIDDEN]} + Txt = <<"Denied by ACL">>, + IQ#iq{type = error, sub_el = [SubEl, ?ERRT_FORBIDDEN(Lang, Txt)]} end; %% Unknown "set" or "get" request process_iq(_, #iq{type = Type, sub_el = SubEl} = IQ, _) diff --git a/src/mod_proxy65_stream.erl b/src/mod_proxy65_stream.erl index 36dd7af19..840173299 100644 --- a/src/mod_proxy65_stream.erl +++ b/src/mod_proxy65_stream.erl @@ -153,7 +153,7 @@ wait_for_auth(Packet, #state{socket = Socket, host = Host} = StateData) -> case mod_proxy65_lib:unpack_auth_request(Packet) of {User, Pass} -> - Result = ejabberd_auth:check_password(User, Host, Pass), + Result = ejabberd_auth:check_password(User, <<"">>, Host, Pass), gen_tcp:send(Socket, mod_proxy65_lib:make_auth_reply(Result)), case Result of diff --git a/src/mod_pubsub.erl b/src/mod_pubsub.erl index 616c64929..e42d5c058 100644 --- a/src/mod_pubsub.erl +++ b/src/mod_pubsub.erl @@ -63,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]). @@ -241,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, @@ -256,22 +257,26 @@ init([ServerHost, Opts]) -> 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), {max_subscriptions_node, MaxSubsNode}), - ets:insert(gen_mod:get_module_proc(ServerHost, config), {default_node_config, DefaultNodeCfg}), - 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, @@ -309,7 +314,6 @@ 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), @@ -482,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">>}, @@ -504,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; _ -> [] @@ -873,7 +877,6 @@ 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, @@ -918,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} @@ -951,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, <<>>, <<>>]), @@ -968,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, @@ -1022,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 +1034,10 @@ do_route(ServerHost, Access, Plugins, Host, From, To, Packet) -> none -> ok; invalid -> - Err = jlib:make_error_reply(Packet, ?ERR_BAD_REQUEST), + Lang = fxml:get_attr_s(<<"xml:lang">>, Attrs), + Txt = <<"Incorrect authorization response">>, + Err = jlib:make_error_reply( + Packet, ?ERRT_BAD_REQUEST(Lang, Txt)), ejabberd_router:route(To, From, Err); XFields -> handle_authorization_response(Host, From, To, Packet, XFields) @@ -1040,7 +1047,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">> -> @@ -1236,7 +1243,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 :: ( @@ -1255,16 +1262,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, @@ -1276,10 +1283,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, @@ -1289,14 +1296,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, @@ -1307,37 +1314,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} @@ -1362,10 +1369,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); @@ -1380,11 +1387,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; @@ -1414,13 +1421,14 @@ adhoc_request(Host, _ServerHost, Owner, send_pending_node_form(Host, Owner, Lang, Plugins); adhoc_request(Host, _ServerHost, Owner, #adhoc_request{node = ?NS_PUBSUB_GET_PENDING, - action = <<"execute">>, xdata = XData}, + action = <<"execute">>, xdata = XData, lang = Lang}, _Access, _Plugins) -> ParseOptions = case XData of #xmlel{name = <<"x">>} = XEl -> case jlib:parse_xdata_submit(XEl) of invalid -> - {error, ?ERR_BAD_REQUEST}; + Txt = <<"Incorrect data form">>, + {error, ?ERRT_BAD_REQUEST(Lang, Txt)}; XData2 -> case set_xoption(Host, XData2, []) of NewOpts when is_list(NewOpts) -> {result, NewOpts}; @@ -1428,8 +1436,8 @@ adhoc_request(Host, _ServerHost, Owner, end end; _ -> - ?INFO_MSG("Bad XForm: ~p", [XData]), - {error, ?ERR_BAD_REQUEST} + Txt = <<"No data form found">>, + {error, ?ERRT_BAD_REQUEST(Lang, Txt)} end, case ParseOptions of {result, XForm} -> @@ -1459,7 +1467,9 @@ send_pending_node_form(Host, Owner, _Lang, Plugins) -> end, case lists:filter(Filter, Plugins) of [] -> - {error, ?ERR_FEATURE_NOT_IMPLEMENTED}; + Err = extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, + unsupported, <<"get-pending">>), + {error, Err}; Ps -> XOpts = [#xmlel{name = <<"option">>, attrs = [], children = [#xmlel{name = <<"value">>, @@ -1500,10 +1510,11 @@ send_pending_auth_events(Host, Node, Owner) -> true -> case node_call(Host, Type, get_affiliation, [Nidx, Owner]) of {result, owner} -> node_call(Host, Type, get_node_subscriptions, [Nidx]); - _ -> {error, ?ERR_FORBIDDEN} + _ -> {error, ?ERRT_FORBIDDEN(?MYLANG, <<"You're not an owner">>)} end; false -> - {error, ?ERR_FEATURE_NOT_IMPLEMENTED} + {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, + unsupported, <<"get-pending">>)} end end, case transaction(Host, Node, Action, sync_dirty) of @@ -1597,9 +1608,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; @@ -1609,7 +1620,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] -> @@ -1640,6 +1651,7 @@ send_authorization_approval(Host, JID, SNode, Subscription) -> ejabberd_router:route(service_jid(Host), JID, Stanza). handle_authorization_response(Host, From, To, Packet, XFields) -> + Lang = fxml:get_tag_attr_s(<<"xml:lang">>, Packet), case {lists:keysearch(<<"pubsub#node">>, 1, XFields), lists:keysearch(<<"pubsub#subscriber_jid">>, 1, XFields), lists:keysearch(<<"pubsub#allow">>, 1, XFields)} @@ -1661,7 +1673,7 @@ handle_authorization_response(Host, From, To, Packet, XFields) -> {result, Subs} = node_call(Host, Type, get_subscriptions, [Nidx, Subscriber]), update_auth(Host, Node, Type, Nidx, Subscriber, Allow, Subs); false -> - {error, ?ERR_FORBIDDEN} + {error, ?ERRT_FORBIDDEN(Lang, <<"You're not an owner">>)} end end, case transaction(Host, Node, Action, sync_dirty) of @@ -1676,7 +1688,8 @@ handle_authorization_response(Host, From, To, Packet, XFields) -> ejabberd_router:route(To, From, Err) end; _ -> - Err = jlib:make_error_reply(Packet, ?ERR_NOT_ACCEPTABLE), + Txt = <<"Incorrect data form">>, + Err = jlib:make_error_reply(Packet, ?ERRT_NOT_ACCEPTABLE(Lang, Txt)), ejabberd_router:route(To, From, Err) end. @@ -1687,7 +1700,7 @@ update_auth(Host, Node, Type, Nidx, Subscriber, Allow, Subs) -> end, Subs), case Sub of - [{pending, SubId}] -> + [{pending, SubId}|_] -> NewSub = case Allow of true -> subscribed; false -> none @@ -1696,7 +1709,8 @@ update_auth(Host, Node, Type, Nidx, Subscriber, Allow, Subs) -> send_authorization_approval(Host, Subscriber, Node, NewSub), {result, ok}; _ -> - {error, ?ERR_UNEXPECTED_REQUEST} + Txt = <<"No pending subscriptions found">>, + {error, ?ERRT_UNEXPECTED_REQUEST(?MYLANG, Txt)} end. -define(XFIELD(Type, Label, Var, Val), @@ -1775,6 +1789,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(), @@ -1788,8 +1816,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 -> @@ -1808,13 +1834,14 @@ 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] -> case jlib:parse_xdata_submit(XEl) of invalid -> - {error, ?ERR_BAD_REQUEST}; + Txt = <<"Incorrect data form">>, + {error, ?ERRT_BAD_REQUEST(?MYLANG, Txt)}; XData -> case set_xoption(Host, XData, node_options(Host, Type)) of NewOpts when is_list(NewOpts) -> {result, NewOpts}; @@ -1823,7 +1850,8 @@ create_node(Host, ServerHost, Node, Owner, GivenType, Access, Configuration) -> end; _ -> ?INFO_MSG("Node ~p; bad configuration: ~p", [Node, Configuration]), - {error, ?ERR_BAD_REQUEST} + Txt = <<"No data form found">>, + {error, ?ERRT_BAD_REQUEST(?MYLANG, Txt)} end, case ParseOptions of {result, NodeOptions} -> @@ -1860,7 +1888,8 @@ create_node(Host, ServerHost, Node, Owner, GivenType, Access, Configuration) -> Error end; _ -> - {error, ?ERR_FORBIDDEN} + Txt1 = <<"You're not allowed to create nodes">>, + {error, ?ERRT_FORBIDDEN(?MYLANG, Txt1)} end end, Reply = [#xmlel{name = <<"pubsub">>, @@ -1910,7 +1939,7 @@ create_node(Host, ServerHost, Node, Owner, GivenType, Access, Configuration) -> | {error, xmlel()} ). delete_node(_Host, <<>>, _Owner) -> - {error, ?ERR_NOT_ALLOWED}; + {error, ?ERRT_NOT_ALLOWED(?MYLANG, <<"No node specified">>)}; delete_node(Host, Node, Owner) -> Action = fun (#pubsub_node{type = Type, id = Nidx}) -> case node_call(Host, Type, get_affiliation, [Nidx, Owner]) of @@ -1922,7 +1951,7 @@ delete_node(Host, Node, Owner) -> Error -> Error end; _ -> - {error, ?ERR_FORBIDDEN} + {error, ?ERRT_FORBIDDEN(?MYLANG, <<"You're not an owner">>)} end end, Reply = [], @@ -2231,22 +2260,28 @@ publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload, Access) -> {result, Reply}; {result, {_, Result}} -> {result, Result}; - {error, ?ERR_ITEM_NOT_FOUND} -> - Type = select_type(ServerHost, Host, Node), - case lists:member(<<"auto-create">>, plugin_features(Host, Type)) of + {error, _} = Error -> + case is_item_not_found(Error) of true -> - case create_node(Host, ServerHost, Node, Publisher, Type, Access, []) of - {result, - [#xmlel{name = <<"pubsub">>, - attrs = [{<<"xmlns">>, ?NS_PUBSUB}], - children = [#xmlel{name = <<"create">>, - attrs = [{<<"node">>, NewNode}]}]}]} -> - publish_item(Host, ServerHost, NewNode, Publisher, ItemId, Payload); - _ -> - {error, ?ERR_ITEM_NOT_FOUND} + Type = select_type(ServerHost, Host, Node), + case lists:member(<<"auto-create">>, plugin_features(Host, Type)) of + true -> + case create_node(Host, ServerHost, Node, Publisher, Type, Access, []) of + {result, + [#xmlel{name = <<"pubsub">>, + attrs = [{<<"xmlns">>, ?NS_PUBSUB}], + children = [#xmlel{name = <<"create">>, + attrs = [{<<"node">>, NewNode}]}]}]} -> + publish_item(Host, ServerHost, NewNode, Publisher, ItemId, Payload); + _ -> + {error, ?ERR_ITEM_NOT_FOUND} + end; + false -> + Txt = <<"Automatic node creation is not enabled">>, + {error, ?ERRT_ITEM_NOT_FOUND(?MYLANG, Txt)} end; false -> - {error, ?ERR_ITEM_NOT_FOUND} + Error end; Error -> Error @@ -2400,7 +2435,9 @@ get_items(Host, Node, From, SubId, SMaxItems, ItemIds, RSM) -> end; true -> case catch jlib:binary_to_integer(SMaxItems) of - {'EXIT', _} -> {error, ?ERR_BAD_REQUEST}; + {'EXIT', _} -> + Txt = <<"Value of 'max_items' should be integer">>, + {error, ?ERRT_BAD_REQUEST(?MYLANG, Txt)}; Val -> Val end end, @@ -2490,7 +2527,7 @@ get_last_item(Host, Type, Nidx, LJID, mnesia) -> {result, {[LastItem|_], _}} -> LastItem; _ -> undefined end; -get_last_item(Host, Type, Nidx, LJID, odbc) -> +get_last_item(Host, Type, Nidx, LJID, sql) -> case node_action(Host, Type, get_last_items, [Nidx, LJID, 1]) of {result, [LastItem]} -> LastItem; _ -> undefined @@ -2505,7 +2542,7 @@ get_last_items(Host, Type, Nidx, LJID, Number, mnesia) -> {result, {Items, _}} -> lists:sublist(Items, Number); _ -> [] end; -get_last_items(Host, Type, Nidx, LJID, Number, odbc) -> +get_last_items(Host, Type, Nidx, LJID, Number, sql) -> case node_action(Host, Type, get_last_items, [Nidx, LJID, Number]) of {result, Items} -> Items; _ -> [] @@ -2623,7 +2660,7 @@ get_affiliations(Host, Node, JID) -> {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, <<"modify-affiliations">>)}; Affiliation /= owner -> - {error, ?ERR_FORBIDDEN}; + {error, ?ERRT_FORBIDDEN(?MYLANG, <<"You're not an owner">>)}; true -> node_call(Host, Type, get_node_affiliations, [Nidx]) end @@ -2668,8 +2705,8 @@ set_affiliations(Host, Node, From, EntitiesEls) -> (El, Acc) -> case El of #xmlel{name = <<"affiliation">>, attrs = Attrs} -> - JID = jid:from_string(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 -> [{jid:tolower(JID), Affiliation} | Acc] end @@ -2716,7 +2753,7 @@ set_affiliations(Host, Node, From, EntitiesEls) -> FilteredEntities), {result, []}; _ -> - {error, ?ERR_FORBIDDEN} + {error, ?ERRT_FORBIDDEN(?MYLANG, <<"You're not an owner">>)} end end, case transaction(Host, Node, Action, sync_dirty) of @@ -2932,7 +2969,7 @@ get_subscriptions(Host, Node, JID) -> {error, extended_error(?ERR_FEATURE_NOT_IMPLEMENTED, unsupported, <<"manage-subscriptions">>)}; Affiliation /= owner -> - {error, ?ERR_FORBIDDEN}; + {error, ?ERRT_FORBIDDEN(?MYLANG, <<"You're not an owner">>)}; true -> node_call(Host, Type, get_node_subscriptions, [Nidx]) end @@ -2975,7 +3012,7 @@ get_subscriptions_for_send_last(Host, PType, mnesia, JID, LJID, BJID) -> || {Node, Sub, SubId, SubJID} <- Subs, Sub =:= subscribed, (SubJID == LJID) or (SubJID == BJID), match_option(Node, send_last_published_item, on_sub_and_presence)]; -get_subscriptions_for_send_last(Host, PType, odbc, JID, LJID, BJID) -> +get_subscriptions_for_send_last(Host, PType, sql, JID, LJID, BJID) -> case catch node_action(Host, PType, get_entity_subscriptions_for_send_last, [Host, JID]) @@ -2998,9 +3035,9 @@ set_subscriptions(Host, Node, From, EntitiesEls) -> (El, Acc) -> case El of #xmlel{name = <<"subscription">>, attrs = Attrs} -> - JID = jid:from_string(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 -> [{jid:tolower(JID), Sub, SubId} | Acc] end @@ -3043,10 +3080,10 @@ set_subscriptions(Host, Node, From, EntitiesEls) -> [], Entities), case Result of [] -> {result, []}; - _ -> {error, ?ERR_NOT_ACCEPTABLE} + [{error, E}|_] -> {error, E} end; _ -> - {error, ?ERR_FORBIDDEN} + {error, ?ERRT_FORBIDDEN(?MYLANG, <<"You're not an owner">>)} end end, case transaction(Host, Node, Action, sync_dirty) of @@ -3170,17 +3207,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 @@ -3294,9 +3329,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), @@ -3587,7 +3627,7 @@ get_configure(Host, ServerHost, Node, From, Lang) -> children = get_configure_xfields(Type, Options, Lang, Groups)}]}]}]}; _ -> - {error, ?ERR_FORBIDDEN} + {error, ?ERRT_FORBIDDEN(Lang, <<"You're not an owner">>)} end end, case transaction(Host, Node, Action, sync_dirty) of @@ -3626,7 +3666,7 @@ get_option(Options, Var, Def) -> end. node_options(Host, Type) -> - case config(serverhost(Host), default_node_config) of + case config(Host, default_node_config) of undefined -> node_plugin_options(Host, Type); [] -> node_plugin_options(Host, Type); Config -> Config @@ -3648,7 +3688,7 @@ filter_node_options(Options) -> node_owners_action(Host, Type, Nidx, []) -> case gen_mod:db_type(serverhost(Host), ?MODULE) of - odbc -> + sql -> case node_action(Host, Type, get_node_affiliations, [Nidx]) of {result, Affs} -> [LJID || {LJID, Aff} <- Affs, Aff =:= owner]; _ -> [] @@ -3661,7 +3701,7 @@ node_owners_action(_Host, _Type, _Nidx, Owners) -> node_owners_call(Host, Type, Nidx, []) -> case gen_mod:db_type(serverhost(Host), ?MODULE) of - odbc -> + sql -> case node_call(Host, Type, get_node_affiliations, [Nidx]) of {result, Affs} -> [LJID || {LJID, Aff} <- Affs, Aff =:= owner]; _ -> [] @@ -3777,7 +3817,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> @@ -3788,9 +3830,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">>} -> @@ -3799,7 +3841,8 @@ set_configure(Host, Node, From, Els, Lang) -> {result, owner} -> case jlib:parse_xdata_submit(XEl) of invalid -> - {error, ?ERR_BAD_REQUEST}; + Txt = <<"Incorrect data form">>, + {error, ?ERRT_BAD_REQUEST(Lang, Txt)}; XData -> OldOpts = case Options of [] -> node_options(Host, Type); @@ -3819,7 +3862,8 @@ set_configure(Host, Node, From, Els, Lang) -> end end; _ -> - {error, ?ERR_FORBIDDEN} + Txt = <<"You're not an owner">>, + {error, ?ERRT_FORBIDDEN(Lang, Txt)} end end, case transaction(Host, Node, Action, transaction) of @@ -3833,10 +3877,12 @@ set_configure(Host, Node, From, Els, Lang) -> Other end; _ -> - {error, ?ERR_BAD_REQUEST} + Txt = <<"Incorrect data form">>, + {error, ?ERRT_BAD_REQUEST(Lang, Txt)} end; _ -> - {error, ?ERR_BAD_REQUEST} + Txt = <<"No data form found">>, + {error, ?ERRT_BAD_REQUEST(Lang, Txt)} end. add_opt(Key, Value, Opts) -> @@ -3851,7 +3897,10 @@ add_opt(Key, Value, Opts) -> _ -> error end, case BoolVal of - error -> {error, ?ERR_NOT_ACCEPTABLE}; + error -> + Txt = <<"Value of '~s' should be boolean">>, + ErrTxt = iolist_to_binary(io_lib:format(Txt, [Opt])), + {error, ?ERRT_NOT_ACCEPTABLE(?MYLANG, ErrTxt)}; _ -> set_xoption(Host, Opts, add_opt(Opt, BoolVal, NewOpts)) end). @@ -3864,10 +3913,14 @@ add_opt(Key, Value, Opts) -> if (Max =:= undefined) orelse (IVal =< Max) -> set_xoption(Host, Opts, add_opt(Opt, IVal, NewOpts)); true -> - {error, ?ERR_NOT_ACCEPTABLE} + Txt = <<"Incorrect value of '~s'">>, + ErrTxt = iolist_to_binary(io_lib:format(Txt, [Opt])), + {error, ?ERRT_NOT_ACCEPTABLE(?MYLANG, ErrTxt)} end; _ -> - {error, ?ERR_NOT_ACCEPTABLE} + Txt = <<"Value of '~s' should be integer">>, + ErrTxt = iolist_to_binary(io_lib:format(Txt, [Opt])), + {error, ?ERRT_NOT_ACCEPTABLE(?MYLANG, ErrTxt)} end). -define(SET_ALIST_XOPT(Opt, Val, Vals), @@ -3875,7 +3928,9 @@ add_opt(Key, Value, Opts) -> true -> set_xoption(Host, Opts, add_opt(Opt, jlib:binary_to_atom(Val), NewOpts)); false -> - {error, ?ERR_NOT_ACCEPTABLE} + Txt = <<"Incorrect value of '~s'">>, + ErrTxt = iolist_to_binary(io_lib:format(Txt, [Opt])), + {error, ?ERRT_NOT_ACCEPTABLE(?MYLANG, ErrTxt)} end). -define(SET_LIST_XOPT(Opt, Val), @@ -3931,25 +3986,21 @@ 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, undefined). + config(Host, max_items_node, undefined). -get_max_subscriptions_node({_, ServerHost, _}) -> - get_max_subscriptions_node(ServerHost); get_max_subscriptions_node(Host) -> - config(serverhost(Host), max_subscriptions_node, undefined). + 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); @@ -3999,19 +4050,13 @@ 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. @@ -4021,19 +4066,19 @@ tree(_Host, <<"virtual">>) -> tree(Host, Name) -> case gen_mod:db_type(serverhost(Host), ?MODULE) of mnesia -> jlib:binary_to_atom(<<"nodetree_", Name/binary>>); - odbc -> jlib:binary_to_atom(<<"nodetree_", Name/binary, "_odbc">>); + sql -> jlib:binary_to_atom(<<"nodetree_", Name/binary, "_sql">>); _ -> Name end. plugin(Host, Name) -> case gen_mod:db_type(serverhost(Host), ?MODULE) of mnesia -> jlib:binary_to_atom(<<"node_", Name/binary>>); - odbc -> jlib:binary_to_atom(<<"node_", Name/binary, "_odbc">>); + sql -> jlib:binary_to_atom(<<"node_", Name/binary, "_sql">>); _ -> Name end. plugins(Host) -> - case config(serverhost(Host), plugins) of + case config(Host, plugins) of undefined -> [?STDNODE]; [] -> [?STDNODE]; Plugins -> Plugins @@ -4042,12 +4087,15 @@ plugins(Host) -> subscription_plugin(Host) -> case gen_mod:db_type(serverhost(Host), ?MODULE) of mnesia -> pubsub_subscription; - odbc -> pubsub_subscription_odbc; + sql -> pubsub_subscription_sql; _ -> none end. 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; @@ -4064,14 +4112,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>>. @@ -4125,8 +4173,9 @@ features(Host, Node) when is_binary(Node) -> tree_call({_User, Server, _Resource}, Function, Args) -> tree_call(Server, Function, Args); tree_call(Host, Function, Args) -> - ?DEBUG("tree_call ~p ~p ~p", [Host, Function, Args]), - catch apply(tree(Host), Function, Args). + Tree = tree(Host), + ?DEBUG("tree_call apply(~s, ~s, ~p) @ ~s", [Tree, Function, Args, Host]), + catch apply(Tree, Function, Args). tree_action(Host, Function, Args) -> ?DEBUG("tree_action ~p ~p ~p", [Host, Function, Args]), @@ -4135,8 +4184,8 @@ tree_action(Host, Function, Args) -> case gen_mod:db_type(ServerHost, ?MODULE) of mnesia -> catch mnesia:sync_dirty(Fun); - odbc -> - case catch ejabberd_odbc:sql_bloc(ServerHost, Fun) of + sql -> + case catch ejabberd_sql:sql_bloc(ServerHost, Fun) of {atomic, Result} -> Result; {aborted, Reason} -> @@ -4195,7 +4244,7 @@ transaction(Host, Fun, Trans) -> ServerHost = serverhost(Host), DBType = gen_mod:db_type(ServerHost, ?MODULE), Retry = case DBType of - odbc -> 2; + sql -> 2; _ -> 1 end, transaction_retry(Host, ServerHost, Fun, Trans, DBType, Retry). @@ -4206,12 +4255,12 @@ transaction_retry(Host, ServerHost, Fun, Trans, DBType, Count) -> Res = case DBType of mnesia -> catch mnesia:Trans(Fun); - odbc -> + sql -> SqlFun = case Trans of transaction -> sql_transaction; _ -> sql_bloc end, - catch ejabberd_odbc:SqlFun(ServerHost, Fun); + catch ejabberd_sql:SqlFun(ServerHost, Fun); _ -> {unsupported, DBType} end, @@ -4253,6 +4302,13 @@ extended_error(#xmlel{name = Error, attrs = Attrs, children = SubEls}, Ext, ExtA #xmlel{name = Error, attrs = Attrs, children = lists:reverse([#xmlel{name = Ext, attrs = ExtAttrs} | SubEls])}. +is_item_not_found({error, ErrEl}) -> + case fxml:get_subtag_with_xmlns( + ErrEl, <<"item-not-found">>, ?NS_STANZAS) of + #xmlel{} -> true; + _ -> false + end. + string_to_ljid(JID) -> case jid:from_string(JID) of error -> @@ -4273,6 +4329,7 @@ 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} diff --git a/src/mod_register.erl b/src/mod_register.erl index c7bfd963b..c1a7cab81 100644 --- a/src/mod_register.erl +++ b/src/mod_register.erl @@ -121,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, @@ -132,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 @@ -151,21 +151,28 @@ process_iq(From, To, %% modules. lists:foreach can %% only return ok: not_allowed -> + Txt = <<"Removal is not allowed">>, IQ#iq{type = error, - sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; + sub_el = [SubEl, + ?ERRT_NOT_ALLOWED(Lang, Txt)]}; not_exists -> + Txt = <<"No such user">>, IQ#iq{type = error, sub_el = - [SubEl, ?ERR_ITEM_NOT_FOUND]}; - _ -> + [SubEl, + ?ERRT_ITEM_NOT_FOUND(Lang, Txt)]}; + Err -> + ?ERROR_MSG("failed to remove user ~s@~s: ~p", + [User, Server, Err]), IQ#iq{type = error, sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]} end; true -> + Txt = <<"No password in this query">>, IQ#iq{type = error, - sub_el = [SubEl, ?ERR_BAD_REQUEST]} + sub_el = [SubEl, ?ERRT_BAD_REQUEST(Lang, Txt)]} end end; (UTag == false) and (RTag /= false) and AllowRemove -> @@ -182,11 +189,13 @@ process_iq(From, To, ejabberd_auth:remove_user(User, Server), ignore; _ -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]} + Txt = <<"The query is only allowed from local users">>, + IQ#iq{type = error, + sub_el = [SubEl, ?ERRT_NOT_ALLOWED(Lang, Txt)]} 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); @@ -200,11 +209,14 @@ process_iq(From, To, SubEl, Source, Lang, true); _ -> + Txt = <<"Incorrect data form">>, IQ#iq{type = error, - sub_el = [SubEl, ?ERR_BAD_REQUEST]} + sub_el = [SubEl, ?ERRT_BAD_REQUEST(Lang, Txt)]} end; {error, malformed} -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_BAD_REQUEST]}; + Txt = <<"Incorrect CAPTCHA submit">>, + IQ#iq{type = error, + sub_el = [SubEl, ?ERRT_BAD_REQUEST(Lang, Txt)]}; _ -> ErrText = <<"The CAPTCHA verification has failed">>, IQ#iq{type = error, @@ -344,7 +356,8 @@ try_register_or_set_password(User, Server, Password, IQ#iq{type = error, sub_el = [SubEl, Error]} end; deny -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_FORBIDDEN]} + Txt = <<"Denied by ACL">>, + IQ#iq{type = error, sub_el = [SubEl, ?ERRT_FORBIDDEN(Lang, Txt)]} end; _ -> IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]} @@ -359,13 +372,17 @@ try_set_password(User, Server, Password, IQ, SubEl, of ok -> IQ#iq{type = result, sub_el = []}; {error, empty_password} -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_BAD_REQUEST]}; + Txt = <<"Empty password">>, + IQ#iq{type = error, sub_el = [SubEl, ?ERRT_BAD_REQUEST(Lang, Txt)]}; {error, not_allowed} -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; + Txt = <<"Changing password is not allowed">>, + IQ#iq{type = error, sub_el = [SubEl, ?ERRT_NOT_ALLOWED(Lang, Txt)]}; {error, invalid_jid} -> IQ#iq{type = error, - sub_el = [SubEl, ?ERR_ITEM_NOT_FOUND]}; - _ -> + sub_el = [SubEl, ?ERR_JID_MALFORMED]}; + Err -> + ?ERROR_MSG("failed to register user ~s@~s: ~p", + [User, Server, Err]), IQ#iq{type = error, sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]} end; @@ -377,7 +394,7 @@ try_set_password(User, Server, Password, IQ, SubEl, try_register(User, Server, Password, SourceRaw, Lang) -> case jid:is_nodename(User) of - false -> {error, ?ERR_BAD_REQUEST}; + false -> {error, ?ERRT_BAD_REQUEST(Lang, <<"Malformed username">>)}; _ -> JID = jid:make(User, Server, <<"">>), Access = gen_mod:get_module_opt(Server, ?MODULE, access, @@ -387,8 +404,8 @@ try_register(User, Server, Password, SourceRaw, Lang) -> case {acl:match_rule(Server, Access, JID), check_ip_access(SourceRaw, IPAccess)} of - {deny, _} -> {error, ?ERR_FORBIDDEN}; - {_, deny} -> {error, ?ERR_FORBIDDEN}; + {deny, _} -> {error, ?ERRT_FORBIDDEN(Lang, <<"Denied by ACL">>)}; + {_, deny} -> {error, ?ERRT_FORBIDDEN(Lang, <<"Denied by ACL">>)}; {allow, allow} -> Source = may_remove_resource(SourceRaw), case check_timeout(Source) of @@ -406,14 +423,20 @@ try_register(User, Server, Password, SourceRaw, Lang) -> Error -> remove_timeout(Source), case Error of - {atomic, exists} -> {error, ?ERR_CONFLICT}; + {atomic, exists} -> + Txt = <<"User already exists">>, + {error, ?ERRT_CONFLICT(Lang, Txt)}; {error, invalid_jid} -> {error, ?ERR_JID_MALFORMED}; {error, not_allowed} -> {error, ?ERR_NOT_ALLOWED}; {error, too_many_users} -> - {error, ?ERR_NOT_ALLOWED}; - {error, _Reason} -> + Txt = <<"Too many users registered">>, + {error, ?ERRT_RESOURCE_CONSTRAINT(Lang, Txt)}; + {error, _} -> + ?ERROR_MSG("failed to register user " + "~s@~s: ~p", + [User, Server, Error]), {error, ?ERR_INTERNAL_SERVER_ERROR} end end; @@ -599,7 +622,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), diff --git a/src/mod_register_web.erl b/src/mod_register_web.erl index 35f52f3b2..d93140312 100644 --- a/src/mod_register_web.erl +++ b/src/mod_register_web.erl @@ -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.">>)), @@ -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, @@ -437,7 +438,7 @@ check_account_exists(Username, Host) -> end. check_password(Username, Host, Password) -> - case ejabberd_auth:check_password(Username, Host, + case ejabberd_auth:check_password(Username, <<"">>, Host, Password) of true -> password_correct; @@ -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">>, <<"">>, @@ -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 diff --git a/src/mod_roster.erl b/src/mod_roster.erl index adc2210db..16354dd8f 100644 --- a/src/mod_roster.erl +++ b/src/mod_roster.erl @@ -49,8 +49,7 @@ 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, - mod_opt_type/1]). + mod_opt_type/1, set_roster/1]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -65,23 +64,27 @@ -export_type([subscription/0]). +-callback init(binary(), gen_mod:opts()) -> any(). +-callback import(binary(), #roster{} | #roster_version{}) -> ok | pass. +-callback read_roster_version(binary(), binary()) -> binary() | error. +-callback write_roster_version(binary(), binary(), boolean(), binary()) -> any(). +-callback get_roster(binary(), binary()) -> [#roster{}]. +-callback get_roster_by_jid(binary(), binary(), ljid()) -> #roster{}. +-callback get_only_items(binary(), binary()) -> [#roster{}]. +-callback roster_subscribe(binary(), binary(), ljid(), #roster{}) -> any(). +-callback transaction(binary(), function()) -> {atomic, any()} | {aborted, any()}. +-callback get_roster_by_jid_with_groups(binary(), binary(), ljid()) -> #roster{}. +-callback remove_user(binary(), binary()) -> {atomic, any()}. +-callback update_roster(binary(), binary(), ljid(), #roster{}) -> any(). +-callback del_roster(binary(), binary(), ljid()) -> any(). +-callback read_subscription_and_groups(binary(), binary(), ljid()) -> + {subscription(), [binary()]}. + start(Host, Opts) -> IQDisc = gen_mod:get_opt(iqdisc, Opts, fun gen_iq_handler:check_type/1, one_queue), - case gen_mod:db_type(Host, Opts) of - mnesia -> - mnesia:create_table(roster, - [{disc_copies, [node()]}, - {attributes, record_info(fields, roster)}]), - mnesia:create_table(roster_version, - [{disc_copies, [node()]}, - {attributes, - record_info(fields, roster_version)}]), - update_tables(), - mnesia:add_table_index(roster, us), - mnesia:add_table_index(roster_version, us); - _ -> ok - end, + Mod = gen_mod:db_mod(Host, Opts, ?MODULE), + Mod:init(Host, Opts), ejabberd_hooks:add(roster_get, Host, ?MODULE, get_user_roster, 50), ejabberd_hooks:add(roster_in_subscription, Host, @@ -137,13 +140,14 @@ process_iq(From, To, IQ) when ((From#jid.luser == <<"">>) andalso (From#jid.reso process_iq_manager(From, To, IQ); process_iq(From, To, IQ) -> - #iq{sub_el = SubEl} = IQ, + #iq{sub_el = SubEl, lang = Lang} = IQ, #jid{lserver = LServer} = From, case lists:member(LServer, ?MYHOSTS) of true -> process_local_iq(From, To, IQ); _ -> + Txt = <<"The query is only allowed from local users">>, IQ#iq{type = error, - sub_el = [SubEl, ?ERR_ITEM_NOT_FOUND]} + sub_el = [SubEl, ?ERRT_ITEM_NOT_FOUND(Lang, Txt)]} end. process_local_iq(From, To, #iq{type = Type} = IQ) -> @@ -193,28 +197,8 @@ roster_version(LServer, LUser) -> end. read_roster_version(LUser, LServer) -> - read_roster_version(LUser, LServer, - gen_mod:db_type(LServer, ?MODULE)). - -read_roster_version(LUser, LServer, mnesia) -> - US = {LUser, LServer}, - case mnesia:dirty_read(roster_version, US) of - [#roster_version{version = V}] -> V; - [] -> 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 - end; -read_roster_version(LServer, LUser, riak) -> - case ejabberd_riak:get(roster_version, roster_version_schema(), - {LUser, LServer}) of - {ok, #roster_version{version = V}} -> V; - _Err -> error - end. + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:read_roster_version(LUser, LServer). write_roster_version(LUser, LServer) -> write_roster_version(LUser, LServer, false). @@ -224,38 +208,10 @@ write_roster_version_t(LUser, LServer) -> write_roster_version(LUser, LServer, InTransaction) -> 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)), + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:write_roster_version(LUser, LServer, InTransaction, Ver), Ver. -write_roster_version(LUser, LServer, InTransaction, Ver, - mnesia) -> - US = {LUser, LServer}, - if InTransaction -> - mnesia:write(#roster_version{us = US, version = Ver}); - true -> - mnesia:dirty_write(#roster_version{us = US, - version = Ver}) - end; -write_roster_version(LUser, LServer, InTransaction, Ver, - odbc) -> - Username = ejabberd_odbc:escape(LUser), - EVer = ejabberd_odbc:escape(Ver), - if InTransaction -> - odbc_queries:set_roster_version(Username, EVer); - true -> - odbc_queries:sql_transaction(LServer, - fun () -> - odbc_queries:set_roster_version(Username, - EVer) - end) - end; -write_roster_version(LUser, LServer, _InTransaction, Ver, - riak) -> - US = {LUser, LServer}, - ejabberd_riak:put(#roster_version{us = US, version = Ver}, - roster_version_schema()). - %% Load roster from DB only if neccesary. %% It is neccesary if %% - roster versioning is disabled in server OR @@ -267,7 +223,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 @@ -351,65 +307,15 @@ get_user_roster(Acc, {LUser, LServer}) -> ++ Acc. get_roster(LUser, LServer) -> - get_roster(LUser, LServer, - gen_mod:db_type(LServer, ?MODULE)). + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:get_roster(LUser, LServer). -get_roster(LUser, LServer, mnesia) -> - US = {LUser, LServer}, - case catch mnesia:dirty_index_read(roster, US, - #roster.us) - of - Items when is_list(Items)-> Items; - _ -> [] - end; -get_roster(LUser, LServer, riak) -> - case ejabberd_riak:get_by_index(roster, roster_schema(), - <<"us">>, {LUser, LServer}) of - {ok, Items} -> Items; - _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 = - 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">>, @@ -440,61 +346,16 @@ item_to_xml(Item) -> children = SubEls}. get_roster_by_jid_t(LUser, LServer, LJID) -> - DBType = gen_mod:db_type(LServer, ?MODULE), - get_roster_by_jid_t(LUser, LServer, LJID, DBType). - -get_roster_by_jid_t(LUser, LServer, LJID, mnesia) -> - case mnesia:read({roster, {LUser, LServer, LJID}}) of - [] -> - #roster{usj = {LUser, LServer, LJID}, - us = {LUser, LServer}, jid = LJID}; - [I] -> - I#roster{jid = LJID, name = <<"">>, groups = [], - xs = []} - end; -get_roster_by_jid_t(LUser, LServer, LJID, odbc) -> - Username = ejabberd_odbc:escape(LUser), - SJID = ejabberd_odbc:escape(jid:to_string(LJID)), - {selected, - [<<"username">>, <<"jid">>, <<"nick">>, - <<"subscription">>, <<"ask">>, <<"askmessage">>, - <<"server">>, <<"subscribe">>, <<"type">>], - Res} = - odbc_queries:get_roster_by_jid(LServer, Username, SJID), - case Res of - [] -> - #roster{usj = {LUser, LServer, LJID}, - us = {LUser, LServer}, jid = LJID}; - [I] -> - R = raw_to_record(LServer, I), - case R of - %% Bad JID in database: - error -> - #roster{usj = {LUser, LServer, LJID}, - us = {LUser, LServer}, jid = LJID}; - _ -> - R#roster{usj = {LUser, LServer, LJID}, - us = {LUser, LServer}, jid = LJID, name = <<"">>} - end - end; -get_roster_by_jid_t(LUser, LServer, LJID, riak) -> - case ejabberd_riak:get(roster, roster_schema(), {LUser, LServer, LJID}) of - {ok, I} -> - I#roster{jid = LJID, name = <<"">>, groups = [], - xs = []}; - {error, notfound} -> - #roster{usj = {LUser, LServer, LJID}, - us = {LUser, LServer}, jid = LJID}; - Err -> - exit(Err) - end. + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:get_roster_by_jid(LUser, LServer, LJID). -try_process_iq_set(From, To, #iq{sub_el = SubEl} = IQ) -> +try_process_iq_set(From, To, #iq{sub_el = SubEl, lang = Lang} = IQ) -> #jid{server = Server} = From, Access = gen_mod:get_module_opt(Server, ?MODULE, access, fun(A) when is_atom(A) -> A end, all), case acl:match_rule(Server, Access, From) of deny -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; + Txt = <<"Denied by ACL">>, + IQ#iq{type = error, sub_el = [SubEl, ?ERRT_NOT_ALLOWED(Lang, Txt)]}; allow -> process_iq_set(From, To, IQ) end. @@ -509,7 +370,7 @@ process_iq_set(From, To, #iq{sub_el = SubEl, id = Id} = IQ) -> process_item_set(From, To, #xmlel{attrs = Attrs, children = Els}, Managed) -> - JID1 = jid:from_string(xml:get_attr_s(<<"jid">>, + JID1 = jid:from_string(fxml:get_attr_s(<<"jid">>, Attrs)), #jid{user = User, luser = LUser, lserver = LServer} = From, @@ -578,10 +439,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, @@ -640,86 +501,37 @@ push_item_version(Server, User, From, Item, end, ejabberd_sm:get_user_resources(User, Server)). -get_subscription_lists(Acc, User, Server) -> +get_subscription_lists(_Acc, User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), - DBType = gen_mod:db_type(LServer, ?MODULE), - Items = get_subscription_lists(Acc, LUser, LServer, - DBType), + Mod = gen_mod:db_mod(LServer, ?MODULE), + Items = Mod:get_only_items(LUser, LServer), fill_subscription_lists(LServer, Items, [], []). -get_subscription_lists(_, LUser, LServer, mnesia) -> - US = {LUser, LServer}, - case mnesia:dirty_index_read(roster, US, #roster.us) of - Items when is_list(Items) -> Items; - _ -> [] - 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) -> - lists:map(fun(I) -> raw_to_record(LServer, I) end, Items); - _ -> [] - end; -get_subscription_lists(_, LUser, LServer, riak) -> - case ejabberd_riak:get_by_index(roster, roster_schema(), - <<"us">>, {LUser, LServer}) of - {ok, Items} -> Items; - _Err -> [] - end. - -fill_subscription_lists(LServer, [#roster{} = I | Is], - F, T) -> +fill_subscription_lists(LServer, [I | Is], F, T) -> J = element(3, I#roster.usj), case I#roster.subscription of - both -> - fill_subscription_lists(LServer, Is, [J | F], [J | T]); - from -> - fill_subscription_lists(LServer, Is, [J | F], T); - to -> fill_subscription_lists(LServer, Is, F, [J | T]); - _ -> fill_subscription_lists(LServer, Is, F, T) - end; -fill_subscription_lists(LServer, [RawI | Is], F, T) -> - I = raw_to_record(LServer, RawI), - case I of - %% Bad JID in database: - error -> fill_subscription_lists(LServer, Is, F, T); - _ -> fill_subscription_lists(LServer, [I | Is], F, T) + both -> + fill_subscription_lists(LServer, Is, [J | F], [J | T]); + from -> + fill_subscription_lists(LServer, Is, [J | F], T); + to -> fill_subscription_lists(LServer, Is, F, [J | T]); + _ -> fill_subscription_lists(LServer, Is, F, T) end; -fill_subscription_lists(_LServer, [], F, T) -> {F, T}. +fill_subscription_lists(_LServer, [], F, T) -> + {F, T}. ask_to_pending(subscribe) -> out; ask_to_pending(unsubscribe) -> none; ask_to_pending(Ask) -> Ask. roster_subscribe_t(LUser, LServer, LJID, Item) -> - DBType = gen_mod:db_type(LServer, ?MODULE), - roster_subscribe_t(LUser, LServer, LJID, Item, DBType). - -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(jid:to_string(LJID)), - odbc_queries:roster_subscribe(LServer, Username, SJID, - ItemVals); -roster_subscribe_t(LUser, LServer, _LJID, Item, riak) -> - ejabberd_riak:put(Item, roster_schema(), - [{'2i', [{<<"us">>, {LUser, LServer}}]}]). + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:roster_subscribe(LUser, LServer, LJID, Item). transaction(LServer, F) -> - case gen_mod:db_type(LServer, ?MODULE) of - mnesia -> mnesia:transaction(F); - odbc -> ejabberd_odbc:sql_transaction(LServer, F); - riak -> {atomic, F()} - end. + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:transaction(LServer, F). in_subscription(_, User, Server, JID, Type, Reason) -> process_subscription(in, User, Server, JID, Type, @@ -729,57 +541,8 @@ out_subscription(User, Server, JID, Type) -> process_subscription(out, User, Server, JID, Type, <<"">>). get_roster_by_jid_with_groups_t(LUser, LServer, LJID) -> - DBType = gen_mod:db_type(LServer, ?MODULE), - get_roster_by_jid_with_groups_t(LUser, LServer, LJID, - DBType). - -get_roster_by_jid_with_groups_t(LUser, LServer, LJID, - mnesia) -> - case mnesia:read({roster, {LUser, LServer, LJID}}) of - [] -> - #roster{usj = {LUser, LServer, LJID}, - us = {LUser, LServer}, jid = LJID}; - [I] -> I - end; -get_roster_by_jid_with_groups_t(LUser, LServer, LJID, - odbc) -> - Username = ejabberd_odbc:escape(LUser), - SJID = ejabberd_odbc:escape(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">>], - []} -> - #roster{usj = {LUser, LServer, LJID}, - us = {LUser, LServer}, jid = LJID} - end; -get_roster_by_jid_with_groups_t(LUser, LServer, LJID, riak) -> - case ejabberd_riak:get(roster, roster_schema(), {LUser, LServer, LJID}) of - {ok, I} -> - I; - {error, notfound} -> - #roster{usj = {LUser, LServer, LJID}, - us = {LUser, LServer}, jid = LJID}; - Err -> - exit(Err) - end. + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:get_roster_by_jid_with_groups(LUser, LServer, LJID). process_subscription(Direction, User, Server, JID1, Type, Reason) -> @@ -977,22 +740,8 @@ remove_user(User, 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)). - -remove_user(LUser, LServer, mnesia) -> - US = {LUser, LServer}, - F = fun () -> - lists:foreach(fun (R) -> mnesia:delete_object(R) end, - mnesia:index_read(roster, US, #roster.us)) - end, - mnesia:transaction(F); -remove_user(LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - odbc_queries:del_user_roster_t(LServer, Username), - ok; -remove_user(LUser, LServer, riak) -> - {atomic, ejabberd_riak:delete_by_index(roster, <<"us">>, {LUser, LServer})}. + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:remove_user(LUser, LServer). %% For each contact with Subscription: %% Both or From, send a "unsubscribed" presence stanza; @@ -1050,39 +799,16 @@ set_items(User, Server, SubEl) -> transaction(LServer, F). update_roster_t(LUser, LServer, LJID, Item) -> - DBType = gen_mod:db_type(LServer, ?MODULE), - update_roster_t(LUser, LServer, LJID, Item, DBType). - -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(jid:to_string(LJID)), - ItemVals = record_to_string(Item), - ItemGroups = groups_to_string(Item), - odbc_queries:update_roster(LServer, Username, SJID, ItemVals, - ItemGroups); -update_roster_t(LUser, LServer, _LJID, Item, riak) -> - ejabberd_riak:put(Item, roster_schema(), - [{'2i', [{<<"us">>, {LUser, LServer}}]}]). + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:update_roster(LUser, LServer, LJID, Item). del_roster_t(LUser, LServer, LJID) -> - DBType = gen_mod:db_type(LServer, ?MODULE), - del_roster_t(LUser, LServer, LJID, DBType). - -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(jid:to_string(LJID)), - odbc_queries:del_roster(LServer, Username, SJID); -del_roster_t(LUser, LServer, LJID, riak) -> - ejabberd_riak:delete(roster, {LUser, LServer, LJID}). + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:del_roster(LUser, LServer, LJID). process_item_set_t(LUser, LServer, #xmlel{attrs = Attrs, children = Els}) -> - JID1 = jid:from_string(xml:get_attr_s(<<"jid">>, + JID1 = jid:from_string(fxml:get_attr_s(<<"jid">>, Attrs)), case JID1 of error -> ok; @@ -1141,13 +867,12 @@ process_item_attrs_ws(Item, []) -> Item. get_in_pending_subscriptions(Ls, User, Server) -> LServer = jid:nameprep(Server), - get_in_pending_subscriptions(Ls, User, Server, - gen_mod:db_type(LServer, ?MODULE)). + Mod = gen_mod:db_mod(LServer, ?MODULE), + get_in_pending_subscriptions(Ls, User, Server, Mod). -get_in_pending_subscriptions(Ls, User, Server, DBType) - when DBType == mnesia; DBType == riak -> +get_in_pending_subscriptions(Ls, User, Server, Mod) -> JID = jid:make(User, Server, <<"">>), - Result = get_roster(JID#jid.luser, JID#jid.lserver, DBType), + Result = Mod:get_only_items(JID#jid.luser, JID#jid.lserver), Ls ++ lists:map(fun (R) -> Message = R#roster.askmessage, Status = if is_binary(Message) -> (Message); @@ -1172,102 +897,15 @@ get_in_pending_subscriptions(Ls, User, Server, DBType) _ -> false end end, - Result)); -get_in_pending_subscriptions(Ls, User, Server, odbc) -> - 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) -> - Ls ++ - lists:map(fun (R) -> - Message = R#roster.askmessage, - #xmlel{name = <<"presence">>, - attrs = - [{<<"from">>, - jid:to_string(R#roster.jid)}, - {<<"to">>, jid:to_string(JID)}, - {<<"type">>, <<"subscribe">>}], - children = - [#xmlel{name = <<"status">>, - attrs = [], - children = - [{xmlcdata, Message}]}]} - end, - lists:flatmap(fun (I) -> - case raw_to_record(LServer, I) of - %% Bad JID in database: - error -> []; - R -> - case R#roster.ask of - in -> [R]; - both -> [R]; - _ -> [] - end - end - end, - Items)); - _ -> Ls - end. + Result)). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% read_subscription_and_groups(User, Server, LJID) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), - read_subscription_and_groups(LUser, LServer, LJID, - gen_mod:db_type(LServer, ?MODULE)). - -read_subscription_and_groups(LUser, LServer, LJID, - mnesia) -> - case catch mnesia:dirty_read(roster, - {LUser, LServer, LJID}) - of - [#roster{subscription = Subscription, - groups = Groups}] -> - {Subscription, Groups}; - _ -> error - end; -read_subscription_and_groups(LUser, LServer, LJID, - odbc) -> - Username = ejabberd_odbc:escape(LUser), - SJID = ejabberd_odbc:escape(jid:to_string(LJID)), - case catch odbc_queries:get_subscription(LServer, - Username, SJID) - of - {selected, [<<"subscription">>], [[SSubscription]]} -> - Subscription = case SSubscription of - <<"B">> -> both; - <<"T">> -> to; - <<"F">> -> from; - _ -> none - end, - Groups = case catch - odbc_queries:get_rostergroup_by_jid(LServer, Username, - SJID) - of - {selected, [<<"grp">>], JGrps} when is_list(JGrps) -> - [JGrp || [JGrp] <- JGrps]; - _ -> [] - end, - {Subscription, Groups}; - _ -> error - end; -read_subscription_and_groups(LUser, LServer, LJID, - riak) -> - case ejabberd_riak:get(roster, roster_schema(), {LUser, LServer, LJID}) of - {ok, #roster{subscription = Subscription, - groups = Groups}} -> - {Subscription, Groups}; - _ -> - error - end. + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:read_subscription_and_groups(LUser, LServer, LJID). get_jid_info(_, User, Server, JID) -> LJID = jid:tolower(JID), @@ -1287,128 +925,6 @@ get_jid_info(_, User, Server, JID) -> %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -raw_to_record(LServer, - [User, SJID, Nick, SSubscription, SAsk, SAskMessage, - _SServer, _SSubscribe, _SType]) -> - case jid:from_string(SJID) of - error -> error; - JID -> - LJID = jid:tolower(JID), - Subscription = case SSubscription of - <<"B">> -> both; - <<"T">> -> to; - <<"F">> -> from; - _ -> none - end, - Ask = case SAsk of - <<"S">> -> subscribe; - <<"U">> -> unsubscribe; - <<"B">> -> both; - <<"O">> -> out; - <<"I">> -> in; - _ -> none - end, - #roster{usj = {User, LServer, LJID}, - us = {User, LServer}, jid = LJID, name = Nick, - subscription = Subscription, ask = Ask, - askmessage = SAskMessage} - end. - -record_to_string(#roster{us = {User, _Server}, - jid = JID, name = Name, subscription = Subscription, - ask = Ask, askmessage = AskMessage}) -> - Username = ejabberd_odbc:escape(User), - SJID = - ejabberd_odbc:escape(jid:to_string(jid:tolower(JID))), - Nick = ejabberd_odbc:escape(Name), - 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, - SAskMessage = ejabberd_odbc:escape(AskMessage), - [Username, SJID, Nick, SSubscription, SAsk, SAskMessage, - <<"N">>, <<"">>, <<"item">>]. - -groups_to_string(#roster{us = {User, _Server}, - jid = JID, groups = Groups}) -> - Username = ejabberd_odbc:escape(User), - SJID = - ejabberd_odbc:escape(jid:to_string(jid:tolower(JID))), - lists:foldl(fun (<<"">>, Acc) -> Acc; - (Group, Acc) -> - G = ejabberd_odbc:escape(Group), - [[Username, SJID, G] | Acc] - end, - [], Groups). - -update_tables() -> - update_roster_table(), - update_roster_version_table(). - -update_roster_table() -> - Fields = record_info(fields, roster), - case mnesia:table_info(roster, attributes) of - Fields -> - ejabberd_config:convert_table_to_binary( - roster, Fields, set, - fun(#roster{usj = {U, _, _}}) -> U end, - fun(#roster{usj = {U, S, {LU, LS, LR}}, - us = {U1, S1}, - jid = {U2, S2, R2}, - name = Name, - groups = Gs, - askmessage = Ask, - xs = Xs} = R) -> - R#roster{usj = {iolist_to_binary(U), - iolist_to_binary(S), - {iolist_to_binary(LU), - iolist_to_binary(LS), - iolist_to_binary(LR)}}, - us = {iolist_to_binary(U1), - iolist_to_binary(S1)}, - jid = {iolist_to_binary(U2), - iolist_to_binary(S2), - iolist_to_binary(R2)}, - name = iolist_to_binary(Name), - groups = [iolist_to_binary(G) || G <- Gs], - askmessage = try iolist_to_binary(Ask) - catch _:_ -> <<"">> end, - xs = [xml:to_xmlel(X) || X <- Xs]} - end); - _ -> - ?INFO_MSG("Recreating roster table", []), - mnesia:transform_table(roster, ignore, Fields) - end. - -%% Convert roster table to support virtual host -%% Convert roster table: xattrs fields become -update_roster_version_table() -> - Fields = record_info(fields, roster_version), - case mnesia:table_info(roster_version, attributes) of - Fields -> - ejabberd_config:convert_table_to_binary( - roster_version, Fields, set, - fun(#roster_version{us = {U, _}}) -> U end, - fun(#roster_version{us = {U, S}, version = Ver} = R) -> - R#roster_version{us = {iolist_to_binary(U), - iolist_to_binary(S)}, - version = iolist_to_binary(Ver)} - end); - _ -> - ?INFO_MSG("Recreating roster_version table", []), - mnesia:transform_table(roster_version, ignore, Fields) - end. - webadmin_page(_, Host, #request{us = _US, path = [<<"user">>, U, <<"roster">>], q = Query, lang = Lang} = @@ -1632,8 +1148,9 @@ process_iq_manager(From, To, IQ) -> true -> process_iq_manager2(MatchDomain, To, IQ); false -> - #iq{sub_el = SubEl} = IQ, - IQ#iq{type = error, sub_el = [SubEl, ?ERR_BAD_REQUEST]} + #iq{sub_el = SubEl, lang = Lang} = IQ, + Txt = <<"Roster management is not allowed from this domain">>, + IQ#iq{type = error, sub_el = [SubEl, ?ERRT_BAD_REQUEST(Lang, Txt)]} end. process_iq_manager2(MatchDomain, To, IQ) -> @@ -1705,68 +1222,17 @@ is_managed_from_id(<<"roster-remotely-managed">>) -> is_managed_from_id(_Id) -> false. -roster_schema() -> - {record_info(fields, roster), #roster{}}. - -roster_version_schema() -> - {record_info(fields, roster_version), #roster_version{}}. - -export(_Server) -> - [{roster, - fun(Host, #roster{usj = {LUser, LServer, LJID}} = R) - when LServer == Host -> - Username = ejabberd_odbc:escape(LUser), - 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, - ItemVals, ItemGroups); - (_Host, _R) -> - [] - end}, - {roster_version, - fun(Host, #roster_version{us = {LUser, LServer}, version = Ver}) - when LServer == Host -> - Username = ejabberd_odbc:escape(LUser), - SVer = ejabberd_odbc:escape(Ver), - [[<<"delete from roster_version where username='">>, - Username, <<"';">>], - [<<"insert into roster_version(username, version) values('">>, - Username, <<"', '">>, SVer, <<"');">>]]; - (_Host, _R) -> - [] - end}]. +export(LServer) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:export(LServer). import(LServer) -> - [{<<"select username, jid, nick, subscription, " - "ask, askmessage, server, subscribe, type from rosterusers;">>, - fun([LUser, JID|_] = Row) -> - Item = raw_to_record(LServer, Row), - Username = ejabberd_odbc:escape(LUser), - SJID = ejabberd_odbc:escape(JID), - {selected, _, Rows} = - ejabberd_odbc:sql_query_t( - [<<"select grp from rostergroups where username='">>, - Username, <<"' and jid='">>, SJID, <<"'">>]), - Groups = [Grp || [Grp] <- Rows], - Item#roster{groups = Groups} - end}, - {<<"select username, version from roster_version;">>, - fun([LUser, Ver]) -> - #roster_version{us = {LUser, LServer}, version = Ver} - end}]. - -import(_LServer, mnesia, #roster{} = R) -> - mnesia:dirty_write(R); -import(_LServer, mnesia, #roster_version{} = RV) -> - mnesia:dirty_write(RV); -import(_LServer, riak, #roster{us = {LUser, LServer}} = R) -> - ejabberd_riak:put(R, roster_schema(), - [{'2i', [{<<"us">>, {LUser, LServer}}]}]); -import(_LServer, riak, #roster_version{} = RV) -> - ejabberd_riak:put(RV, roster_version_schema()); -import(_, _, _) -> - pass. + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:import(LServer). + +import(LServer, DBType, R) -> + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:import(LServer, R). mod_opt_type(access) -> fun (A) when is_atom(A) -> A end; diff --git a/src/mod_roster_mnesia.erl b/src/mod_roster_mnesia.erl new file mode 100644 index 000000000..ddfa34d68 --- /dev/null +++ b/src/mod_roster_mnesia.erl @@ -0,0 +1,171 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 13 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_roster_mnesia). + +-behaviour(mod_roster). + +%% API +-export([init/2, read_roster_version/2, write_roster_version/4, + get_roster/2, get_roster_by_jid/3, get_only_items/2, + roster_subscribe/4, get_roster_by_jid_with_groups/3, + remove_user/2, update_roster/4, del_roster/3, transaction/2, + read_subscription_and_groups/3, import/2]). + +-include("jlib.hrl"). +-include("mod_roster.hrl"). +-include("logger.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + mnesia:create_table(roster, + [{disc_copies, [node()]}, + {attributes, record_info(fields, roster)}]), + mnesia:create_table(roster_version, + [{disc_copies, [node()]}, + {attributes, + record_info(fields, roster_version)}]), + update_tables(), + mnesia:add_table_index(roster, us), + mnesia:add_table_index(roster_version, us). + +read_roster_version(LUser, LServer) -> + US = {LUser, LServer}, + case mnesia:dirty_read(roster_version, US) of + [#roster_version{version = V}] -> V; + [] -> error + end. + +write_roster_version(LUser, LServer, InTransaction, Ver) -> + US = {LUser, LServer}, + if InTransaction -> + mnesia:write(#roster_version{us = US, version = Ver}); + true -> + mnesia:dirty_write(#roster_version{us = US, version = Ver}) + end. + +get_roster(LUser, LServer) -> + mnesia:dirty_index_read(roster, {LUser, LServer}, #roster.us). + +get_roster_by_jid(LUser, LServer, LJID) -> + case mnesia:read({roster, {LUser, LServer, LJID}}) of + [] -> + #roster{usj = {LUser, LServer, LJID}, + us = {LUser, LServer}, jid = LJID}; + [I] -> + I#roster{jid = LJID, name = <<"">>, groups = [], + xs = []} + end. + +get_only_items(LUser, LServer) -> + get_roster(LUser, LServer). + +roster_subscribe(_LUser, _LServer, _LJID, Item) -> + mnesia:write(Item). + +get_roster_by_jid_with_groups(LUser, LServer, LJID) -> + case mnesia:read({roster, {LUser, LServer, LJID}}) of + [] -> + #roster{usj = {LUser, LServer, LJID}, + us = {LUser, LServer}, jid = LJID}; + [I] -> I + end. + +remove_user(LUser, LServer) -> + US = {LUser, LServer}, + F = fun () -> + lists:foreach( + fun (R) -> mnesia:delete_object(R) end, + mnesia:index_read(roster, US, #roster.us)) + end, + mnesia:transaction(F). + +update_roster(_LUser, _LServer, _LJID, Item) -> + mnesia:write(Item). + +del_roster(LUser, LServer, LJID) -> + mnesia:delete({roster, {LUser, LServer, LJID}}). + +read_subscription_and_groups(LUser, LServer, LJID) -> + case mnesia:dirty_read(roster, {LUser, LServer, LJID}) of + [#roster{subscription = Subscription, groups = Groups}] -> + {Subscription, Groups}; + _ -> + error + end. + +transaction(_LServer, F) -> + mnesia:transaction(F). + +import(_LServer, #roster{} = R) -> + mnesia:dirty_write(R); +import(_LServer, #roster_version{} = RV) -> + mnesia:dirty_write(RV). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +update_tables() -> + update_roster_table(), + update_roster_version_table(). + +update_roster_table() -> + Fields = record_info(fields, roster), + case mnesia:table_info(roster, attributes) of + Fields -> + ejabberd_config:convert_table_to_binary( + roster, Fields, set, + fun(#roster{usj = {U, _, _}}) -> U end, + fun(#roster{usj = {U, S, {LU, LS, LR}}, + us = {U1, S1}, + jid = {U2, S2, R2}, + name = Name, + groups = Gs, + askmessage = Ask, + xs = Xs} = R) -> + R#roster{usj = {iolist_to_binary(U), + iolist_to_binary(S), + {iolist_to_binary(LU), + iolist_to_binary(LS), + iolist_to_binary(LR)}}, + us = {iolist_to_binary(U1), + iolist_to_binary(S1)}, + jid = {iolist_to_binary(U2), + iolist_to_binary(S2), + iolist_to_binary(R2)}, + name = iolist_to_binary(Name), + groups = [iolist_to_binary(G) || G <- Gs], + askmessage = try iolist_to_binary(Ask) + catch _:_ -> <<"">> end, + xs = [fxml:to_xmlel(X) || X <- Xs]} + end); + _ -> + ?INFO_MSG("Recreating roster table", []), + mnesia:transform_table(roster, ignore, Fields) + end. + +%% Convert roster table to support virtual host +%% Convert roster table: xattrs fields become +update_roster_version_table() -> + Fields = record_info(fields, roster_version), + case mnesia:table_info(roster_version, attributes) of + Fields -> + ejabberd_config:convert_table_to_binary( + roster_version, Fields, set, + fun(#roster_version{us = {U, _}}) -> U end, + fun(#roster_version{us = {U, S}, version = Ver} = R) -> + R#roster_version{us = {iolist_to_binary(U), + iolist_to_binary(S)}, + version = iolist_to_binary(Ver)} + end); + _ -> + ?INFO_MSG("Recreating roster_version table", []), + mnesia:transform_table(roster_version, ignore, Fields) + end. diff --git a/src/mod_roster_riak.erl b/src/mod_roster_riak.erl new file mode 100644 index 000000000..38e873827 --- /dev/null +++ b/src/mod_roster_riak.erl @@ -0,0 +1,113 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 14 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_roster_riak). + + +-behaviour(mod_roster). + +%% API +-export([init/2, read_roster_version/2, write_roster_version/4, + get_roster/2, get_roster_by_jid/3, + roster_subscribe/4, get_roster_by_jid_with_groups/3, + remove_user/2, update_roster/4, del_roster/3, transaction/2, + read_subscription_and_groups/3, get_only_items/2, import/2]). + +-include("jlib.hrl"). +-include("mod_roster.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ok. + +read_roster_version(LUser, LServer) -> + case ejabberd_riak:get(roster_version, roster_version_schema(), + {LUser, LServer}) of + {ok, #roster_version{version = V}} -> V; + _Err -> error + end. + +write_roster_version(LUser, LServer, _InTransaction, Ver) -> + US = {LUser, LServer}, + ejabberd_riak:put(#roster_version{us = US, version = Ver}, + roster_version_schema()). + +get_roster(LUser, LServer) -> + case ejabberd_riak:get_by_index(roster, roster_schema(), + <<"us">>, {LUser, LServer}) of + {ok, Items} -> Items; + _Err -> [] + end. + +get_roster_by_jid(LUser, LServer, LJID) -> + case ejabberd_riak:get(roster, roster_schema(), {LUser, LServer, LJID}) of + {ok, I} -> + I#roster{jid = LJID, name = <<"">>, groups = [], xs = []}; + {error, notfound} -> + #roster{usj = {LUser, LServer, LJID}, + us = {LUser, LServer}, jid = LJID}; + Err -> + exit(Err) + end. + +get_only_items(LUser, LServer) -> + get_roster(LUser, LServer). + +roster_subscribe(LUser, LServer, _LJID, Item) -> + ejabberd_riak:put(Item, roster_schema(), + [{'2i', [{<<"us">>, {LUser, LServer}}]}]). + +transaction(_LServer, F) -> + {atomic, F()}. + +get_roster_by_jid_with_groups(LUser, LServer, LJID) -> + case ejabberd_riak:get(roster, roster_schema(), {LUser, LServer, LJID}) of + {ok, I} -> + I; + {error, notfound} -> + #roster{usj = {LUser, LServer, LJID}, + us = {LUser, LServer}, jid = LJID}; + Err -> + exit(Err) + end. + +remove_user(LUser, LServer) -> + {atomic, ejabberd_riak:delete_by_index(roster, <<"us">>, {LUser, LServer})}. + +update_roster(LUser, LServer, _LJID, Item) -> + ejabberd_riak:put(Item, roster_schema(), + [{'2i', [{<<"us">>, {LUser, LServer}}]}]). + +del_roster(LUser, LServer, LJID) -> + ejabberd_riak:delete(roster, {LUser, LServer, LJID}). + +read_subscription_and_groups(LUser, LServer, LJID) -> + case ejabberd_riak:get(roster, roster_schema(), {LUser, LServer, LJID}) of + {ok, #roster{subscription = Subscription, + groups = Groups}} -> + {Subscription, Groups}; + _ -> + error + end. + +import(_LServer, #roster{us = {LUser, LServer}} = R) -> + ejabberd_riak:put(R, roster_schema(), + [{'2i', [{<<"us">>, {LUser, LServer}}]}]); +import(_LServer, #roster_version{} = RV) -> + ejabberd_riak:put(RV, roster_version_schema()). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +roster_schema() -> + {record_info(fields, roster), #roster{}}. + +roster_version_schema() -> + {record_info(fields, roster_version), #roster_version{}}. diff --git a/src/mod_roster_sql.erl b/src/mod_roster_sql.erl new file mode 100644 index 000000000..616d0ec60 --- /dev/null +++ b/src/mod_roster_sql.erl @@ -0,0 +1,308 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 14 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_roster_sql). + +-behaviour(mod_roster). + +%% API +-export([init/2, read_roster_version/2, write_roster_version/4, + get_roster/2, get_roster_by_jid/3, + roster_subscribe/4, get_roster_by_jid_with_groups/3, + remove_user/2, update_roster/4, del_roster/3, transaction/2, + read_subscription_and_groups/3, get_only_items/2, + import/1, import/2, export/1]). + +-include("jlib.hrl"). +-include("mod_roster.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ok. + +read_roster_version(LUser, LServer) -> + case sql_queries:get_roster_version(LServer, LUser) of + {selected, [{Version}]} -> Version; + {selected, []} -> error + end. + +write_roster_version(LUser, LServer, InTransaction, Ver) -> + Username = ejabberd_sql:escape(LUser), + EVer = ejabberd_sql:escape(Ver), + if InTransaction -> + sql_queries:set_roster_version(Username, EVer); + true -> + sql_queries:sql_transaction( + LServer, + fun () -> + sql_queries:set_roster_version(Username, EVer) + end) + end. + +get_roster(LUser, LServer) -> + case catch sql_queries:get_roster(LServer, LUser) of + {selected, Items} when is_list(Items) -> + JIDGroups = case catch sql_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), + 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); + _ -> + [] + end. + +get_roster_by_jid(LUser, LServer, LJID) -> + {selected, Res} = + sql_queries:get_roster_by_jid(LServer, LUser, jid:to_string(LJID)), + case Res of + [] -> + #roster{usj = {LUser, LServer, LJID}, + us = {LUser, LServer}, jid = LJID}; + [I] -> + R = raw_to_record(LServer, I), + case R of + %% Bad JID in database: + error -> + #roster{usj = {LUser, LServer, LJID}, + us = {LUser, LServer}, jid = LJID}; + _ -> + R#roster{usj = {LUser, LServer, LJID}, + us = {LUser, LServer}, jid = LJID, name = <<"">>} + end + end. + +get_only_items(LUser, LServer) -> + case catch sql_queries:get_roster(LServer, LUser) of + {selected, Is} when is_list(Is) -> + lists:map(fun(I) -> raw_to_record(LServer, I) end, Is); + _ -> [] + end. + +roster_subscribe(_LUser, _LServer, _LJID, Item) -> + ItemVals = record_to_row(Item), + sql_queries:roster_subscribe(ItemVals). + +transaction(LServer, F) -> + ejabberd_sql:sql_transaction(LServer, F). + +get_roster_by_jid_with_groups(LUser, LServer, LJID) -> + SJID = jid:to_string(LJID), + case sql_queries:get_roster_by_jid(LServer, LUser, SJID) of + {selected, [I]} -> + R = raw_to_record(LServer, I), + Groups = + case sql_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. + +remove_user(LUser, LServer) -> + sql_queries:del_user_roster_t(LServer, LUser), + {atomic, ok}. + +update_roster(LUser, LServer, LJID, Item) -> + SJID = jid:to_string(LJID), + ItemVals = record_to_row(Item), + ItemGroups = Item#roster.groups, + sql_queries:update_roster(LServer, LUser, SJID, ItemVals, + ItemGroups). + +del_roster(LUser, LServer, LJID) -> + SJID = jid:to_string(LJID), + sql_queries:del_roster(LServer, LUser, SJID). + +read_subscription_and_groups(LUser, LServer, LJID) -> + SJID = jid:to_string(LJID), + case catch sql_queries:get_subscription(LServer, LUser, SJID) of + {selected, [{SSubscription}]} -> + Subscription = case SSubscription of + <<"B">> -> both; + <<"T">> -> to; + <<"F">> -> from; + _ -> none + end, + Groups = case catch sql_queries:get_rostergroup_by_jid( + LServer, LUser, SJID) of + {selected, JGrps} when is_list(JGrps) -> + [JGrp || {JGrp} <- JGrps]; + _ -> [] + end, + {Subscription, Groups}; + _ -> + error + end. + +export(_Server) -> + [{roster, + fun(Host, #roster{usj = {LUser, LServer, LJID}} = R) + when LServer == Host -> + Username = ejabberd_sql:escape(LUser), + SJID = ejabberd_sql:escape(jid:to_string(LJID)), + ItemVals = record_to_string(R), + ItemGroups = groups_to_string(R), + sql_queries:update_roster_sql(Username, SJID, + ItemVals, ItemGroups); + (_Host, _R) -> + [] + end}, + {roster_version, + fun(Host, #roster_version{us = {LUser, LServer}, version = Ver}) + when LServer == Host -> + Username = ejabberd_sql:escape(LUser), + SVer = ejabberd_sql:escape(Ver), + [[<<"delete from roster_version where username='">>, + Username, <<"';">>], + [<<"insert into roster_version(username, version) values('">>, + Username, <<"', '">>, SVer, <<"');">>]]; + (_Host, _R) -> + [] + end}]. + +import(LServer) -> + [{<<"select username, jid, nick, subscription, " + "ask, askmessage, server, subscribe, type from rosterusers;">>, + fun([LUser, JID|_] = Row) -> + Item = raw_to_record(LServer, Row), + Username = ejabberd_sql:escape(LUser), + SJID = ejabberd_sql:escape(JID), + {selected, _, Rows} = + ejabberd_sql:sql_query_t( + [<<"select grp from rostergroups where username='">>, + Username, <<"' and jid='">>, SJID, <<"'">>]), + Groups = [Grp || [Grp] <- Rows], + Item#roster{groups = Groups} + end}, + {<<"select username, version from roster_version;">>, + fun([LUser, Ver]) -> + #roster_version{us = {LUser, LServer}, version = Ver} + end}]. + +import(_, _) -> + pass. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +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}); +raw_to_record(LServer, + {User, SJID, Nick, SSubscription, SAsk, SAskMessage, + _SServer, _SSubscribe, _SType}) -> + case jid:from_string(SJID) of + error -> error; + JID -> + LJID = jid:tolower(JID), + Subscription = case SSubscription of + <<"B">> -> both; + <<"T">> -> to; + <<"F">> -> from; + _ -> none + end, + Ask = case SAsk of + <<"S">> -> subscribe; + <<"U">> -> unsubscribe; + <<"B">> -> both; + <<"O">> -> out; + <<"I">> -> in; + _ -> none + end, + #roster{usj = {User, LServer, LJID}, + us = {User, LServer}, jid = LJID, name = Nick, + subscription = Subscription, ask = Ask, + askmessage = SAskMessage} + end. + +record_to_string(#roster{us = {User, _Server}, + jid = JID, name = Name, subscription = Subscription, + ask = Ask, askmessage = AskMessage}) -> + Username = ejabberd_sql:escape(User), + SJID = + ejabberd_sql:escape(jid:to_string(jid:tolower(JID))), + Nick = ejabberd_sql:escape(Name), + 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, + SAskMessage = ejabberd_sql:escape(AskMessage), + [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_sql:escape(User), + SJID = + ejabberd_sql:escape(jid:to_string(jid:tolower(JID))), + lists:foldl(fun (<<"">>, Acc) -> Acc; + (Group, Acc) -> + G = ejabberd_sql:escape(Group), + [[Username, SJID, G] | Acc] + end, + [], Groups). diff --git a/src/mod_shared_roster.erl b/src/mod_shared_roster.erl index cde0f66b8..6670cf77b 100644 --- a/src/mod_shared_roster.erl +++ b/src/mod_shared_roster.erl @@ -38,7 +38,7 @@ 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, + is_user_in_group/3, add_user_to_group/3, opts_to_binary/1, remove_user_from_group/3, mod_opt_type/1]). -include("ejabberd.hrl"). @@ -52,25 +52,28 @@ -include("ejabberd_web_admin.hrl"). --record(sr_group, {group_host = {<<"">>, <<"">>} :: {'$1' | binary(), '$2' | binary()}, - opts = [] :: list() | '_' | '$2'}). - --record(sr_user, {us = {<<"">>, <<"">>} :: {binary(), binary()}, - group_host = {<<"">>, <<"">>} :: {binary(), binary()}}). +-include("mod_shared_roster.hrl"). + +-type group_options() :: [{atom(), any()}]. +-callback init(binary(), gen_mod:opts()) -> any(). +-callback import(binary(), #sr_user{} | #sr_group{}) -> ok | pass. +-callback list_groups(binary()) -> [binary()]. +-callback groups_with_opts(binary()) -> [{binary(), group_options()}]. +-callback create_group(binary(), binary(), group_options()) -> {atomic, any()}. +-callback delete_group(binary(), binary()) -> {atomic, any()}. +-callback get_group_opts(binary(), binary()) -> group_options() | error. +-callback set_group_opts(binary(), binary(), group_options()) -> {atomic, any()}. +-callback get_user_groups({binary(), binary()}, binary()) -> [binary()]. +-callback get_group_explicit_users(binary(), binary()) -> [{binary(), binary()}]. +-callback get_user_displayed_groups(binary(), binary(), group_options()) -> + [{binary(), group_options()}]. +-callback is_user_in_group({binary(), binary()}, binary(), binary()) -> boolean(). +-callback add_user_to_group(binary(), {binary(), binary()}, binary()) -> {atomic, any()}. +-callback remove_user_from_group(binary(), {binary(), binary()}, binary()) -> {atomic, any()}. start(Host, Opts) -> - case gen_mod:db_type(Host, Opts) of - mnesia -> - mnesia:create_table(sr_group, - [{disc_copies, [node()]}, - {attributes, record_info(fields, sr_group)}]), - mnesia:create_table(sr_user, - [{disc_copies, [node()]}, {type, bag}, - {attributes, record_info(fields, sr_user)}]), - update_tables(), - mnesia:add_table_index(sr_user, group_host); - _ -> ok - end, + Mod = gen_mod:db_mod(Host, Opts, ?MODULE), + Mod:init(Host, Opts), ejabberd_hooks:add(webadmin_menu_host, Host, ?MODULE, webadmin_menu, 70), ejabberd_hooks:add(webadmin_page_host, Host, ?MODULE, @@ -206,11 +209,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. @@ -391,195 +394,36 @@ process_subscription(Direction, User, Server, JID, end. list_groups(Host) -> - list_groups(Host, gen_mod:db_type(Host, ?MODULE)). - -list_groups(Host, mnesia) -> - mnesia:dirty_select(sr_group, - [{#sr_group{group_host = {'$1', '$2'}, _ = '_'}, - [{'==', '$2', Host}], ['$1']}]); -list_groups(Host, riak) -> - case ejabberd_riak:get_keys_by_index(sr_group, <<"host">>, Host) of - {ok, Gs} -> - [G || {G, _} <- Gs]; - _ -> - [] - end; -list_groups(Host, odbc) -> - case ejabberd_odbc:sql_query(Host, - [<<"select name from sr_group;">>]) - of - {selected, [<<"name">>], Rs} -> [G || [G] <- Rs]; - _ -> [] - end. + Mod = gen_mod:db_mod(Host, ?MODULE), + Mod:list_groups(Host). groups_with_opts(Host) -> - groups_with_opts(Host, gen_mod:db_type(Host, ?MODULE)). - -groups_with_opts(Host, mnesia) -> - Gs = mnesia:dirty_select(sr_group, - [{#sr_group{group_host = {'$1', Host}, opts = '$2', - _ = '_'}, - [], [['$1', '$2']]}]), - lists:map(fun ([G, O]) -> {G, O} end, Gs); -groups_with_opts(Host, riak) -> - case ejabberd_riak:get_by_index(sr_group, sr_group_schema(), - <<"host">>, Host) of - {ok, Rs} -> - [{G, O} || #sr_group{group_host = {G, _}, opts = O} <- Rs]; - _ -> - [] - end; -groups_with_opts(Host, odbc) -> - case ejabberd_odbc:sql_query(Host, - [<<"select name, opts from sr_group;">>]) - of - {selected, [<<"name">>, <<"opts">>], Rs} -> - [{G, opts_to_binary(ejabberd_odbc:decode_term(Opts))} - || [G, Opts] <- Rs]; - _ -> [] - end. + Mod = gen_mod:db_mod(Host, ?MODULE), + Mod:groups_with_opts(Host). create_group(Host, Group) -> create_group(Host, Group, []). create_group(Host, Group, Opts) -> - create_group(Host, Group, Opts, - gen_mod:db_type(Host, ?MODULE)). - -create_group(Host, Group, Opts, mnesia) -> - R = #sr_group{group_host = {Group, Host}, opts = Opts}, - F = fun () -> mnesia:write(R) end, - mnesia:transaction(F); -create_group(Host, Group, Opts, riak) -> - {atomic, ejabberd_riak:put(#sr_group{group_host = {Group, Host}, - opts = Opts}, - sr_group_schema(), - [{'2i', [{<<"host">>, Host}]}])}; -create_group(Host, Group, Opts, odbc) -> - SGroup = ejabberd_odbc:escape(Group), - SOpts = ejabberd_odbc:encode_term(Opts), - F = fun () -> - odbc_queries:update_t(<<"sr_group">>, - [<<"name">>, <<"opts">>], [SGroup, SOpts], - [<<"name='">>, SGroup, <<"'">>]) - end, - ejabberd_odbc:sql_transaction(Host, F). + Mod = gen_mod:db_mod(Host, ?MODULE), + Mod:create_group(Host, Group, Opts). delete_group(Host, Group) -> - delete_group(Host, Group, - gen_mod:db_type(Host, ?MODULE)). - -delete_group(Host, Group, mnesia) -> - GroupHost = {Group, Host}, - F = fun () -> - mnesia:delete({sr_group, GroupHost}), - Users = mnesia:index_read(sr_user, GroupHost, - #sr_user.group_host), - lists:foreach(fun (UserEntry) -> - mnesia:delete_object(UserEntry) - end, - Users) - end, - mnesia:transaction(F); -delete_group(Host, Group, riak) -> - try - ok = ejabberd_riak:delete(sr_group, {Group, Host}), - ok = ejabberd_riak:delete_by_index(sr_user, <<"group_host">>, - {Group, Host}), - {atomic, ok} - catch _:{badmatch, Err} -> - {atomic, Err} - end; -delete_group(Host, Group, odbc) -> - SGroup = ejabberd_odbc:escape(Group), - F = fun () -> - ejabberd_odbc:sql_query_t([<<"delete from sr_group where name='">>, - SGroup, <<"';">>]), - ejabberd_odbc:sql_query_t([<<"delete from sr_user where grp='">>, - SGroup, <<"';">>]) - end, - case ejabberd_odbc:sql_transaction(Host, F) of - {atomic,{updated,_}} -> {atomic, ok}; - Res -> Res - end. + Mod = gen_mod:db_mod(Host, ?MODULE), + Mod:delete_group(Host, Group). get_group_opts(Host, Group) -> - get_group_opts(Host, Group, - gen_mod:db_type(Host, ?MODULE)). - -get_group_opts(Host, Group, mnesia) -> - case catch mnesia:dirty_read(sr_group, {Group, Host}) of - [#sr_group{opts = Opts}] -> Opts; - _ -> error - end; -get_group_opts(Host, Group, riak) -> - case ejabberd_riak:get(sr_group, sr_group_schema(), {Group, Host}) of - {ok, #sr_group{opts = Opts}} -> Opts; - _ -> error - end; -get_group_opts(Host, Group, odbc) -> - SGroup = ejabberd_odbc:escape(Group), - case catch ejabberd_odbc:sql_query(Host, - [<<"select opts from sr_group where name='">>, - SGroup, <<"';">>]) - of - {selected, [<<"opts">>], [[SOpts]]} -> - opts_to_binary(ejabberd_odbc:decode_term(SOpts)); - _ -> error - end. + Mod = gen_mod:db_mod(Host, ?MODULE), + Mod:get_group_opts(Host, Group). set_group_opts(Host, Group, Opts) -> - set_group_opts(Host, Group, Opts, - gen_mod:db_type(Host, ?MODULE)). - -set_group_opts(Host, Group, Opts, mnesia) -> - R = #sr_group{group_host = {Group, Host}, opts = Opts}, - F = fun () -> mnesia:write(R) end, - mnesia:transaction(F); -set_group_opts(Host, Group, Opts, riak) -> - {atomic, ejabberd_riak:put(#sr_group{group_host = {Group, Host}, - opts = Opts}, - sr_group_schema(), - [{'2i', [{<<"host">>, Host}]}])}; -set_group_opts(Host, Group, Opts, odbc) -> - SGroup = ejabberd_odbc:escape(Group), - SOpts = ejabberd_odbc:encode_term(Opts), - F = fun () -> - odbc_queries:update_t(<<"sr_group">>, - [<<"name">>, <<"opts">>], [SGroup, SOpts], - [<<"name='">>, SGroup, <<"'">>]) - end, - ejabberd_odbc:sql_transaction(Host, F). + Mod = gen_mod:db_mod(Host, ?MODULE), + Mod:set_group_opts(Host, Group, Opts). get_user_groups(US) -> Host = element(2, US), - DBType = gen_mod:db_type(Host, ?MODULE), - get_user_groups(US, Host, DBType) ++ - get_special_users_groups(Host). - -get_user_groups(US, Host, mnesia) -> - case catch mnesia:dirty_read(sr_user, US) of - Rs when is_list(Rs) -> - [Group - || #sr_user{group_host = {Group, H}} <- Rs, H == Host]; - _ -> [] - end; -get_user_groups(US, Host, riak) -> - case ejabberd_riak:get_by_index(sr_user, sr_user_schema(), <<"us">>, US) of - {ok, Rs} -> - [Group || #sr_user{group_host = {Group, H}} <- Rs, H == Host]; - _ -> - [] - end; -get_user_groups(US, Host, odbc) -> - SJID = make_jid_s(US), - case catch ejabberd_odbc:sql_query(Host, - [<<"select grp from sr_user where jid='">>, - SJID, <<"';">>]) - of - {selected, [<<"grp">>], Rs} -> [G || [G] <- Rs]; - _ -> [] - end. + Mod = gen_mod:db_mod(Host, ?MODULE), + Mod:get_user_groups(US, Host) ++ get_special_users_groups(Host). is_group_enabled(Host1, Group1) -> {Host, Group} = split_grouphost(Host1, Group1), @@ -630,39 +474,8 @@ get_group_users(Host, Group, GroupOpts) -> ++ get_group_explicit_users(Host, Group). get_group_explicit_users(Host, Group) -> - get_group_explicit_users(Host, Group, - gen_mod:db_type(Host, ?MODULE)). - -get_group_explicit_users(Host, Group, mnesia) -> - Read = (catch mnesia:dirty_index_read(sr_user, - {Group, Host}, #sr_user.group_host)), - case Read of - Rs when is_list(Rs) -> [R#sr_user.us || R <- Rs]; - _ -> [] - end; -get_group_explicit_users(Host, Group, riak) -> - case ejabberd_riak:get_by_index(sr_user, sr_user_schema(), - <<"group_host">>, {Group, Host}) of - {ok, Rs} -> - [R#sr_user.us || R <- Rs]; - _ -> - [] - end; -get_group_explicit_users(Host, Group, odbc) -> - SGroup = ejabberd_odbc:escape(Group), - case catch ejabberd_odbc:sql_query(Host, - [<<"select jid from sr_user where grp='">>, - SGroup, <<"';">>]) - of - {selected, [<<"jid">>], Rs} -> - lists:map(fun ([JID]) -> - {U, S, _} = - jid:tolower(jid:from_string(JID)), - {U, S} - end, - Rs); - _ -> [] - end. + Mod = gen_mod:db_mod(Host, ?MODULE), + Mod:get_group_explicit_users(Host, Group). get_group_name(Host1, Group1) -> {Host, Group} = split_grouphost(Host1, Group1), @@ -718,44 +531,10 @@ get_special_displayed_groups(GroupsOpts) -> %% for the list of groups of that server that user is member %% get the list of groups displayed get_user_displayed_groups(LUser, LServer, GroupsOpts) -> - Groups = get_user_displayed_groups(LUser, LServer, - GroupsOpts, - gen_mod:db_type(LServer, ?MODULE)), + Mod = gen_mod:db_mod(LServer, ?MODULE), + Groups = Mod:get_user_displayed_groups(LUser, LServer, GroupsOpts), displayed_groups(GroupsOpts, Groups). -get_user_displayed_groups(LUser, LServer, GroupsOpts, - mnesia) -> - case catch mnesia:dirty_read(sr_user, {LUser, LServer}) - of - Rs when is_list(Rs) -> - [{Group, proplists:get_value(Group, GroupsOpts, [])} - || #sr_user{group_host = {Group, H}} <- Rs, - H == LServer]; - _ -> [] - end; -get_user_displayed_groups(LUser, LServer, GroupsOpts, - riak) -> - case ejabberd_riak:get_by_index(sr_user, sr_user_schema(), - <<"us">>, {LUser, LServer}) of - {ok, Rs} -> - [{Group, proplists:get_value(Group, GroupsOpts, [])} - || #sr_user{group_host = {Group, _}} <- Rs]; - _ -> - [] - end; -get_user_displayed_groups(LUser, LServer, GroupsOpts, - odbc) -> - SJID = make_jid_s(LUser, LServer), - case catch ejabberd_odbc:sql_query(LServer, - [<<"select grp from sr_user where jid='">>, - SJID, <<"';">>]) - of - {selected, [<<"grp">>], Rs} -> - [{Group, proplists:get_value(Group, GroupsOpts, [])} - || [Group] <- Rs]; - _ -> [] - end. - %% @doc Get the list of groups that are displayed to this user get_user_displayed_groups(US) -> Host = element(2, US), @@ -779,42 +558,12 @@ get_user_displayed_groups(US) -> is_group_enabled(Host, Group)]. is_user_in_group(US, Group, Host) -> - is_user_in_group(US, Group, Host, - gen_mod:db_type(Host, ?MODULE)). - -is_user_in_group(US, Group, Host, mnesia) -> - case catch mnesia:dirty_match_object(#sr_user{us = US, - group_host = {Group, Host}}) - of - [] -> lists:member(US, get_group_users(Host, Group)); - _ -> true - end; -is_user_in_group(US, Group, Host, riak) -> - case ejabberd_riak:get_by_index(sr_user, sr_user_schema(), <<"us">>, US) of - {ok, Rs} -> - case lists:any( - fun(#sr_user{group_host = {G, H}}) -> - (Group == G) and (Host == H) - end, Rs) of - false -> - lists:member(US, get_group_users(Host, Group)); - true -> - true - end; - _Err -> - false - end; -is_user_in_group(US, Group, Host, odbc) -> - SJID = make_jid_s(US), - SGroup = ejabberd_odbc:escape(Group), - case catch ejabberd_odbc:sql_query(Host, - [<<"select * from sr_user where jid='">>, - SJID, <<"' and grp='">>, SGroup, - <<"';">>]) - of - {selected, _, []} -> - lists:member(US, get_group_users(Host, Group)); - _ -> true + Mod = gen_mod:db_mod(Host, ?MODULE), + case Mod:is_user_in_group(US, Group, Host) of + false -> + lists:member(US, get_group_users(Host, Group)); + true -> + true end. %% @spec (Host::string(), {User::string(), Server::string()}, Group::string()) -> {atomic, ok} @@ -837,31 +586,10 @@ add_user_to_group(Host, US, Group) -> push_displayed_to_user(LUser, LServer, Host, both, DisplayedGroups), broadcast_user_to_displayed(LUser, LServer, Host, both, DisplayedToGroups), broadcast_displayed_to_user(LUser, LServer, Host, both, DisplayedGroups), - add_user_to_group(Host, US, Group, gen_mod:db_type(Host, ?MODULE)) + Mod = gen_mod:db_mod(Host, ?MODULE), + Mod:add_user_to_group(Host, US, Group) end. -add_user_to_group(Host, US, Group, mnesia) -> - R = #sr_user{us = US, group_host = {Group, Host}}, - F = fun () -> mnesia:write(R) end, - mnesia:transaction(F); -add_user_to_group(Host, US, Group, riak) -> - {atomic, ejabberd_riak:put( - #sr_user{us = US, group_host = {Group, Host}}, - sr_user_schema(), - [{i, {US, {Group, Host}}}, - {'2i', [{<<"us">>, US}, - {<<"group_host">>, {Group, Host}}]}])}; -add_user_to_group(Host, US, Group, odbc) -> - SJID = make_jid_s(US), - SGroup = ejabberd_odbc:escape(Group), - F = fun () -> - odbc_queries:update_t(<<"sr_user">>, - [<<"jid">>, <<"grp">>], [SJID, SGroup], - [<<"jid='">>, SJID, <<"' and grp='">>, - SGroup, <<"'">>]) - end, - ejabberd_odbc:sql_transaction(Host, F). - get_displayed_groups(Group, LServer) -> GroupsOpts = groups_with_opts(LServer), GroupOpts = proplists:get_value(Group, GroupsOpts, []), @@ -894,8 +622,8 @@ remove_user_from_group(Host, US, Group) -> end, (?MODULE):set_group_opts(Host, Group, NewGroupOpts); nomatch -> - Result = remove_user_from_group(Host, US, Group, - gen_mod:db_type(Host, ?MODULE)), + Mod = gen_mod:db_mod(Host, ?MODULE), + Result = Mod:remove_user_from_group(Host, US, Group), DisplayedToGroups = displayed_to_groups(Group, Host), DisplayedGroups = get_displayed_groups(Group, LServer), push_user_to_displayed(LUser, LServer, Group, Host, remove, DisplayedToGroups), @@ -903,23 +631,6 @@ remove_user_from_group(Host, US, Group) -> Result end. -remove_user_from_group(Host, US, Group, mnesia) -> - R = #sr_user{us = US, group_host = {Group, Host}}, - F = fun () -> mnesia:delete_object(R) end, - mnesia:transaction(F); -remove_user_from_group(Host, US, Group, riak) -> - {atomic, ejabberd_riak:delete(sr_group, {US, {Group, Host}})}; -remove_user_from_group(Host, US, Group, odbc) -> - SJID = make_jid_s(US), - SGroup = ejabberd_odbc:escape(Group), - F = fun () -> - ejabberd_odbc:sql_query_t([<<"delete from sr_user where jid='">>, - SJID, <<"' and grp='">>, SGroup, - <<"';">>]), - ok - end, - ejabberd_odbc:sql_transaction(Host, F). - push_members_to_user(LUser, LServer, Group, Host, Subscription) -> GroupsOpts = groups_with_opts(LServer), @@ -1160,7 +871,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">>)]; @@ -1254,7 +965,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 @@ -1385,13 +1096,6 @@ displayed_groups_update(Members, DisplayedGroups, Subscription) -> end end, Members). -make_jid_s(U, S) -> - ejabberd_odbc:escape(jid:to_string(jid:tolower(jid:make(U, - S, - <<"">>)))). - -make_jid_s({U, S}) -> make_jid_s(U, S). - opts_to_binary(Opts) -> lists:map( fun({name, Name}) -> @@ -1404,105 +1108,17 @@ opts_to_binary(Opts) -> Opt end, Opts). -sr_group_schema() -> - {record_info(fields, sr_group), #sr_group{}}. - -sr_user_schema() -> - {record_info(fields, sr_user), #sr_user{}}. - -update_tables() -> - update_sr_group_table(), - update_sr_user_table(). - -update_sr_group_table() -> - Fields = record_info(fields, sr_group), - case mnesia:table_info(sr_group, attributes) of - Fields -> - ejabberd_config:convert_table_to_binary( - sr_group, Fields, set, - fun(#sr_group{group_host = {G, _}}) -> G end, - fun(#sr_group{group_host = {G, H}, - opts = Opts} = R) -> - R#sr_group{group_host = {iolist_to_binary(G), - iolist_to_binary(H)}, - opts = opts_to_binary(Opts)} - end); - _ -> - ?INFO_MSG("Recreating sr_group table", []), - mnesia:transform_table(sr_group, ignore, Fields) - end. - -update_sr_user_table() -> - Fields = record_info(fields, sr_user), - case mnesia:table_info(sr_user, attributes) of - Fields -> - ejabberd_config:convert_table_to_binary( - sr_user, Fields, bag, - fun(#sr_user{us = {U, _}}) -> U end, - fun(#sr_user{us = {U, S}, group_host = {G, H}} = R) -> - R#sr_user{us = {iolist_to_binary(U), iolist_to_binary(S)}, - group_host = {iolist_to_binary(G), - iolist_to_binary(H)}} - end); - _ -> - ?INFO_MSG("Recreating sr_user table", []), - mnesia:transform_table(sr_user, ignore, Fields) - end. - -export(_Server) -> - [{sr_group, - fun(Host, #sr_group{group_host = {Group, LServer}, opts = Opts}) - when LServer == Host -> - SGroup = ejabberd_odbc:escape(Group), - SOpts = ejabberd_odbc:encode_term(Opts), - [[<<"delete from sr_group where name='">>, Group, <<"';">>], - [<<"insert into sr_group(name, opts) values ('">>, - SGroup, <<"', '">>, SOpts, <<"');">>]]; - (_Host, _R) -> - [] - end}, - {sr_user, - fun(Host, #sr_user{us = {U, S}, group_host = {Group, LServer}}) - when LServer == Host -> - SGroup = ejabberd_odbc:escape(Group), - SJID = ejabberd_odbc:escape( - 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 ('">>, - SJID, <<"', '">>, SGroup, <<"');">>]]; - (_Host, _R) -> - [] - end}]. +export(LServer) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:export(LServer). import(LServer) -> - [{<<"select name, opts from sr_group;">>, - fun([Group, SOpts]) -> - #sr_group{group_host = {Group, LServer}, - opts = ejabberd_odbc:decode_term(SOpts)} - end}, - {<<"select jid, grp from sr_user;">>, - fun([SJID, Group]) -> - #jid{luser = U, lserver = S} = jid:from_string(SJID), - #sr_user{us = {U, S}, group_host = {Group, LServer}} - end}]. - -import(_LServer, mnesia, #sr_group{} = G) -> - mnesia:dirty_write(G); - -import(_LServer, mnesia, #sr_user{} = U) -> - mnesia:dirty_write(U); -import(_LServer, riak, #sr_group{group_host = {_, Host}} = G) -> - ejabberd_riak:put(G, sr_group_schema(), [{'2i', [{<<"host">>, Host}]}]); -import(_LServer, riak, #sr_user{us = US, group_host = {Group, Host}} = User) -> - ejabberd_riak:put(User, sr_user_schema(), - [{i, {US, {Group, Host}}}, - {'2i', [{<<"us">>, US}, - {<<"group_host">>, {Group, Host}}]}]); -import(_, _, _) -> - pass. + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:import(LServer). + +import(LServer, DBType, Data) -> + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:import(LServer, Data). 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 a4ac65c10..34588b90c 100644 --- a/src/mod_shared_roster_ldap.erl +++ b/src/mod_shared_roster_ldap.erl @@ -47,16 +47,13 @@ -include("logger.hrl"). -include("jlib.hrl"). -include("mod_roster.hrl"). - -include("eldap.hrl"). -define(CACHE_SIZE, 1000). - --define(USER_CACHE_VALIDITY, 300). - +-define(USER_CACHE_VALIDITY, 300). %% in seconds -define(GROUP_CACHE_VALIDITY, 300). - --define(LDAP_SEARCH_TIMEOUT, 5). +-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(), @@ -334,12 +331,11 @@ 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}} + fun () -> search_group_info(State, Group) end) of + {ok, #group_info{members = Members}} when Members /= undefined -> - Members; - _ -> [] + Members; + _ -> [] end. get_group_name(Host, Group) -> @@ -381,79 +377,49 @@ search_group_info(State, Group) -> 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; + [] -> + 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 = - jid: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))}} + {GroupDesc, MembersLists} = lists:foldl(fun(Entry, Acc) -> + extract_members(State, Extractor, AuthChecker, Entry, Acc) + end, + {Group, []}, LDAPEntries), + {ok, #group_info{desc = GroupDesc, members = lists:usort(lists:flatten(MembersLists))}} + end. + +extract_members(State, Extractor, AuthChecker, #eldap_entry{attributes = Attrs}, {DescAcc, JIDsAcc}) -> + Host = State#state.host, + 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 = jid: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. search_user_name(State, User) -> diff --git a/src/mod_shared_roster_mnesia.erl b/src/mod_shared_roster_mnesia.erl new file mode 100644 index 000000000..ca2e55e2f --- /dev/null +++ b/src/mod_shared_roster_mnesia.erl @@ -0,0 +1,167 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 14 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_shared_roster_mnesia). + +-behaviour(mod_shared_roster). + +%% API +-export([init/2, list_groups/1, groups_with_opts/1, create_group/3, + delete_group/2, get_group_opts/2, set_group_opts/3, + get_user_groups/2, get_group_explicit_users/2, + get_user_displayed_groups/3, is_user_in_group/3, + add_user_to_group/3, remove_user_from_group/3, import/2]). + +-include("jlib.hrl"). +-include("mod_roster.hrl"). +-include("mod_shared_roster.hrl"). +-include("logger.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + mnesia:create_table(sr_group, + [{disc_copies, [node()]}, + {attributes, record_info(fields, sr_group)}]), + mnesia:create_table(sr_user, + [{disc_copies, [node()]}, {type, bag}, + {attributes, record_info(fields, sr_user)}]), + update_tables(), + mnesia:add_table_index(sr_user, group_host). + +list_groups(Host) -> + mnesia:dirty_select(sr_group, + [{#sr_group{group_host = {'$1', '$2'}, _ = '_'}, + [{'==', '$2', Host}], ['$1']}]). + +groups_with_opts(Host) -> + Gs = mnesia:dirty_select(sr_group, + [{#sr_group{group_host = {'$1', Host}, opts = '$2', + _ = '_'}, + [], [['$1', '$2']]}]), + lists:map(fun ([G, O]) -> {G, O} end, Gs). + +create_group(Host, Group, Opts) -> + R = #sr_group{group_host = {Group, Host}, opts = Opts}, + F = fun () -> mnesia:write(R) end, + mnesia:transaction(F). + +delete_group(Host, Group) -> + GroupHost = {Group, Host}, + F = fun () -> + mnesia:delete({sr_group, GroupHost}), + Users = mnesia:index_read(sr_user, GroupHost, + #sr_user.group_host), + lists:foreach(fun (UserEntry) -> + mnesia:delete_object(UserEntry) + end, + Users) + end, + mnesia:transaction(F). + +get_group_opts(Host, Group) -> + case catch mnesia:dirty_read(sr_group, {Group, Host}) of + [#sr_group{opts = Opts}] -> Opts; + _ -> error + end. + +set_group_opts(Host, Group, Opts) -> + R = #sr_group{group_host = {Group, Host}, opts = Opts}, + F = fun () -> mnesia:write(R) end, + mnesia:transaction(F). + +get_user_groups(US, Host) -> + case catch mnesia:dirty_read(sr_user, US) of + Rs when is_list(Rs) -> + [Group || #sr_user{group_host = {Group, H}} <- Rs, H == Host]; + _ -> + [] + end. + +get_group_explicit_users(Host, Group) -> + Read = (catch mnesia:dirty_index_read(sr_user, + {Group, Host}, #sr_user.group_host)), + case Read of + Rs when is_list(Rs) -> [R#sr_user.us || R <- Rs]; + _ -> [] + end. + +get_user_displayed_groups(LUser, LServer, GroupsOpts) -> + case catch mnesia:dirty_read(sr_user, {LUser, LServer}) of + Rs when is_list(Rs) -> + [{Group, proplists:get_value(Group, GroupsOpts, [])} + || #sr_user{group_host = {Group, H}} <- Rs, + H == LServer]; + _ -> + [] + end. + +is_user_in_group(US, Group, Host) -> + case mnesia:dirty_match_object( + #sr_user{us = US, group_host = {Group, Host}}) of + [] -> false; + _ -> true + end. + +add_user_to_group(Host, US, Group) -> + R = #sr_user{us = US, group_host = {Group, Host}}, + F = fun () -> mnesia:write(R) end, + mnesia:transaction(F). + +remove_user_from_group(Host, US, Group) -> + R = #sr_user{us = US, group_host = {Group, Host}}, + F = fun () -> mnesia:delete_object(R) end, + mnesia:transaction(F). + +import(_LServer, #sr_group{} = G) -> + mnesia:dirty_write(G); +import(_LServer, #sr_user{} = U) -> + mnesia:dirty_write(U). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +update_tables() -> + update_sr_group_table(), + update_sr_user_table(). + +update_sr_group_table() -> + Fields = record_info(fields, sr_group), + case mnesia:table_info(sr_group, attributes) of + Fields -> + ejabberd_config:convert_table_to_binary( + sr_group, Fields, set, + fun(#sr_group{group_host = {G, _}}) -> G end, + fun(#sr_group{group_host = {G, H}, + opts = Opts} = R) -> + R#sr_group{group_host = {iolist_to_binary(G), + iolist_to_binary(H)}, + opts = mod_shared_roster:opts_to_binary(Opts)} + end); + _ -> + ?INFO_MSG("Recreating sr_group table", []), + mnesia:transform_table(sr_group, ignore, Fields) + end. + +update_sr_user_table() -> + Fields = record_info(fields, sr_user), + case mnesia:table_info(sr_user, attributes) of + Fields -> + ejabberd_config:convert_table_to_binary( + sr_user, Fields, bag, + fun(#sr_user{us = {U, _}}) -> U end, + fun(#sr_user{us = {U, S}, group_host = {G, H}} = R) -> + R#sr_user{us = {iolist_to_binary(U), iolist_to_binary(S)}, + group_host = {iolist_to_binary(G), + iolist_to_binary(H)}} + end); + _ -> + ?INFO_MSG("Recreating sr_user table", []), + mnesia:transform_table(sr_user, ignore, Fields) + end. diff --git a/src/mod_shared_roster_riak.erl b/src/mod_shared_roster_riak.erl new file mode 100644 index 000000000..0df35e37d --- /dev/null +++ b/src/mod_shared_roster_riak.erl @@ -0,0 +1,139 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 14 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_shared_roster_riak). + +-behaviour(mod_shared_roster). + +%% API +-export([init/2, list_groups/1, groups_with_opts/1, create_group/3, + delete_group/2, get_group_opts/2, set_group_opts/3, + get_user_groups/2, get_group_explicit_users/2, + get_user_displayed_groups/3, is_user_in_group/3, + add_user_to_group/3, remove_user_from_group/3, import/2]). + +-include("jlib.hrl"). +-include("mod_roster.hrl"). +-include("mod_shared_roster.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ok. + +list_groups(Host) -> + case ejabberd_riak:get_keys_by_index(sr_group, <<"host">>, Host) of + {ok, Gs} -> + [G || {G, _} <- Gs]; + _ -> + [] + end. + +groups_with_opts(Host) -> + case ejabberd_riak:get_by_index(sr_group, sr_group_schema(), + <<"host">>, Host) of + {ok, Rs} -> + [{G, O} || #sr_group{group_host = {G, _}, opts = O} <- Rs]; + _ -> + [] + end. + +create_group(Host, Group, Opts) -> + {atomic, ejabberd_riak:put(#sr_group{group_host = {Group, Host}, + opts = Opts}, + sr_group_schema(), + [{'2i', [{<<"host">>, Host}]}])}. + +delete_group(Host, Group) -> + try + ok = ejabberd_riak:delete(sr_group, {Group, Host}), + ok = ejabberd_riak:delete_by_index(sr_user, <<"group_host">>, + {Group, Host}), + {atomic, ok} + catch _:{badmatch, Err} -> + {atomic, Err} + end. + +get_group_opts(Host, Group) -> + case ejabberd_riak:get(sr_group, sr_group_schema(), {Group, Host}) of + {ok, #sr_group{opts = Opts}} -> Opts; + _ -> error + end. + +set_group_opts(Host, Group, Opts) -> + {atomic, ejabberd_riak:put(#sr_group{group_host = {Group, Host}, + opts = Opts}, + sr_group_schema(), + [{'2i', [{<<"host">>, Host}]}])}. + +get_user_groups(US, Host) -> + case ejabberd_riak:get_by_index(sr_user, sr_user_schema(), <<"us">>, US) of + {ok, Rs} -> + [Group || #sr_user{group_host = {Group, H}} <- Rs, H == Host]; + _ -> + [] + end. + +get_group_explicit_users(Host, Group) -> + case ejabberd_riak:get_by_index(sr_user, sr_user_schema(), + <<"group_host">>, {Group, Host}) of + {ok, Rs} -> + [R#sr_user.us || R <- Rs]; + _ -> + [] + end. + +get_user_displayed_groups(LUser, LServer, GroupsOpts) -> + case ejabberd_riak:get_by_index(sr_user, sr_user_schema(), + <<"us">>, {LUser, LServer}) of + {ok, Rs} -> + [{Group, proplists:get_value(Group, GroupsOpts, [])} + || #sr_user{group_host = {Group, _}} <- Rs]; + _ -> + [] + end. + +is_user_in_group(US, Group, Host) -> + case ejabberd_riak:get_by_index(sr_user, sr_user_schema(), <<"us">>, US) of + {ok, Rs} -> + lists:any( + fun(#sr_user{group_host = {G, H}}) -> + (Group == G) and (Host == H) + end, Rs); + _Err -> + false + end. + +add_user_to_group(Host, US, Group) -> + {atomic, ejabberd_riak:put( + #sr_user{us = US, group_host = {Group, Host}}, + sr_user_schema(), + [{i, {US, {Group, Host}}}, + {'2i', [{<<"us">>, US}, + {<<"group_host">>, {Group, Host}}]}])}. + +remove_user_from_group(Host, US, Group) -> + {atomic, ejabberd_riak:delete(sr_group, {US, {Group, Host}})}. + +import(_LServer, #sr_group{group_host = {_, Host}} = G) -> + ejabberd_riak:put(G, sr_group_schema(), [{'2i', [{<<"host">>, Host}]}]); +import(_LServer, #sr_user{us = US, group_host = {Group, Host}} = User) -> + ejabberd_riak:put(User, sr_user_schema(), + [{i, {US, {Group, Host}}}, + {'2i', [{<<"us">>, US}, + {<<"group_host">>, {Group, Host}}]}]). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +sr_group_schema() -> + {record_info(fields, sr_group), #sr_group{}}. + +sr_user_schema() -> + {record_info(fields, sr_user), #sr_user{}}. diff --git a/src/mod_shared_roster_sql.erl b/src/mod_shared_roster_sql.erl new file mode 100644 index 000000000..bd123cd4a --- /dev/null +++ b/src/mod_shared_roster_sql.erl @@ -0,0 +1,212 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 14 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_shared_roster_sql). + +-behaviour(mod_shared_roster). + +%% API +-export([init/2, list_groups/1, groups_with_opts/1, create_group/3, + delete_group/2, get_group_opts/2, set_group_opts/3, + get_user_groups/2, get_group_explicit_users/2, + get_user_displayed_groups/3, is_user_in_group/3, + add_user_to_group/3, remove_user_from_group/3, import/1, + import/2, export/1]). + +-include("jlib.hrl"). +-include("mod_roster.hrl"). +-include("mod_shared_roster.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ok. + +list_groups(Host) -> + case ejabberd_sql:sql_query( + Host, [<<"select name from sr_group;">>]) of + {selected, [<<"name">>], Rs} -> [G || [G] <- Rs]; + _ -> [] + end. + +groups_with_opts(Host) -> + case ejabberd_sql:sql_query(Host, + [<<"select name, opts from sr_group;">>]) + of + {selected, [<<"name">>, <<"opts">>], Rs} -> + [{G, mod_shared_roster:opts_to_binary(ejabberd_sql:decode_term(Opts))} + || [G, Opts] <- Rs]; + _ -> [] + end. + +create_group(Host, Group, Opts) -> + SGroup = ejabberd_sql:escape(Group), + SOpts = ejabberd_sql:encode_term(Opts), + F = fun () -> + sql_queries:update_t(<<"sr_group">>, + [<<"name">>, <<"opts">>], [SGroup, SOpts], + [<<"name='">>, SGroup, <<"'">>]) + end, + ejabberd_sql:sql_transaction(Host, F). + +delete_group(Host, Group) -> + SGroup = ejabberd_sql:escape(Group), + F = fun () -> + ejabberd_sql:sql_query_t([<<"delete from sr_group where name='">>, + SGroup, <<"';">>]), + ejabberd_sql:sql_query_t([<<"delete from sr_user where grp='">>, + SGroup, <<"';">>]) + end, + case ejabberd_sql:sql_transaction(Host, F) of + {atomic,{updated,_}} -> {atomic, ok}; + Res -> Res + end. + +get_group_opts(Host, Group) -> + SGroup = ejabberd_sql:escape(Group), + case catch ejabberd_sql:sql_query( + Host, + [<<"select opts from sr_group where name='">>, + SGroup, <<"';">>]) of + {selected, [<<"opts">>], [[SOpts]]} -> + mod_shared_roster:opts_to_binary(ejabberd_sql:decode_term(SOpts)); + _ -> error + end. + +set_group_opts(Host, Group, Opts) -> + SGroup = ejabberd_sql:escape(Group), + SOpts = ejabberd_sql:encode_term(Opts), + F = fun () -> + sql_queries:update_t(<<"sr_group">>, + [<<"name">>, <<"opts">>], [SGroup, SOpts], + [<<"name='">>, SGroup, <<"'">>]) + end, + ejabberd_sql:sql_transaction(Host, F). + +get_user_groups(US, Host) -> + SJID = make_jid_s(US), + case catch ejabberd_sql:sql_query( + Host, + [<<"select grp from sr_user where jid='">>, + SJID, <<"';">>]) of + {selected, [<<"grp">>], Rs} -> [G || [G] <- Rs]; + _ -> [] + end. + +get_group_explicit_users(Host, Group) -> + SGroup = ejabberd_sql:escape(Group), + case catch ejabberd_sql:sql_query( + Host, + [<<"select jid from sr_user where grp='">>, + SGroup, <<"';">>]) of + {selected, [<<"jid">>], Rs} -> + lists:map( + fun([JID]) -> + {U, S, _} = jid:tolower(jid:from_string(JID)), + {U, S} + end, Rs); + _ -> + [] + end. + +get_user_displayed_groups(LUser, LServer, GroupsOpts) -> + SJID = make_jid_s(LUser, LServer), + case catch ejabberd_sql:sql_query( + LServer, + [<<"select grp from sr_user where jid='">>, + SJID, <<"';">>]) of + {selected, [<<"grp">>], Rs} -> + [{Group, proplists:get_value(Group, GroupsOpts, [])} + || [Group] <- Rs]; + _ -> [] + end. + +is_user_in_group(US, Group, Host) -> + SJID = make_jid_s(US), + SGroup = ejabberd_sql:escape(Group), + case catch ejabberd_sql:sql_query(Host, + [<<"select * from sr_user where jid='">>, + SJID, <<"' and grp='">>, SGroup, + <<"';">>]) of + {selected, _, []} -> false; + _ -> true + end. + +add_user_to_group(Host, US, Group) -> + SJID = make_jid_s(US), + SGroup = ejabberd_sql:escape(Group), + F = fun () -> + sql_queries:update_t(<<"sr_user">>, + [<<"jid">>, <<"grp">>], [SJID, SGroup], + [<<"jid='">>, SJID, <<"' and grp='">>, + SGroup, <<"'">>]) + end, + ejabberd_sql:sql_transaction(Host, F). + +remove_user_from_group(Host, US, Group) -> + SJID = make_jid_s(US), + SGroup = ejabberd_sql:escape(Group), + F = fun () -> + ejabberd_sql:sql_query_t([<<"delete from sr_user where jid='">>, + SJID, <<"' and grp='">>, SGroup, + <<"';">>]), + ok + end, + ejabberd_sql:sql_transaction(Host, F). + +export(_Server) -> + [{sr_group, + fun(Host, #sr_group{group_host = {Group, LServer}, opts = Opts}) + when LServer == Host -> + SGroup = ejabberd_sql:escape(Group), + SOpts = ejabberd_sql:encode_term(Opts), + [[<<"delete from sr_group where name='">>, Group, <<"';">>], + [<<"insert into sr_group(name, opts) values ('">>, + SGroup, <<"', '">>, SOpts, <<"');">>]]; + (_Host, _R) -> + [] + end}, + {sr_user, + fun(Host, #sr_user{us = {U, S}, group_host = {Group, LServer}}) + when LServer == Host -> + SGroup = ejabberd_sql:escape(Group), + SJID = ejabberd_sql:escape( + 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 ('">>, + SJID, <<"', '">>, SGroup, <<"');">>]]; + (_Host, _R) -> + [] + end}]. + +import(LServer) -> + [{<<"select name, opts from sr_group;">>, + fun([Group, SOpts]) -> + #sr_group{group_host = {Group, LServer}, + opts = ejabberd_sql:decode_term(SOpts)} + end}, + {<<"select jid, grp from sr_user;">>, + fun([SJID, Group]) -> + #jid{luser = U, lserver = S} = jid:from_string(SJID), + #sr_user{us = {U, S}, group_host = {Group, LServer}} + end}]. + +import(_, _) -> + pass. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +make_jid_s(U, S) -> + ejabberd_sql:escape(jid:to_string(jid:tolower(jid:make(U, S, <<"">>)))). + +make_jid_s({U, S}) -> make_jid_s(U, S). diff --git a/src/mod_sic.erl b/src/mod_sic.erl index 3ffd9c917..b4eae0daf 100644 --- a/src/mod_sic.erl +++ b/src/mod_sic.erl @@ -60,8 +60,9 @@ process_local_iq(#jid{user = User, server = Server, _To, #iq{type = get, sub_el = _SubEl} = IQ) -> get_ip({User, Server, Resource}, IQ); process_local_iq(_From, _To, - #iq{type = set, sub_el = SubEl} = IQ) -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}. + #iq{type = set, sub_el = SubEl, lang = Lang} = IQ) -> + Txt = <<"Value 'set' of 'type' attribute is not allowed">>, + IQ#iq{type = error, sub_el = [SubEl, ?ERRT_NOT_ALLOWED(Lang, Txt)]}. process_sm_iq(#jid{user = User, server = Server, resource = Resource}, @@ -69,14 +70,17 @@ process_sm_iq(#jid{user = User, server = Server, #iq{type = get, sub_el = _SubEl} = IQ) -> get_ip({User, Server, Resource}, IQ); process_sm_iq(_From, _To, - #iq{type = get, sub_el = SubEl} = IQ) -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_FORBIDDEN]}; + #iq{type = get, sub_el = SubEl, lang = Lang} = IQ) -> + Txt = <<"Query to another users is forbidden">>, + IQ#iq{type = error, sub_el = [SubEl, ?ERRT_FORBIDDEN(Lang, Txt)]}; process_sm_iq(_From, _To, - #iq{type = set, sub_el = SubEl} = IQ) -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}. + #iq{type = set, sub_el = SubEl, lang = Lang} = IQ) -> + Txt = <<"Value 'set' of 'type' attribute is not allowed">>, + IQ#iq{type = error, sub_el = [SubEl, ?ERRT_NOT_ALLOWED(Lang, Txt)]}. get_ip({User, Server, Resource}, - #iq{sub_el = + #iq{lang = Lang, + sub_el = #xmlel{name = Name, attrs = Attrs} = SubEl} = IQ) -> case ejabberd_sm:get_user_ip(User, Server, Resource) of @@ -88,8 +92,9 @@ get_ip({User, Server, Resource}, [{xmlcdata, iolist_to_binary(jlib:ip_to_list(IP))}]}]}; _ -> + Txt = <<"User session not found">>, IQ#iq{type = error, - sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]} + sub_el = [SubEl, ?ERRT_INTERNAL_SERVER_ERROR(Lang, Txt)]} end. mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; diff --git a/src/mod_stats.erl b/src/mod_stats.erl index c9f659d3c..c14cf8d15 100644 --- a/src/mod_stats.erl +++ b/src/mod_stats.erl @@ -50,17 +50,18 @@ stop(Host) -> process_local_iq(_From, To, #iq{id = _ID, type = Type, xmlns = XMLNS, - sub_el = SubEl} = + sub_el = SubEl, lang = Lang} = IQ) -> case Type of set -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; + Txt = <<"Value 'set' of 'type' attribute is not allowed">>, + IQ#iq{type = error, sub_el = [SubEl, ?ERRT_NOT_ALLOWED(Lang, Txt)]}; 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 + case get_local_stats(To#jid.server, Node, Names, Lang) of {result, Res} -> IQ#iq{type = result, sub_el = @@ -76,7 +77,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]) @@ -87,18 +88,18 @@ get_names([_ | Els], Res) -> get_names(Els, Res). #xmlel{name = <<"stat">>, attrs = [{<<"name">>, Name}], children = []}). -get_local_stats(_Server, [], []) -> +get_local_stats(_Server, [], [], _Lang) -> {result, [?STAT(<<"users/online">>), ?STAT(<<"users/total">>), ?STAT(<<"users/all-hosts/online">>), ?STAT(<<"users/all-hosts/total">>)]}; -get_local_stats(Server, [], Names) -> +get_local_stats(Server, [], Names, _Lang) -> {result, lists:map(fun (Name) -> get_local_stat(Server, [], Name) end, Names)}; get_local_stats(_Server, [<<"running nodes">>, _], - []) -> + [], _Lang) -> {result, [?STAT(<<"time/uptime">>), ?STAT(<<"time/cputime">>), ?STAT(<<"users/online">>), @@ -107,16 +108,19 @@ get_local_stats(_Server, [<<"running nodes">>, _], ?STAT(<<"transactions/restarted">>), ?STAT(<<"transactions/logged">>)]}; get_local_stats(_Server, [<<"running nodes">>, ENode], - Names) -> + Names, Lang) -> case search_running_node(ENode) of - false -> {error, ?ERR_ITEM_NOT_FOUND}; + false -> + Txt = <<"No running node found">>, + {error, ?ERRT_ITEM_NOT_FOUND(Lang, Txt)}; Node -> {result, lists:map(fun (Name) -> get_node_stat(Node, Name) end, Names)} end; -get_local_stats(_Server, _, _) -> - {error, ?ERR_FEATURE_NOT_IMPLEMENTED}. +get_local_stats(_Server, _, _, Lang) -> + Txt = <<"No statistics found for this item">>, + {error, ?ERRT_FEATURE_NOT_IMPLEMENTED(Lang, Txt)}. -define(STATVAL(Val, Unit), #xmlel{name = <<"stat">>, diff --git a/src/mod_time.erl b/src/mod_time.erl index 8bfe9f9f9..740b654a7 100644 --- a/src/mod_time.erl +++ b/src/mod_time.erl @@ -51,10 +51,11 @@ stop(Host) -> ?NS_TIME). process_local_iq(_From, _To, - #iq{type = Type, sub_el = SubEl} = IQ) -> + #iq{type = Type, sub_el = SubEl, lang = Lang} = IQ) -> case Type of set -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; + Txt = <<"Value 'set' of 'type' attribute is not allowed">>, + IQ#iq{type = error, sub_el = [SubEl, ?ERRT_NOT_ALLOWED(Lang, Txt)]}; get -> Now_universal = calendar:universal_time(), Now_local = calendar:universal_time_to_local_time(Now_universal), diff --git a/src/mod_vcard.erl b/src/mod_vcard.erl index db19b5570..e5f5d9e3c 100644 --- a/src/mod_vcard.erl +++ b/src/mod_vcard.erl @@ -33,53 +33,31 @@ -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, + process_local_iq/3, process_sm_iq/3, string2lower/1, remove_user/2, export/1, import/1, import/3, - mod_opt_type/1]). + mod_opt_type/1, set_vcard/3, make_vcard_search/4]). -include("ejabberd.hrl"). -include("logger.hrl"). - -include("jlib.hrl"). +-include("mod_vcard.hrl"). -define(JUD_MATCHES, 30). --record(vcard_search, - {us, user, luser, fn, lfn, family, lfamily, given, - lgiven, middle, lmiddle, nickname, lnickname, bday, - lbday, ctry, lctry, locality, llocality, email, lemail, - orgname, lorgname, orgunit, lorgunit}). - --record(vcard, {us = {<<"">>, <<"">>} :: {binary(), binary()} | binary(), - vcard = #xmlel{} :: xmlel()}). - -define(PROCNAME, ejabberd_mod_vcard). +-callback init(binary(), gen_mod:opts()) -> any(). +-callback import(binary(), #vcard{} | #vcard_search{}) -> ok | pass. +-callback get_vcard(binary(), binary()) -> [xmlel()] | error. +-callback set_vcard(binary(), binary(), + xmlel(), #vcard_search{}) -> {atomic, any()}. +-callback search(binary(), [{binary(), [binary()]}], boolean(), + infinity | pos_integer()) -> [binary()]. +-callback remove_user(binary(), binary()) -> {atomic, any()}. + start(Host, Opts) -> - case gen_mod:db_type(Host, Opts) of - mnesia -> - mnesia:create_table(vcard, - [{disc_only_copies, [node()]}, - {attributes, record_info(fields, vcard)}]), - mnesia:create_table(vcard_search, - [{disc_copies, [node()]}, - {attributes, - record_info(fields, vcard_search)}]), - update_tables(), - mnesia:add_table_index(vcard_search, luser), - mnesia:add_table_index(vcard_search, lfn), - mnesia:add_table_index(vcard_search, lfamily), - mnesia:add_table_index(vcard_search, lgiven), - mnesia:add_table_index(vcard_search, lmiddle), - mnesia:add_table_index(vcard_search, lnickname), - mnesia:add_table_index(vcard_search, lbday), - mnesia:add_table_index(vcard_search, lctry), - mnesia:add_table_index(vcard_search, llocality), - mnesia:add_table_index(vcard_search, lemail), - mnesia:add_table_index(vcard_search, lorgname), - mnesia:add_table_index(vcard_search, lorgunit); - _ -> ok - end, + Mod = gen_mod:db_mod(Host, Opts, ?MODULE), + Mod:init(Host, Opts), ejabberd_hooks:add(remove_user, Host, ?MODULE, remove_user, 50), IQDisc = gen_mod:get_opt(iqdisc, Opts, fun gen_iq_handler:check_type/1, @@ -102,7 +80,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. @@ -149,7 +127,8 @@ process_local_iq(_From, _To, #iq{type = Type, lang = Lang, sub_el = SubEl} = IQ) -> case Type of set -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; + Txt = <<"Value 'set' of 'type' attribute is not allowed">>, + IQ#iq{type = error, sub_el = [SubEl, ?ERRT_NOT_ALLOWED(Lang, Txt)]}; get -> IQ#iq{type = result, sub_el = @@ -173,7 +152,7 @@ process_local_iq(_From, _To, end. process_sm_iq(From, To, - #iq{type = Type, sub_el = SubEl} = IQ) -> + #iq{type = Type, lang = Lang, sub_el = SubEl} = IQ) -> case Type of set -> #jid{user = User, lserver = LServer} = From, @@ -182,14 +161,16 @@ process_sm_iq(From, To, set_vcard(User, LServer, SubEl), IQ#iq{type = result, sub_el = []}; false -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]} + Txt = <<"The query is only allowed from local users">>, + IQ#iq{type = error, sub_el = [SubEl, ?ERRT_NOT_ALLOWED(Lang, Txt)]} end; get -> #jid{luser = LUser, lserver = LServer} = To, case get_vcard(LUser, LServer) of error -> + Txt = <<"Database failure">>, IQ#iq{type = error, - sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]}; + sub_el = [SubEl, ?ERRT_INTERNAL_SERVER_ERROR(Lang, Txt)]}; [] -> IQ#iq{type = result, sub_el = [#xmlel{name = <<"vCard">>, @@ -200,68 +181,38 @@ process_sm_iq(From, To, end. get_vcard(LUser, LServer) -> - get_vcard(LUser, LServer, - gen_mod:db_type(LServer, ?MODULE)). - -get_vcard(LUser, LServer, mnesia) -> - US = {LUser, LServer}, - F = fun () -> mnesia:read({vcard, US}) end, - case mnesia:transaction(F) of - {atomic, Rs} -> - lists:map(fun (R) -> R#vcard.vcard end, Rs); - {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 - {error, _Reason} -> error; - VCARD -> [VCARD] - end; - {selected, [<<"vcard">>], []} -> []; - _ -> error - end; -get_vcard(LUser, LServer, riak) -> - case ejabberd_riak:get(vcard, vcard_schema(), {LUser, LServer}) of - {ok, R} -> - [R#vcard.vcard]; - {error, notfound} -> - []; - _ -> - error - end. + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:get_vcard(LUser, LServer). -set_vcard(User, LServer, VCARD) -> - FN = xml:get_path_s(VCARD, [{elem, <<"FN">>}, cdata]), - Family = xml:get_path_s(VCARD, +make_vcard_search(User, LUser, LServer, 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 = jid:nodeprep(User), LFN = string2lower(FN), LFamily = string2lower(Family), LGiven = string2lower(Given), @@ -273,105 +224,42 @@ set_vcard(User, LServer, VCARD) -> LEMail = string2lower(EMail), LOrgName = string2lower(OrgName), LOrgUnit = string2lower(OrgUnit), - if (LUser == error) -> - {error, badarg}; - true -> - case gen_mod:db_type(LServer, ?MODULE) of - mnesia -> - US = {LUser, LServer}, - F = fun () -> - mnesia:write(#vcard{us = US, vcard = VCARD}), - mnesia:write(#vcard_search{us = US, - user = {User, LServer}, - luser = LUser, fn = FN, - lfn = LFN, - family = Family, - lfamily = LFamily, - given = Given, - lgiven = LGiven, - middle = Middle, - lmiddle = LMiddle, - nickname = Nickname, - lnickname = LNickname, - bday = BDay, - lbday = LBDay, - ctry = CTRY, - lctry = LCTRY, - locality = Locality, - llocality = LLocality, - email = EMail, - lemail = LEMail, - orgname = OrgName, - lorgname = LOrgName, - orgunit = OrgUnit, - lorgunit = LOrgUnit}) - end, - mnesia:transaction(F); - riak -> - US = {LUser, LServer}, - ejabberd_riak:put(#vcard{us = US, vcard = VCARD}, - vcard_schema(), - [{'2i', [{<<"user">>, User}, - {<<"luser">>, LUser}, - {<<"fn">>, FN}, - {<<"lfn">>, LFN}, - {<<"family">>, Family}, - {<<"lfamily">>, LFamily}, - {<<"given">>, Given}, - {<<"lgiven">>, LGiven}, - {<<"middle">>, Middle}, - {<<"lmiddle">>, LMiddle}, - {<<"nickname">>, Nickname}, - {<<"lnickname">>, LNickname}, - {<<"bday">>, BDay}, - {<<"lbday">>, LBDay}, - {<<"ctry">>, CTRY}, - {<<"lctry">>, LCTRY}, - {<<"locality">>, Locality}, - {<<"llocality">>, LLocality}, - {<<"email">>, EMail}, - {<<"lemail">>, LEMail}, - {<<"orgname">>, OrgName}, - {<<"lorgname">>, LOrgName}, - {<<"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) - end, - ejabberd_hooks:run(vcard_set, LServer, - [LUser, LServer, VCARD]) + US = {LUser, LServer}, + #vcard_search{us = US, + user = {User, LServer}, + luser = LUser, fn = FN, + lfn = LFN, + family = Family, + lfamily = LFamily, + given = Given, + lgiven = LGiven, + middle = Middle, + lmiddle = LMiddle, + nickname = Nickname, + lnickname = LNickname, + bday = BDay, + lbday = LBDay, + ctry = CTRY, + lctry = LCTRY, + locality = Locality, + llocality = LLocality, + email = EMail, + lemail = LEMail, + orgname = OrgName, + lorgname = LOrgName, + orgunit = OrgUnit, + lorgunit = LOrgUnit}. + +set_vcard(User, LServer, VCARD) -> + case jid:nodeprep(User) of + error -> + {error, badarg}; + LUser -> + VCardSearch = make_vcard_search(User, LUser, LServer, VCARD), + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:set_vcard(LUser, LServer, VCARD, VCardSearch), + ejabberd_hooks:run(vcard_set, LServer, + [LUser, LServer, VCARD]) end. string2lower(String) -> @@ -445,15 +333,17 @@ do_route(ServerHost, From, To, Packet) -> XDataEl = find_xdata_el(SubEl), case XDataEl of false -> - Err = jlib:make_error_reply(Packet, - ?ERR_BAD_REQUEST), + Txt = <<"Data form not found">>, + Err = jlib:make_error_reply( + Packet, ?ERRT_BAD_REQUEST(Lang, Txt)), ejabberd_router:route(To, From, Err); _ -> XData = jlib:parse_xdata_submit(XDataEl), case XData of invalid -> - Err = jlib:make_error_reply(Packet, - ?ERR_BAD_REQUEST), + Txt = <<"Incorrect data form">>, + Err = jlib:make_error_reply( + Packet, ?ERRT_BAD_REQUEST(Lang, Txt)), ejabberd_router:route(To, From, Err); _ -> ResIQ = IQ#iq{type = result, @@ -493,7 +383,8 @@ do_route(ServerHost, From, To, Packet) -> #iq{type = Type, xmlns = ?NS_DISCO_INFO, lang = Lang} -> case Type of set -> - Err = jlib:make_error_reply(Packet, ?ERR_NOT_ALLOWED), + Txt = <<"Value 'set' of 'type' attribute is not allowed">>, + Err = jlib:make_error_reply(Packet, ?ERRT_NOT_ALLOWED(Lang, Txt)), ejabberd_router:route(To, From, Err); get -> Info = ejabberd_hooks:run_fold(disco_info, ServerHost, @@ -539,10 +430,11 @@ do_route(ServerHost, From, To, Packet) -> ++ Info}]}, ejabberd_router:route(To, From, jlib:iq_to_xml(ResIQ)) end; - #iq{type = Type, xmlns = ?NS_DISCO_ITEMS} -> + #iq{type = Type, lang = Lang, xmlns = ?NS_DISCO_ITEMS} -> case Type of set -> - Err = jlib:make_error_reply(Packet, ?ERR_NOT_ALLOWED), + Txt = <<"Value 'set' of 'type' attribute is not allowed">>, + Err = jlib:make_error_reply(Packet, ?ERRT_NOT_ALLOWED(Lang, Txt)), ejabberd_router:route(To, From, Err); get -> ResIQ = IQ#iq{type = result, @@ -587,7 +479,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) @@ -671,498 +563,36 @@ record_to_item(_LServer, #vcard_search{} = R) -> ?FIELD(<<"orgunit">>, (R#vcard_search.orgunit))]}. search(LServer, Data) -> - DBType = gen_mod:db_type(LServer, ?MODULE), - MatchSpec = make_matchspec(LServer, Data, DBType), + Mod = gen_mod:db_mod(LServer, ?MODULE), AllowReturnAll = gen_mod:get_module_opt(LServer, ?MODULE, allow_return_all, fun(B) when is_boolean(B) -> B end, false), - search(LServer, MatchSpec, AllowReturnAll, DBType). - -search(LServer, MatchSpec, AllowReturnAll, mnesia) -> - if (MatchSpec == #vcard_search{_ = '_'}) and - not AllowReturnAll -> - []; - true -> - case catch mnesia:dirty_select(vcard_search, - [{MatchSpec, [], ['$_']}]) - of - {'EXIT', Reason} -> ?ERROR_MSG("~p", [Reason]), []; - Rs -> - case gen_mod:get_module_opt(LServer, ?MODULE, matches, - fun(infinity) -> infinity; - (I) when is_integer(I), - I>0 -> - I - end, ?JUD_MATCHES) of - infinity -> - Rs; - Val -> - lists:sublist(Rs, Val) - end - end - end; -search(LServer, MatchSpec, AllowReturnAll, odbc) -> - if (MatchSpec == <<"">>) and not AllowReturnAll -> []; - true -> - Limit = case gen_mod:get_module_opt(LServer, ?MODULE, matches, - fun(infinity) -> infinity; - (I) when is_integer(I), - I>0 -> - I - end, ?JUD_MATCHES) of - infinity -> - <<"">>; - Val -> - [<<" LIMIT ">>, - jlib:integer_to_binary(Val)] - end, - case catch ejabberd_odbc:sql_query(LServer, - [<<"select username, fn, family, given, " - "middle, nickname, bday, ctry, " - "locality, email, orgname, orgunit " - "from vcard_search ">>, - MatchSpec, Limit, <<";">>]) - of - {selected, - [<<"username">>, <<"fn">>, <<"family">>, <<"given">>, - <<"middle">>, <<"nickname">>, <<"bday">>, <<"ctry">>, - <<"locality">>, <<"email">>, <<"orgname">>, - <<"orgunit">>], - Rs} - when is_list(Rs) -> - Rs; - Error -> ?ERROR_MSG("~p", [Error]), [] - end - end; -search(_LServer, _MatchSpec, _AllowReturnAll, riak) -> - []. - -make_matchspec(LServer, Data, mnesia) -> - GlobMatch = #vcard_search{_ = '_'}, - Match = filter_fields(Data, GlobMatch, LServer, mnesia), - Match; -make_matchspec(LServer, Data, odbc) -> - filter_fields(Data, <<"">>, LServer, odbc); -make_matchspec(_LServer, _Data, riak) -> - []. - -filter_fields([], Match, _LServer, mnesia) -> Match; -filter_fields([], Match, _LServer, odbc) -> - case Match of - <<"">> -> <<"">>; - _ -> [<<" where ">>, Match] - end; -filter_fields([{SVar, [Val]} | Ds], Match, LServer, - mnesia) - when is_binary(Val) and (Val /= <<"">>) -> - LVal = string2lower(Val), - NewMatch = case SVar of - <<"user">> -> - case gen_mod:get_module_opt(LServer, ?MODULE, - search_all_hosts, - fun(B) when is_boolean(B) -> - B - end, true) - of - true -> Match#vcard_search{luser = make_val(LVal)}; - false -> - Host = find_my_host(LServer), - Match#vcard_search{us = {make_val(LVal), Host}} - end; - <<"fn">> -> Match#vcard_search{lfn = make_val(LVal)}; - <<"last">> -> - Match#vcard_search{lfamily = make_val(LVal)}; - <<"first">> -> - Match#vcard_search{lgiven = make_val(LVal)}; - <<"middle">> -> - Match#vcard_search{lmiddle = make_val(LVal)}; - <<"nick">> -> - Match#vcard_search{lnickname = make_val(LVal)}; - <<"bday">> -> - Match#vcard_search{lbday = make_val(LVal)}; - <<"ctry">> -> - Match#vcard_search{lctry = make_val(LVal)}; - <<"locality">> -> - Match#vcard_search{llocality = make_val(LVal)}; - <<"email">> -> - Match#vcard_search{lemail = make_val(LVal)}; - <<"orgname">> -> - Match#vcard_search{lorgname = make_val(LVal)}; - <<"orgunit">> -> - Match#vcard_search{lorgunit = make_val(LVal)}; - _ -> Match - end, - filter_fields(Ds, NewMatch, LServer, mnesia); -filter_fields([{SVar, [Val]} | Ds], Match, LServer, - odbc) - when is_binary(Val) and (Val /= <<"">>) -> - LVal = string2lower(Val), - NewMatch = case SVar of - <<"user">> -> make_val(Match, <<"lusername">>, LVal); - <<"fn">> -> make_val(Match, <<"lfn">>, LVal); - <<"last">> -> make_val(Match, <<"lfamily">>, LVal); - <<"first">> -> make_val(Match, <<"lgiven">>, LVal); - <<"middle">> -> make_val(Match, <<"lmiddle">>, LVal); - <<"nick">> -> make_val(Match, <<"lnickname">>, LVal); - <<"bday">> -> make_val(Match, <<"lbday">>, LVal); - <<"ctry">> -> make_val(Match, <<"lctry">>, LVal); - <<"locality">> -> - make_val(Match, <<"llocality">>, LVal); - <<"email">> -> make_val(Match, <<"lemail">>, LVal); - <<"orgname">> -> make_val(Match, <<"lorgname">>, LVal); - <<"orgunit">> -> make_val(Match, <<"lorgunit">>, LVal); - _ -> Match - end, - filter_fields(Ds, NewMatch, LServer, odbc); -filter_fields([_ | Ds], Match, LServer, DBType) -> - filter_fields(Ds, Match, LServer, DBType). - -make_val(Match, Field, Val) -> - Condition = case str:suffix(<<"*">>, Val) of - true -> - Val1 = str:substr(Val, 1, byte_size(Val) - 1), - SVal = <<(ejabberd_odbc:escape_like(Val1))/binary, - "%">>, - [Field, <<" LIKE '">>, SVal, <<"'">>]; - _ -> - SVal = ejabberd_odbc:escape(Val), - [Field, <<" = '">>, SVal, <<"'">>] - end, - case Match of - <<"">> -> Condition; - _ -> [Match, <<" and ">>, Condition] - end. - -make_val(Val) -> - case str:suffix(<<"*">>, Val) of - true -> [str:substr(Val, 1, byte_size(Val) - 1)] ++ '_'; - _ -> Val - end. - -find_my_host(LServer) -> - Parts = str:tokens(LServer, <<".">>), - find_my_host(Parts, ?MYHOSTS). - -find_my_host([], _Hosts) -> ?MYNAME; -find_my_host([_ | Tail] = Parts, Hosts) -> - Domain = parts_to_string(Parts), - case lists:member(Domain, Hosts) of - true -> Domain; - false -> find_my_host(Tail, Hosts) - end. - -parts_to_string(Parts) -> - str:strip(list_to_binary( - lists:map(fun (S) -> <<S/binary, $.>> end, Parts)), - right, $.). + MaxMatch = gen_mod:get_module_opt(LServer, ?MODULE, matches, + fun(infinity) -> infinity; + (I) when is_integer(I), + I>0 -> + I + end, ?JUD_MATCHES), + Mod:search(LServer, Data, AllowReturnAll, MaxMatch). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -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, - [{elem, <<"N">>}, {elem, <<"FAMILY">>}, cdata]), - Given = xml:get_path_s(VCARD, - [{elem, <<"N">>}, {elem, <<"GIVEN">>}, cdata]), - Middle = xml:get_path_s(VCARD, - [{elem, <<"N">>}, {elem, <<"MIDDLE">>}, cdata]), - Nickname = xml:get_path_s(VCARD, - [{elem, <<"NICKNAME">>}, cdata]), - BDay = xml:get_path_s(VCARD, - [{elem, <<"BDAY">>}, cdata]), - CTRY = xml:get_path_s(VCARD, - [{elem, <<"ADR">>}, {elem, <<"CTRY">>}, cdata]), - Locality = xml:get_path_s(VCARD, - [{elem, <<"ADR">>}, {elem, <<"LOCALITY">>}, - cdata]), - EMail = xml:get_path_s(VCARD, - [{elem, <<"EMAIL">>}, cdata]), - OrgName = xml:get_path_s(VCARD, - [{elem, <<"ORG">>}, {elem, <<"ORGNAME">>}, cdata]), - OrgUnit = xml:get_path_s(VCARD, - [{elem, <<"ORG">>}, {elem, <<"ORGUNIT">>}, cdata]), - {LUser, _LServer} = US, - LFN = string2lower(FN), - LFamily = string2lower(Family), - LGiven = string2lower(Given), - LMiddle = string2lower(Middle), - LNickname = string2lower(Nickname), - LBDay = string2lower(BDay), - LCTRY = string2lower(CTRY), - LLocality = string2lower(Locality), - LEMail = string2lower(EMail), - LOrgName = string2lower(OrgName), - LOrgUnit = string2lower(OrgUnit), - mnesia:write(#vcard_search{us = US, user = User, - luser = LUser, fn = FN, lfn = LFN, - family = Family, lfamily = LFamily, - given = Given, lgiven = LGiven, - middle = Middle, lmiddle = LMiddle, - nickname = Nickname, - lnickname = LNickname, bday = BDay, - lbday = LBDay, ctry = CTRY, lctry = LCTRY, - locality = Locality, - llocality = LLocality, email = EMail, - lemail = LEMail, orgname = OrgName, - lorgname = LOrgName, orgunit = OrgUnit, - lorgunit = LOrgUnit}). - -reindex_vcards() -> - F = fun () -> mnesia:foldl(fun set_vcard_t/2, [], vcard) - end, - mnesia:transaction(F). - 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({vcard, US}), - mnesia:delete({vcard_search, US}) - 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, <<"';">>]]); -remove_user(LUser, LServer, riak) -> - {atomic, ejabberd_riak:delete(vcard, {LUser, LServer})}. + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:remove_user(LUser, LServer). -update_tables() -> - update_vcard_table(), - update_vcard_search_table(). - -update_vcard_table() -> - Fields = record_info(fields, vcard), - case mnesia:table_info(vcard, attributes) of - Fields -> - ejabberd_config:convert_table_to_binary( - vcard, Fields, set, - fun(#vcard{us = {U, _}}) -> U end, - fun(#vcard{us = {U, S}, vcard = El} = R) -> - R#vcard{us = {iolist_to_binary(U), - iolist_to_binary(S)}, - vcard = xml:to_xmlel(El)} - end); - _ -> - ?INFO_MSG("Recreating vcard table", []), - mnesia:transform_table(vcard, ignore, Fields) - end. - -update_vcard_search_table() -> - Fields = record_info(fields, vcard_search), - case mnesia:table_info(vcard_search, attributes) of - Fields -> - ejabberd_config:convert_table_to_binary( - vcard_search, Fields, set, - fun(#vcard_search{us = {U, _}}) -> U end, - fun(#vcard_search{} = VS) -> - [vcard_search | L] = tuple_to_list(VS), - NewL = lists:map( - fun({U, S}) -> - {iolist_to_binary(U), - iolist_to_binary(S)}; - (Str) -> - iolist_to_binary(Str) - end, L), - list_to_tuple([vcard_search | NewL]) - end); - _ -> - ?INFO_MSG("Recreating vcard_search table", []), - mnesia:transform_table(vcard_search, ignore, Fields) - end. - -vcard_schema() -> - {record_info(fields, vcard), #vcard{}}. - -export(_Server) -> - [{vcard, - fun(Host, #vcard{us = {LUser, LServer}, vcard = VCARD}) - when LServer == Host -> - Username = ejabberd_odbc:escape(LUser), - SVCARD = - ejabberd_odbc:escape(xml:element_to_binary(VCARD)), - [[<<"delete from vcard where username='">>, Username, <<"';">>], - [<<"insert into vcard(username, vcard) values ('">>, - Username, <<"', '">>, SVCARD, <<"');">>]]; - (_Host, _R) -> - [] - end}, - {vcard_search, - fun(Host, #vcard_search{user = {User, LServer}, luser = LUser, - fn = FN, lfn = LFN, family = Family, - lfamily = LFamily, given = Given, - lgiven = LGiven, middle = Middle, - lmiddle = LMiddle, nickname = Nickname, - lnickname = LNickname, bday = BDay, - lbday = LBDay, ctry = CTRY, lctry = LCTRY, - locality = Locality, llocality = LLocality, - email = EMail, lemail = LEMail, - orgname = OrgName, lorgname = LOrgName, - orgunit = OrgUnit, lorgunit = LOrgUnit}) - when LServer == Host -> - Username = ejabberd_odbc:escape(User), - LUsername = ejabberd_odbc:escape(LUser), - 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), - [[<<"delete from vcard_search where lusername='">>, - LUsername, <<"';">>], - [<<"insert into 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)values (">>, - <<" '">>, Username, <<"', '">>, LUsername, - <<"', '">>, SFN, <<"', '">>, SLFN, - <<"', '">>, SFamily, <<"', '">>, SLFamily, - <<"', '">>, SGiven, <<"', '">>, SLGiven, - <<"', '">>, SMiddle, <<"', '">>, SLMiddle, - <<"', '">>, SNickname, <<"', '">>, SLNickname, - <<"', '">>, SBDay, <<"', '">>, SLBDay, - <<"', '">>, SCTRY, <<"', '">>, SLCTRY, - <<"', '">>, SLocality, <<"', '">>, SLLocality, - <<"', '">>, SEMail, <<"', '">>, SLEMail, - <<"', '">>, SOrgName, <<"', '">>, SLOrgName, - <<"', '">>, SOrgUnit, <<"', '">>, SLOrgUnit, - <<"');">>]]; - (_Host, _R) -> - [] - end}]. +export(LServer) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:export(LServer). import(LServer) -> - [{<<"select username, vcard from vcard;">>, - fun([LUser, SVCard]) -> - #xmlel{} = VCARD = xml_stream:parse_element(SVCard), - #vcard{us = {LUser, LServer}, vcard = VCARD} - end}, - {<<"select username, lusername, fn, lfn, family, lfamily, " - "given, lgiven, middle, lmiddle, nickname, lnickname, " - "bday, lbday, ctry, lctry, locality, llocality, email, " - "lemail, orgname, lorgname, orgunit, lorgunit from vcard_search;">>, - fun([User, LUser, FN, LFN, - Family, LFamily, Given, LGiven, - Middle, LMiddle, Nickname, LNickname, - BDay, LBDay, CTRY, LCTRY, Locality, LLocality, - EMail, LEMail, OrgName, LOrgName, OrgUnit, LOrgUnit]) -> - #vcard_search{us = {LUser, LServer}, - user = {User, LServer}, luser = LUser, - fn = FN, lfn = LFN, family = Family, - lfamily = LFamily, given = Given, - lgiven = LGiven, middle = Middle, - lmiddle = LMiddle, nickname = Nickname, - lnickname = LNickname, bday = BDay, - lbday = LBDay, ctry = CTRY, lctry = LCTRY, - locality = Locality, llocality = LLocality, - email = EMail, lemail = LEMail, - orgname = OrgName, lorgname = LOrgName, - orgunit = OrgUnit, lorgunit = LOrgUnit} - end}]. + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:import(LServer). -import(_LServer, mnesia, #vcard{} = VCard) -> - mnesia:dirty_write(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, - [{elem, <<"N">>}, {elem, <<"FAMILY">>}, cdata]), - Given = xml:get_path_s(El, - [{elem, <<"N">>}, {elem, <<"GIVEN">>}, cdata]), - Middle = xml:get_path_s(El, - [{elem, <<"N">>}, {elem, <<"MIDDLE">>}, cdata]), - Nickname = xml:get_path_s(El, - [{elem, <<"NICKNAME">>}, cdata]), - BDay = xml:get_path_s(El, - [{elem, <<"BDAY">>}, cdata]), - CTRY = xml:get_path_s(El, - [{elem, <<"ADR">>}, {elem, <<"CTRY">>}, cdata]), - Locality = xml:get_path_s(El, - [{elem, <<"ADR">>}, {elem, <<"LOCALITY">>}, - cdata]), - EMail1 = xml:get_path_s(El, - [{elem, <<"EMAIL">>}, {elem, <<"USERID">>}, cdata]), - EMail2 = xml:get_path_s(El, - [{elem, <<"EMAIL">>}, cdata]), - OrgName = xml:get_path_s(El, - [{elem, <<"ORG">>}, {elem, <<"ORGNAME">>}, cdata]), - OrgUnit = xml:get_path_s(El, - [{elem, <<"ORG">>}, {elem, <<"ORGUNIT">>}, cdata]), - EMail = case EMail1 of - <<"">> -> EMail2; - _ -> EMail1 - end, - LFN = string2lower(FN), - LFamily = string2lower(Family), - LGiven = string2lower(Given), - LMiddle = string2lower(Middle), - LNickname = string2lower(Nickname), - LBDay = string2lower(BDay), - LCTRY = string2lower(CTRY), - LLocality = string2lower(Locality), - LEMail = string2lower(EMail), - LOrgName = string2lower(OrgName), - LOrgUnit = string2lower(OrgUnit), - ejabberd_riak:put(VCard, vcard_schema(), - [{'2i', [{<<"user">>, LUser}, - {<<"luser">>, LUser}, - {<<"fn">>, FN}, - {<<"lfn">>, LFN}, - {<<"family">>, Family}, - {<<"lfamily">>, LFamily}, - {<<"given">>, Given}, - {<<"lgiven">>, LGiven}, - {<<"middle">>, Middle}, - {<<"lmiddle">>, LMiddle}, - {<<"nickname">>, Nickname}, - {<<"lnickname">>, LNickname}, - {<<"bday">>, BDay}, - {<<"lbday">>, LBDay}, - {<<"ctry">>, CTRY}, - {<<"lctry">>, LCTRY}, - {<<"locality">>, Locality}, - {<<"llocality">>, LLocality}, - {<<"email">>, EMail}, - {<<"lemail">>, LEMail}, - {<<"orgname">>, OrgName}, - {<<"lorgname">>, LOrgName}, - {<<"orgunit">>, OrgUnit}, - {<<"lorgunit">>, LOrgUnit}]}]); -import(_LServer, riak, #vcard_search{}) -> - ok; -import(_, _, _) -> - pass. +import(LServer, DBType, VCard) -> + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:import(LServer, VCard). mod_opt_type(allow_return_all) -> fun (B) when is_boolean(B) -> B end; diff --git a/src/mod_vcard_ldap.erl b/src/mod_vcard_ldap.erl index b24356d8e..46f81af09 100644 --- a/src/mod_vcard_ldap.erl +++ b/src/mod_vcard_ldap.erl @@ -21,7 +21,7 @@ %%% 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). @@ -173,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}. @@ -206,7 +206,8 @@ process_local_iq(_From, _To, #iq{type = Type, lang = Lang, sub_el = SubEl} = IQ) -> case Type of set -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; + Txt = <<"Value 'set' of 'type' attribute is not allowed">>, + IQ#iq{type = error, sub_el = [SubEl, ?ERRT_NOT_ALLOWED(Lang, Txt)]}; get -> IQ#iq{type = result, sub_el = @@ -240,10 +241,11 @@ process_sm_iq(_From, #jid{lserver = LServer} = To, process_vcard_ldap(To, IQ, Server) -> {ok, State} = eldap_utils:get_state(Server, ?PROCNAME), - #iq{type = Type, sub_el = SubEl} = IQ, + #iq{type = Type, sub_el = SubEl, lang = Lang} = IQ, case Type of set -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; + Txt = <<"Value 'set' of 'type' attribute is not allowed">>, + IQ#iq{type = error, sub_el = [SubEl, ?ERRT_NOT_ALLOWED(Lang, Txt)]}; get -> #jid{luser = LUser} = To, LServer = State#state.serverhost, @@ -455,15 +457,17 @@ route(State, From, To, Packet) -> XDataEl = find_xdata_el(SubEl), case XDataEl of false -> - Err = jlib:make_error_reply(Packet, - ?ERR_BAD_REQUEST), + Txt = <<"Data form not found">>, + Err = jlib:make_error_reply( + Packet, ?ERRT_BAD_REQUEST(Lang, Txt)), ejabberd_router:route(To, From, Err); _ -> XData = jlib:parse_xdata_submit(XDataEl), case XData of invalid -> - Err = jlib:make_error_reply(Packet, - ?ERR_BAD_REQUEST), + Txt = <<"Incorrect data form">>, + Err = jlib:make_error_reply( + Packet, ?ERRT_BAD_REQUEST(Lang, Txt)), ejabberd_router:route(To, From, Err); _ -> ResIQ = IQ#iq{type = result, @@ -505,7 +509,8 @@ route(State, From, To, Packet) -> #iq{type = Type, xmlns = ?NS_DISCO_INFO, lang = Lang} -> case Type of set -> - Err = jlib:make_error_reply(Packet, ?ERR_NOT_ALLOWED), + Txt = <<"Value 'set' of 'type' attribute is not allowed">>, + Err = jlib:make_error_reply(Packet, ?ERRT_NOT_ALLOWED(Lang, Txt)), ejabberd_router:route(To, From, Err); get -> Info = ejabberd_hooks:run_fold(disco_info, ServerHost, @@ -545,10 +550,11 @@ route(State, From, To, Packet) -> ++ Info}]}, ejabberd_router:route(To, From, jlib:iq_to_xml(ResIQ)) end; - #iq{type = Type, xmlns = ?NS_DISCO_ITEMS} -> + #iq{type = Type, lang = Lang, xmlns = ?NS_DISCO_ITEMS} -> case Type of set -> - Err = jlib:make_error_reply(Packet, ?ERR_NOT_ALLOWED), + Txt = <<"Value 'set' of 'type' attribute is not allowed">>, + Err = jlib:make_error_reply(Packet, ?ERRT_NOT_ALLOWED(Lang, Txt)), ejabberd_router:route(To, From, Err); get -> ResIQ = IQ#iq{type = result, @@ -723,7 +729,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) diff --git a/src/mod_vcard_mnesia.erl b/src/mod_vcard_mnesia.erl new file mode 100644 index 000000000..781a135c8 --- /dev/null +++ b/src/mod_vcard_mnesia.erl @@ -0,0 +1,213 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 13 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_vcard_mnesia). + +-behaviour(mod_vcard). + +%% API +-export([init/2, import/2, get_vcard/2, set_vcard/4, search/4, remove_user/2]). + +-include("ejabberd.hrl"). +-include("jlib.hrl"). +-include("mod_vcard.hrl"). +-include("logger.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + mnesia:create_table(vcard, + [{disc_only_copies, [node()]}, + {attributes, record_info(fields, vcard)}]), + mnesia:create_table(vcard_search, + [{disc_copies, [node()]}, + {attributes, + record_info(fields, vcard_search)}]), + update_tables(), + mnesia:add_table_index(vcard_search, luser), + mnesia:add_table_index(vcard_search, lfn), + mnesia:add_table_index(vcard_search, lfamily), + mnesia:add_table_index(vcard_search, lgiven), + mnesia:add_table_index(vcard_search, lmiddle), + mnesia:add_table_index(vcard_search, lnickname), + mnesia:add_table_index(vcard_search, lbday), + mnesia:add_table_index(vcard_search, lctry), + mnesia:add_table_index(vcard_search, llocality), + mnesia:add_table_index(vcard_search, lemail), + mnesia:add_table_index(vcard_search, lorgname), + mnesia:add_table_index(vcard_search, lorgunit). + +get_vcard(LUser, LServer) -> + US = {LUser, LServer}, + F = fun () -> mnesia:read({vcard, US}) end, + case mnesia:transaction(F) of + {atomic, Rs} -> + lists:map(fun (R) -> R#vcard.vcard end, Rs); + {aborted, _Reason} -> error + end. + +set_vcard(LUser, LServer, VCARD, VCardSearch) -> + US = {LUser, LServer}, + F = fun () -> + mnesia:write(#vcard{us = US, vcard = VCARD}), + mnesia:write(VCardSearch) + end, + mnesia:transaction(F). + +search(LServer, Data, AllowReturnAll, MaxMatch) -> + MatchSpec = make_matchspec(LServer, Data), + if (MatchSpec == #vcard_search{_ = '_'}) and + not AllowReturnAll -> + []; + true -> + case catch mnesia:dirty_select(vcard_search, + [{MatchSpec, [], ['$_']}]) of + {'EXIT', Reason} -> + ?ERROR_MSG("~p", [Reason]), []; + Rs -> + case MaxMatch of + infinity -> + Rs; + Val -> + lists:sublist(Rs, Val) + end + end + end. + +remove_user(LUser, LServer) -> + US = {LUser, LServer}, + F = fun () -> + mnesia:delete({vcard, US}), + mnesia:delete({vcard_search, US}) + end, + mnesia:transaction(F). + +import(_LServer, #vcard{} = VCard) -> + mnesia:dirty_write(VCard); +import(_LServer, #vcard_search{} = S) -> + mnesia:dirty_write(S). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +update_tables() -> + update_vcard_table(), + update_vcard_search_table(). + +update_vcard_table() -> + Fields = record_info(fields, vcard), + case mnesia:table_info(vcard, attributes) of + Fields -> + ejabberd_config:convert_table_to_binary( + vcard, Fields, set, + fun(#vcard{us = {U, _}}) -> U end, + fun(#vcard{us = {U, S}, vcard = El} = R) -> + R#vcard{us = {iolist_to_binary(U), + iolist_to_binary(S)}, + vcard = fxml:to_xmlel(El)} + end); + _ -> + ?INFO_MSG("Recreating vcard table", []), + mnesia:transform_table(vcard, ignore, Fields) + end. + +update_vcard_search_table() -> + Fields = record_info(fields, vcard_search), + case mnesia:table_info(vcard_search, attributes) of + Fields -> + ejabberd_config:convert_table_to_binary( + vcard_search, Fields, set, + fun(#vcard_search{us = {U, _}}) -> U end, + fun(#vcard_search{} = VS) -> + [vcard_search | L] = tuple_to_list(VS), + NewL = lists:map( + fun({U, S}) -> + {iolist_to_binary(U), + iolist_to_binary(S)}; + (Str) -> + iolist_to_binary(Str) + end, L), + list_to_tuple([vcard_search | NewL]) + end); + _ -> + ?INFO_MSG("Recreating vcard_search table", []), + mnesia:transform_table(vcard_search, ignore, Fields) + end. + +make_matchspec(LServer, Data) -> + GlobMatch = #vcard_search{_ = '_'}, + Match = filter_fields(Data, GlobMatch, LServer), + Match. + +filter_fields([], Match, _LServer) -> + Match; +filter_fields([{SVar, [Val]} | Ds], Match, LServer) + when is_binary(Val) and (Val /= <<"">>) -> + LVal = mod_vcard:string2lower(Val), + NewMatch = case SVar of + <<"user">> -> + case gen_mod:get_module_opt(LServer, ?MODULE, + search_all_hosts, + fun(B) when is_boolean(B) -> + B + end, true) of + true -> Match#vcard_search{luser = make_val(LVal)}; + false -> + Host = find_my_host(LServer), + Match#vcard_search{us = {make_val(LVal), Host}} + end; + <<"fn">> -> Match#vcard_search{lfn = make_val(LVal)}; + <<"last">> -> + Match#vcard_search{lfamily = make_val(LVal)}; + <<"first">> -> + Match#vcard_search{lgiven = make_val(LVal)}; + <<"middle">> -> + Match#vcard_search{lmiddle = make_val(LVal)}; + <<"nick">> -> + Match#vcard_search{lnickname = make_val(LVal)}; + <<"bday">> -> + Match#vcard_search{lbday = make_val(LVal)}; + <<"ctry">> -> + Match#vcard_search{lctry = make_val(LVal)}; + <<"locality">> -> + Match#vcard_search{llocality = make_val(LVal)}; + <<"email">> -> + Match#vcard_search{lemail = make_val(LVal)}; + <<"orgname">> -> + Match#vcard_search{lorgname = make_val(LVal)}; + <<"orgunit">> -> + Match#vcard_search{lorgunit = make_val(LVal)}; + _ -> Match + end, + filter_fields(Ds, NewMatch, LServer); +filter_fields([_ | Ds], Match, LServer) -> + filter_fields(Ds, Match, LServer). + +make_val(Val) -> + case str:suffix(<<"*">>, Val) of + true -> [str:substr(Val, 1, byte_size(Val) - 1)] ++ '_'; + _ -> Val + end. + +find_my_host(LServer) -> + Parts = str:tokens(LServer, <<".">>), + find_my_host(Parts, ?MYHOSTS). + +find_my_host([], _Hosts) -> ?MYNAME; +find_my_host([_ | Tail] = Parts, Hosts) -> + Domain = parts_to_string(Parts), + case lists:member(Domain, Hosts) of + true -> Domain; + false -> find_my_host(Tail, Hosts) + end. + +parts_to_string(Parts) -> + str:strip(list_to_binary( + lists:map(fun (S) -> <<S/binary, $.>> end, Parts)), + right, $.). diff --git a/src/mod_vcard_riak.erl b/src/mod_vcard_riak.erl new file mode 100644 index 000000000..386347387 --- /dev/null +++ b/src/mod_vcard_riak.erl @@ -0,0 +1,151 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 13 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_vcard_riak). + +-behaviour(mod_vcard). + +%% API +-export([init/2, get_vcard/2, set_vcard/4, search/4, remove_user/2, + import/2]). + +-include("jlib.hrl"). +-include("mod_vcard.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ok. + +get_vcard(LUser, LServer) -> + case ejabberd_riak:get(vcard, vcard_schema(), {LUser, LServer}) of + {ok, R} -> + [R#vcard.vcard]; + {error, notfound} -> + []; + _ -> + error + end. + +set_vcard(LUser, LServer, VCARD, + #vcard_search{user = {User, _}, + fn = FN, + lfn = LFN, + family = Family, + lfamily = LFamily, + given = Given, + lgiven = LGiven, + middle = Middle, + lmiddle = LMiddle, + nickname = Nickname, + lnickname = LNickname, + bday = BDay, + lbday = LBDay, + ctry = CTRY, + lctry = LCTRY, + locality = Locality, + llocality = LLocality, + email = EMail, + lemail = LEMail, + orgname = OrgName, + lorgname = LOrgName, + orgunit = OrgUnit, + lorgunit = LOrgUnit}) -> + US = {LUser, LServer}, + {atomic, + ejabberd_riak:put(#vcard{us = US, vcard = VCARD}, + vcard_schema(), + [{'2i', [{<<"user">>, User}, + {<<"luser">>, LUser}, + {<<"fn">>, FN}, + {<<"lfn">>, LFN}, + {<<"family">>, Family}, + {<<"lfamily">>, LFamily}, + {<<"given">>, Given}, + {<<"lgiven">>, LGiven}, + {<<"middle">>, Middle}, + {<<"lmiddle">>, LMiddle}, + {<<"nickname">>, Nickname}, + {<<"lnickname">>, LNickname}, + {<<"bday">>, BDay}, + {<<"lbday">>, LBDay}, + {<<"ctry">>, CTRY}, + {<<"lctry">>, LCTRY}, + {<<"locality">>, Locality}, + {<<"llocality">>, LLocality}, + {<<"email">>, EMail}, + {<<"lemail">>, LEMail}, + {<<"orgname">>, OrgName}, + {<<"lorgname">>, LOrgName}, + {<<"orgunit">>, OrgUnit}, + {<<"lorgunit">>, LOrgUnit}]}])}. + +search(_LServer, _Data, _AllowReturnAll, _MaxMatch) -> + []. + +remove_user(LUser, LServer) -> + {atomic, ejabberd_riak:delete(vcard, {LUser, LServer})}. + +import(_LServer, #vcard{us = {LUser, LServer}, vcard = El} = VCard) -> + #vcard_search{fn = FN, + lfn = LFN, + family = Family, + lfamily = LFamily, + given = Given, + lgiven = LGiven, + middle = Middle, + lmiddle = LMiddle, + nickname = Nickname, + lnickname = LNickname, + bday = BDay, + lbday = LBDay, + ctry = CTRY, + lctry = LCTRY, + locality = Locality, + llocality = LLocality, + email = EMail, + lemail = LEMail, + orgname = OrgName, + lorgname = LOrgName, + orgunit = OrgUnit, + lorgunit = LOrgUnit} = + mod_vcard:make_vcard_search(LUser, LUser, LServer, El), + ejabberd_riak:put(VCard, vcard_schema(), + [{'2i', [{<<"user">>, LUser}, + {<<"luser">>, LUser}, + {<<"fn">>, FN}, + {<<"lfn">>, LFN}, + {<<"family">>, Family}, + {<<"lfamily">>, LFamily}, + {<<"given">>, Given}, + {<<"lgiven">>, LGiven}, + {<<"middle">>, Middle}, + {<<"lmiddle">>, LMiddle}, + {<<"nickname">>, Nickname}, + {<<"lnickname">>, LNickname}, + {<<"bday">>, BDay}, + {<<"lbday">>, LBDay}, + {<<"ctry">>, CTRY}, + {<<"lctry">>, LCTRY}, + {<<"locality">>, Locality}, + {<<"llocality">>, LLocality}, + {<<"email">>, EMail}, + {<<"lemail">>, LEMail}, + {<<"orgname">>, OrgName}, + {<<"lorgname">>, LOrgName}, + {<<"orgunit">>, OrgUnit}, + {<<"lorgunit">>, LOrgUnit}]}]); +import(_LServer, #vcard_search{}) -> + ok. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +vcard_schema() -> + {record_info(fields, vcard), #vcard{}}. diff --git a/src/mod_vcard_sql.erl b/src/mod_vcard_sql.erl new file mode 100644 index 000000000..9fcd5cd5e --- /dev/null +++ b/src/mod_vcard_sql.erl @@ -0,0 +1,268 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 13 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_vcard_sql). + +-compile([{parse_transform, ejabberd_sql_pt}]). + +-behaviour(mod_vcard). + +%% API +-export([init/2, get_vcard/2, set_vcard/4, search/4, remove_user/2, + import/1, import/2, export/1]). + +-include("jlib.hrl"). +-include("mod_vcard.hrl"). +-include("logger.hrl"). +-include("ejabberd_sql_pt.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ok. + +get_vcard(LUser, LServer) -> + case catch sql_queries:get_vcard(LServer, LUser) of + {selected, [{SVCARD}]} -> + case fxml_stream:parse_element(SVCARD) of + {error, _Reason} -> error; + VCARD -> [VCARD] + end; + {selected, []} -> []; + _ -> error + end. + +set_vcard(LUser, LServer, VCARD, + #vcard_search{user = {User, _}, + fn = FN, + lfn = LFN, + family = Family, + lfamily = LFamily, + given = Given, + lgiven = LGiven, + middle = Middle, + lmiddle = LMiddle, + nickname = Nickname, + lnickname = LNickname, + bday = BDay, + lbday = LBDay, + ctry = CTRY, + lctry = LCTRY, + locality = Locality, + llocality = LLocality, + email = EMail, + lemail = LEMail, + orgname = OrgName, + lorgname = LOrgName, + orgunit = OrgUnit, + lorgunit = LOrgUnit}) -> + SVCARD = fxml:element_to_binary(VCARD), + sql_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). + +search(LServer, Data, AllowReturnAll, MaxMatch) -> + MatchSpec = make_matchspec(LServer, Data), + if (MatchSpec == <<"">>) and not AllowReturnAll -> []; + true -> + Limit = case MaxMatch of + infinity -> + <<"">>; + Val -> + [<<" LIMIT ">>, jlib:integer_to_binary(Val)] + end, + case catch ejabberd_sql:sql_query( + LServer, + [<<"select username, fn, family, given, " + "middle, nickname, bday, ctry, " + "locality, email, orgname, orgunit " + "from vcard_search ">>, + MatchSpec, Limit, <<";">>]) of + {selected, + [<<"username">>, <<"fn">>, <<"family">>, <<"given">>, + <<"middle">>, <<"nickname">>, <<"bday">>, <<"ctry">>, + <<"locality">>, <<"email">>, <<"orgname">>, + <<"orgunit">>], Rs} when is_list(Rs) -> + Rs; + Error -> + ?ERROR_MSG("~p", [Error]), [] + end + end. + +remove_user(LUser, LServer) -> + ejabberd_sql:sql_transaction( + LServer, + fun() -> + ejabberd_sql:sql_query_t( + ?SQL("delete from vcard where username=%(LUser)s")), + ejabberd_sql:sql_query_t( + ?SQL("delete from vcard_search where lusername=%(LUser)s")) + end). + +export(_Server) -> + [{vcard, + fun(Host, #vcard{us = {LUser, LServer}, vcard = VCARD}) + when LServer == Host -> + Username = ejabberd_sql:escape(LUser), + SVCARD = + ejabberd_sql:escape(fxml:element_to_binary(VCARD)), + [[<<"delete from vcard where username='">>, Username, <<"';">>], + [<<"insert into vcard(username, vcard) values ('">>, + Username, <<"', '">>, SVCARD, <<"');">>]]; + (_Host, _R) -> + [] + end}, + {vcard_search, + fun(Host, #vcard_search{user = {User, LServer}, luser = LUser, + fn = FN, lfn = LFN, family = Family, + lfamily = LFamily, given = Given, + lgiven = LGiven, middle = Middle, + lmiddle = LMiddle, nickname = Nickname, + lnickname = LNickname, bday = BDay, + lbday = LBDay, ctry = CTRY, lctry = LCTRY, + locality = Locality, llocality = LLocality, + email = EMail, lemail = LEMail, + orgname = OrgName, lorgname = LOrgName, + orgunit = OrgUnit, lorgunit = LOrgUnit}) + when LServer == Host -> + Username = ejabberd_sql:escape(User), + LUsername = ejabberd_sql:escape(LUser), + SFN = ejabberd_sql:escape(FN), + SLFN = ejabberd_sql:escape(LFN), + SFamily = ejabberd_sql:escape(Family), + SLFamily = ejabberd_sql:escape(LFamily), + SGiven = ejabberd_sql:escape(Given), + SLGiven = ejabberd_sql:escape(LGiven), + SMiddle = ejabberd_sql:escape(Middle), + SLMiddle = ejabberd_sql:escape(LMiddle), + SNickname = ejabberd_sql:escape(Nickname), + SLNickname = ejabberd_sql:escape(LNickname), + SBDay = ejabberd_sql:escape(BDay), + SLBDay = ejabberd_sql:escape(LBDay), + SCTRY = ejabberd_sql:escape(CTRY), + SLCTRY = ejabberd_sql:escape(LCTRY), + SLocality = ejabberd_sql:escape(Locality), + SLLocality = ejabberd_sql:escape(LLocality), + SEMail = ejabberd_sql:escape(EMail), + SLEMail = ejabberd_sql:escape(LEMail), + SOrgName = ejabberd_sql:escape(OrgName), + SLOrgName = ejabberd_sql:escape(LOrgName), + SOrgUnit = ejabberd_sql:escape(OrgUnit), + SLOrgUnit = ejabberd_sql:escape(LOrgUnit), + [[<<"delete from vcard_search where lusername='">>, + LUsername, <<"';">>], + [<<"insert into 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)values (">>, + <<" '">>, Username, <<"', '">>, LUsername, + <<"', '">>, SFN, <<"', '">>, SLFN, + <<"', '">>, SFamily, <<"', '">>, SLFamily, + <<"', '">>, SGiven, <<"', '">>, SLGiven, + <<"', '">>, SMiddle, <<"', '">>, SLMiddle, + <<"', '">>, SNickname, <<"', '">>, SLNickname, + <<"', '">>, SBDay, <<"', '">>, SLBDay, + <<"', '">>, SCTRY, <<"', '">>, SLCTRY, + <<"', '">>, SLocality, <<"', '">>, SLLocality, + <<"', '">>, SEMail, <<"', '">>, SLEMail, + <<"', '">>, SOrgName, <<"', '">>, SLOrgName, + <<"', '">>, SOrgUnit, <<"', '">>, SLOrgUnit, + <<"');">>]]; + (_Host, _R) -> + [] + end}]. + +import(LServer) -> + [{<<"select username, vcard from vcard;">>, + fun([LUser, SVCard]) -> + #xmlel{} = VCARD = fxml_stream:parse_element(SVCard), + #vcard{us = {LUser, LServer}, vcard = VCARD} + end}, + {<<"select username, lusername, fn, lfn, family, lfamily, " + "given, lgiven, middle, lmiddle, nickname, lnickname, " + "bday, lbday, ctry, lctry, locality, llocality, email, " + "lemail, orgname, lorgname, orgunit, lorgunit from vcard_search;">>, + fun([User, LUser, FN, LFN, + Family, LFamily, Given, LGiven, + Middle, LMiddle, Nickname, LNickname, + BDay, LBDay, CTRY, LCTRY, Locality, LLocality, + EMail, LEMail, OrgName, LOrgName, OrgUnit, LOrgUnit]) -> + #vcard_search{us = {LUser, LServer}, + user = {User, LServer}, luser = LUser, + fn = FN, lfn = LFN, family = Family, + lfamily = LFamily, given = Given, + lgiven = LGiven, middle = Middle, + lmiddle = LMiddle, nickname = Nickname, + lnickname = LNickname, bday = BDay, + lbday = LBDay, ctry = CTRY, lctry = LCTRY, + locality = Locality, llocality = LLocality, + email = EMail, lemail = LEMail, + orgname = OrgName, lorgname = LOrgName, + orgunit = OrgUnit, lorgunit = LOrgUnit} + end}]. + +import(_, _) -> + pass. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +make_matchspec(LServer, Data) -> + filter_fields(Data, <<"">>, LServer). + +filter_fields([], Match, _LServer) -> + case Match of + <<"">> -> <<"">>; + _ -> [<<" where ">>, Match] + end; +filter_fields([{SVar, [Val]} | Ds], Match, LServer) + when is_binary(Val) and (Val /= <<"">>) -> + LVal = mod_vcard:string2lower(Val), + NewMatch = case SVar of + <<"user">> -> make_val(Match, <<"lusername">>, LVal); + <<"fn">> -> make_val(Match, <<"lfn">>, LVal); + <<"last">> -> make_val(Match, <<"lfamily">>, LVal); + <<"first">> -> make_val(Match, <<"lgiven">>, LVal); + <<"middle">> -> make_val(Match, <<"lmiddle">>, LVal); + <<"nick">> -> make_val(Match, <<"lnickname">>, LVal); + <<"bday">> -> make_val(Match, <<"lbday">>, LVal); + <<"ctry">> -> make_val(Match, <<"lctry">>, LVal); + <<"locality">> -> + make_val(Match, <<"llocality">>, LVal); + <<"email">> -> make_val(Match, <<"lemail">>, LVal); + <<"orgname">> -> make_val(Match, <<"lorgname">>, LVal); + <<"orgunit">> -> make_val(Match, <<"lorgunit">>, LVal); + _ -> Match + end, + filter_fields(Ds, NewMatch, LServer); +filter_fields([_ | Ds], Match, LServer) -> + filter_fields(Ds, Match, LServer). + +make_val(Match, Field, Val) -> + Condition = case str:suffix(<<"*">>, Val) of + true -> + Val1 = str:substr(Val, 1, byte_size(Val) - 1), + SVal = <<(ejabberd_sql:escape_like(Val1))/binary, + "%">>, + [Field, <<" LIKE '">>, SVal, <<"'">>]; + _ -> + SVal = ejabberd_sql:escape(Val), + [Field, <<" = '">>, SVal, <<"'">>] + end, + case Match of + <<"">> -> Condition; + _ -> [Match, <<" and ">>, Condition] + end. diff --git a/src/mod_vcard_xupdate.erl b/src/mod_vcard_xupdate.erl index 96ee09d87..198312c36 100644 --- a/src/mod_vcard_xupdate.erl +++ b/src/mod_vcard_xupdate.erl @@ -17,26 +17,22 @@ -include("ejabberd.hrl"). -include("logger.hrl"). - +-include("mod_vcard_xupdate.hrl"). -include("jlib.hrl"). --record(vcard_xupdate, {us = {<<>>, <<>>} :: {binary(), binary()}, - hash = <<>> :: binary()}). +-callback init(binary(), gen_mod:opts()) -> any(). +-callback import(binary(), #vcard_xupdate{}) -> ok | pass. +-callback add_xupdate(binary(), binary(), binary()) -> {atomic, any()}. +-callback get_xupdate(binary(), binary()) -> binary() | undefined. +-callback remove_xupdate(binary(), binary()) -> {atomic, any()}. %%==================================================================== %% gen_mod callbacks %%==================================================================== start(Host, Opts) -> - case gen_mod:db_type(Host, Opts) of - mnesia -> - mnesia:create_table(vcard_xupdate, - [{disc_copies, [node()]}, - {attributes, - record_info(fields, vcard_xupdate)}]), - update_table(); - _ -> ok - end, + Mod = gen_mod:db_mod(Host, Opts, ?MODULE), + Mod:init(Host, Opts), ejabberd_hooks:add(c2s_update_presence, Host, ?MODULE, update_presence, 100), ejabberd_hooks:add(vcard_set, Host, ?MODULE, vcard_set, @@ -56,7 +52,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 +60,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); @@ -79,75 +75,16 @@ vcard_set(LUser, LServer, VCARD) -> %%==================================================================== add_xupdate(LUser, LServer, Hash) -> - add_xupdate(LUser, LServer, Hash, - gen_mod:db_type(LServer, ?MODULE)). - -add_xupdate(LUser, LServer, Hash, mnesia) -> - F = fun () -> - mnesia:write(#vcard_xupdate{us = {LUser, LServer}, - hash = Hash}) - end, - mnesia:transaction(F); -add_xupdate(LUser, LServer, Hash, riak) -> - {atomic, ejabberd_riak:put(#vcard_xupdate{us = {LUser, LServer}, - hash = Hash}, - vcard_xupdate_schema())}; -add_xupdate(LUser, LServer, Hash, odbc) -> - Username = ejabberd_odbc:escape(LUser), - SHash = ejabberd_odbc:escape(Hash), - F = fun () -> - odbc_queries:update_t(<<"vcard_xupdate">>, - [<<"username">>, <<"hash">>], - [Username, SHash], - [<<"username='">>, Username, <<"'">>]) - end, - ejabberd_odbc:sql_transaction(LServer, F). + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:add_xupdate(LUser, LServer, Hash). get_xupdate(LUser, LServer) -> - get_xupdate(LUser, LServer, - gen_mod:db_type(LServer, ?MODULE)). - -get_xupdate(LUser, LServer, mnesia) -> - case mnesia:dirty_read(vcard_xupdate, {LUser, LServer}) - of - [#vcard_xupdate{hash = Hash}] -> Hash; - _ -> undefined - end; -get_xupdate(LUser, LServer, riak) -> - case ejabberd_riak:get(vcard_xupdate, vcard_xupdate_schema(), - {LUser, LServer}) of - {ok, #vcard_xupdate{hash = Hash}} -> Hash; - _ -> undefined - end; -get_xupdate(LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - case ejabberd_odbc:sql_query(LServer, - [<<"select hash from vcard_xupdate where " - "username='">>, - Username, <<"';">>]) - of - {selected, [<<"hash">>], [[Hash]]} -> Hash; - _ -> undefined - end. + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:get_xupdate(LUser, LServer). remove_xupdate(LUser, LServer) -> - remove_xupdate(LUser, LServer, - gen_mod:db_type(LServer, ?MODULE)). - -remove_xupdate(LUser, LServer, mnesia) -> - F = fun () -> - mnesia:delete({vcard_xupdate, {LUser, LServer}}) - end, - mnesia:transaction(F); -remove_xupdate(LUser, LServer, riak) -> - {atomic, ejabberd_riak:delete(vcard_xupdate, {LUser, LServer})}; -remove_xupdate(LUser, LServer, odbc) -> - Username = ejabberd_odbc:escape(LUser), - F = fun () -> - ejabberd_odbc:sql_query_t([<<"delete from vcard_xupdate where username='">>, - Username, <<"';">>]) - end, - ejabberd_odbc:sql_transaction(LServer, F). + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:remove_xupdate(LUser, LServer). %%%---------------------------------------------------------------------- %%% Presence stanza rebuilding @@ -184,53 +121,17 @@ build_xphotoel(User, Host) -> attrs = [{<<"xmlns">>, ?NS_VCARD_UPDATE}], children = PhotoEl}. -vcard_xupdate_schema() -> - {record_info(fields, vcard_xupdate), #vcard_xupdate{}}. - -update_table() -> - Fields = record_info(fields, vcard_xupdate), - case mnesia:table_info(vcard_xupdate, attributes) of - Fields -> - ejabberd_config:convert_table_to_binary( - vcard_xupdate, Fields, set, - fun(#vcard_xupdate{us = {U, _}}) -> U end, - fun(#vcard_xupdate{us = {U, S}, hash = Hash} = R) -> - R#vcard_xupdate{us = {iolist_to_binary(U), - iolist_to_binary(S)}, - hash = iolist_to_binary(Hash)} - end); - _ -> - ?INFO_MSG("Recreating vcard_xupdate table", []), - mnesia:transform_table(vcard_xupdate, ignore, Fields) - end. - -export(_Server) -> - [{vcard_xupdate, - fun(Host, #vcard_xupdate{us = {LUser, LServer}, hash = Hash}) - when LServer == Host -> - Username = ejabberd_odbc:escape(LUser), - SHash = ejabberd_odbc:escape(Hash), - [[<<"delete from vcard_xupdate where username='">>, - Username, <<"';">>], - [<<"insert into vcard_xupdate(username, " - "hash) values ('">>, - Username, <<"', '">>, SHash, <<"');">>]]; - (_Host, _R) -> - [] - end}]. +export(LServer) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:export(LServer). import(LServer) -> - [{<<"select username, hash from vcard_xupdate;">>, - fun([LUser, Hash]) -> - #vcard_xupdate{us = {LUser, LServer}, hash = Hash} - end}]. - -import(_LServer, mnesia, #vcard_xupdate{} = R) -> - mnesia:dirty_write(R); -import(_LServer, riak, #vcard_xupdate{} = R) -> - ejabberd_riak:put(R, vcard_xupdate_schema()); -import(_, _, _) -> - pass. + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:import(LServer). + +import(LServer, DBType, LA) -> + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:import(LServer, LA). mod_opt_type(db_type) -> fun gen_mod:v_db/1; mod_opt_type(_) -> [db_type]. diff --git a/src/mod_vcard_xupdate_mnesia.erl b/src/mod_vcard_xupdate_mnesia.erl new file mode 100644 index 000000000..f1b1693e4 --- /dev/null +++ b/src/mod_vcard_xupdate_mnesia.erl @@ -0,0 +1,69 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 13 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_vcard_xupdate_mnesia). +-behaviour(mod_vcard_xupdate). + +%% API +-export([init/2, import/2, add_xupdate/3, get_xupdate/2, remove_xupdate/2]). + +-include("mod_vcard_xupdate.hrl"). +-include("logger.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + mnesia:create_table(vcard_xupdate, + [{disc_copies, [node()]}, + {attributes, + record_info(fields, vcard_xupdate)}]), + update_table(). + +add_xupdate(LUser, LServer, Hash) -> + F = fun () -> + mnesia:write(#vcard_xupdate{us = {LUser, LServer}, + hash = Hash}) + end, + mnesia:transaction(F). + +get_xupdate(LUser, LServer) -> + case mnesia:dirty_read(vcard_xupdate, {LUser, LServer}) + of + [#vcard_xupdate{hash = Hash}] -> Hash; + _ -> undefined + end. + +remove_xupdate(LUser, LServer) -> + F = fun () -> + mnesia:delete({vcard_xupdate, {LUser, LServer}}) + end, + mnesia:transaction(F). + +import(_LServer, #vcard_xupdate{} = R) -> + mnesia:dirty_write(R). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +update_table() -> + Fields = record_info(fields, vcard_xupdate), + case mnesia:table_info(vcard_xupdate, attributes) of + Fields -> + ejabberd_config:convert_table_to_binary( + vcard_xupdate, Fields, set, + fun(#vcard_xupdate{us = {U, _}}) -> U end, + fun(#vcard_xupdate{us = {U, S}, hash = Hash} = R) -> + R#vcard_xupdate{us = {iolist_to_binary(U), + iolist_to_binary(S)}, + hash = iolist_to_binary(Hash)} + end); + _ -> + ?INFO_MSG("Recreating vcard_xupdate table", []), + mnesia:transform_table(vcard_xupdate, ignore, Fields) + end. diff --git a/src/mod_vcard_xupdate_riak.erl b/src/mod_vcard_xupdate_riak.erl new file mode 100644 index 000000000..129a0c6a2 --- /dev/null +++ b/src/mod_vcard_xupdate_riak.erl @@ -0,0 +1,44 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 13 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_vcard_xupdate_riak). + +%% API +-export([init/2, import/2, add_xupdate/3, get_xupdate/2, remove_xupdate/2]). + +-include("mod_vcard_xupdate.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ok. + +add_xupdate(LUser, LServer, Hash) -> + {atomic, ejabberd_riak:put(#vcard_xupdate{us = {LUser, LServer}, + hash = Hash}, + vcard_xupdate_schema())}. + +get_xupdate(LUser, LServer) -> + case ejabberd_riak:get(vcard_xupdate, vcard_xupdate_schema(), + {LUser, LServer}) of + {ok, #vcard_xupdate{hash = Hash}} -> Hash; + _ -> undefined + end. + +remove_xupdate(LUser, LServer) -> + {atomic, ejabberd_riak:delete(vcard_xupdate, {LUser, LServer})}. + +import(_LServer, #vcard_xupdate{} = R) -> + ejabberd_riak:put(R, vcard_xupdate_schema()). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +vcard_xupdate_schema() -> + {record_info(fields, vcard_xupdate), #vcard_xupdate{}}. diff --git a/src/mod_vcard_xupdate_sql.erl b/src/mod_vcard_xupdate_sql.erl new file mode 100644 index 000000000..7f0079dd0 --- /dev/null +++ b/src/mod_vcard_xupdate_sql.erl @@ -0,0 +1,79 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 13 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%%------------------------------------------------------------------- +-module(mod_vcard_xupdate_sql). + +%% API +-export([init/2, import/2, add_xupdate/3, get_xupdate/2, remove_xupdate/2, + import/1, export/1]). + +-include("mod_vcard_xupdate.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ok. + +add_xupdate(LUser, LServer, Hash) -> + Username = ejabberd_sql:escape(LUser), + SHash = ejabberd_sql:escape(Hash), + F = fun () -> + sql_queries:update_t(<<"vcard_xupdate">>, + [<<"username">>, <<"hash">>], + [Username, SHash], + [<<"username='">>, Username, <<"'">>]) + end, + ejabberd_sql:sql_transaction(LServer, F). + +get_xupdate(LUser, LServer) -> + Username = ejabberd_sql:escape(LUser), + case ejabberd_sql:sql_query(LServer, + [<<"select hash from vcard_xupdate where " + "username='">>, + Username, <<"';">>]) + of + {selected, [<<"hash">>], [[Hash]]} -> Hash; + _ -> undefined + end. + +remove_xupdate(LUser, LServer) -> + Username = ejabberd_sql:escape(LUser), + F = fun () -> + ejabberd_sql:sql_query_t([<<"delete from vcard_xupdate where username='">>, + Username, <<"';">>]) + end, + ejabberd_sql:sql_transaction(LServer, F). + +export(_Server) -> + [{vcard_xupdate, + fun(Host, #vcard_xupdate{us = {LUser, LServer}, hash = Hash}) + when LServer == Host -> + Username = ejabberd_sql:escape(LUser), + SHash = ejabberd_sql:escape(Hash), + [[<<"delete from vcard_xupdate where username='">>, + Username, <<"';">>], + [<<"insert into vcard_xupdate(username, " + "hash) values ('">>, + Username, <<"', '">>, SHash, <<"');">>]]; + (_Host, _R) -> + [] + end}]. + +import(LServer) -> + [{<<"select username, hash from vcard_xupdate;">>, + fun([LUser, Hash]) -> + #vcard_xupdate{us = {LUser, LServer}, hash = Hash} + end}]. + +import(_LServer, _) -> + pass. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== diff --git a/src/mod_version.erl b/src/mod_version.erl index 0e3b96bd1..7f7759f2d 100644 --- a/src/mod_version.erl +++ b/src/mod_version.erl @@ -52,11 +52,12 @@ stop(Host) -> process_local_iq(_From, To, #iq{id = _ID, type = Type, xmlns = _XMLNS, - sub_el = SubEl} = + sub_el = SubEl, lang = Lang} = IQ) -> case Type of set -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; + Txt = <<"Value 'set' of 'type' attribute is not allowed">>, + IQ#iq{type = error, sub_el = [SubEl, ?ERRT_NOT_ALLOWED(Lang, Txt)]}; get -> Host = To#jid.lserver, OS = case gen_mod:get_module_opt(Host, ?MODULE, show_os, diff --git a/src/node_buddy.erl b/src/node_buddy.erl index 1ebef4e15..aeacacda1 100644 --- a/src/node_buddy.erl +++ b/src/node_buddy.erl @@ -65,7 +65,8 @@ options() -> {max_payload_size, ?MAX_PAYLOAD_SIZE}, {send_last_published_item, never}, {deliver_notifications, true}, - {presence_based_delivery, false}]. + {presence_based_delivery, false}, + {itemreply, none}]. features() -> [<<"create-nodes">>, diff --git a/src/node_club.erl b/src/node_club.erl index a7ef35bd3..64eff7791 100644 --- a/src/node_club.erl +++ b/src/node_club.erl @@ -65,7 +65,8 @@ options() -> {max_payload_size, ?MAX_PAYLOAD_SIZE}, {send_last_published_item, never}, {deliver_notifications, true}, - {presence_based_delivery, false}]. + {presence_based_delivery, false}, + {itemreply, none}]. features() -> [<<"create-nodes">>, diff --git a/src/node_dag.erl b/src/node_dag.erl index cbb8e18c6..09ee85f91 100644 --- a/src/node_dag.erl +++ b/src/node_dag.erl @@ -77,8 +77,9 @@ publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload) -> #pubsub_node{options = Options} -> case find_opt(node_type, Options) of collection -> + Txt = <<"Publishing items to collection node is not allowed">>, {error, - ?ERR_EXTENDED((?ERR_NOT_ALLOWED), <<"publish">>)}; + ?ERR_EXTENDED(?ERRT_NOT_ALLOWED(?MYLANG, Txt), <<"publish">>)}; _ -> node_hometree:publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload) diff --git a/src/node_dispatch.erl b/src/node_dispatch.erl index f9bfaf4f2..178292d31 100644 --- a/src/node_dispatch.erl +++ b/src/node_dispatch.erl @@ -71,7 +71,8 @@ options() -> {max_payload_size, ?MAX_PAYLOAD_SIZE}, {send_last_published_item, never}, {deliver_notifications, true}, - {presence_based_delivery, false}]. + {presence_based_delivery, false}, + {itemreply, none}]. features() -> [<<"create-nodes">>, diff --git a/src/node_flat.erl b/src/node_flat.erl index 3687bb640..c2efe3e12 100644 --- a/src/node_flat.erl +++ b/src/node_flat.erl @@ -84,7 +84,8 @@ options() -> {max_payload_size, ?MAX_PAYLOAD_SIZE}, {send_last_published_item, on_sub_and_presence}, {deliver_notifications, true}, - {presence_based_delivery, false}]. + {presence_based_delivery, false}, + {itemreply, none}]. features() -> [<<"create-nodes">>, diff --git a/src/node_flat_odbc.erl b/src/node_flat_sql.erl index 794d3f986..5baabcfcf 100644 --- a/src/node_flat_odbc.erl +++ b/src/node_flat_sql.erl @@ -1,5 +1,5 @@ %%%---------------------------------------------------------------------- -%%% File : node_flat_odbc.erl +%%% File : node_flat_sql.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> @@ -29,7 +29,7 @@ %%% types.</p> %%% <p>PubSub plugin nodes are using the {@link gen_node} behaviour.</p> --module(node_flat_odbc). +-module(node_flat_sql). -behaviour(gen_pubsub_node). -author('christophe.romain@process-one.net'). @@ -56,14 +56,14 @@ encode_host/1]). init(_Host, _ServerHost, _Opts) -> - %%pubsub_subscription_odbc:init(), + %%pubsub_subscription_sql:init(), ok. terminate(_Host, _ServerHost) -> ok. options() -> - [{odbc, true}, {rsm, true} | node_flat:options()]. + [{sql, true}, {rsm, true} | node_flat:options()]. features() -> [<<"rsm">> | node_flat:features()]. @@ -74,14 +74,14 @@ create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) -> 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) " + catch ejabberd_sql:sql_query_t([<<"insert into pubsub_state(nodeid, jid, affiliation, subscriptions) " "values(">>, state_to_raw(Nidx, State), <<");">>]), {result, {default, broadcast}}. delete_node(Nodes) -> Reply = lists:map(fun (#pubsub_node{id = Nidx} = PubsubNode) -> Subscriptions = case catch - ejabberd_odbc:sql_query_t([<<"select jid, subscriptions " + ejabberd_sql:sql_query_t([<<"select jid, subscriptions " "from pubsub_state where nodeid='">>, Nidx, <<"';">>]) of {selected, [<<"jid">>, <<"subscriptions">>], RItems} -> @@ -130,12 +130,12 @@ subscribe_node(Nidx, Sender, Subscriber, AccessModel, %% % Requesting entity is anonymous %% {error, ?ERR_FORBIDDEN}; true -> - %%{result, SubId} = pubsub_subscription_odbc:subscribe_node(Subscriber, Nidx, Options), + %%{result, SubId} = pubsub_subscription_sql:subscribe_node(Subscriber, Nidx, Options), {NewSub, SubId} = case Subscriptions of [{subscribed, Id}|_] -> {subscribed, Id}; [] -> - Id = pubsub_subscription_odbc:make_subid(), + Id = pubsub_subscription_sql:make_subid(), Sub = case AccessModel of authorize -> pending; _ -> subscribed @@ -209,7 +209,7 @@ 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), + %%pubsub_subscription_sql:unsubscribe_node(SubKey, Nidx, SubId), case {Affiliation, NewSubs} of {none, []} -> del_state(Nidx, SubKey); _ -> update_subscription(Nidx, SubKey, NewSubs) @@ -296,12 +296,12 @@ get_entity_affiliations(Host, Owner) -> H = encode_host(Host), J = encode_jid(GenKey), Reply = case catch - ejabberd_odbc:sql_query_t([<<"select node, type, i.nodeid, affiliation " + ejabberd_sql: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)} + [{nodetree_tree_sql:raw_to_node(Host, [N, <<"">>, T, I]), decode_affiliation(A)} || [N, T, I, A] <- RItems]; _ -> [] @@ -310,7 +310,7 @@ get_entity_affiliations(Host, Owner) -> get_node_affiliations(Nidx) -> Reply = case catch - ejabberd_odbc:sql_query_t([<<"select jid, affiliation from pubsub_state " + ejabberd_sql:sql_query_t([<<"select jid, affiliation from pubsub_state " "where nodeid='">>, Nidx, <<"';">>]) of {selected, [<<"jid">>, <<"affiliation">>], RItems} -> @@ -325,7 +325,7 @@ get_affiliation(Nidx, Owner) -> GenKey = jid:remove_resource(SubKey), J = encode_jid(GenKey), Reply = case catch - ejabberd_odbc:sql_query_t([<<"select affiliation from pubsub_state " + ejabberd_sql:sql_query_t([<<"select affiliation from pubsub_state " "where nodeid='">>, Nidx, <<"' and jid='">>, J, <<"';">>]) of {selected, [<<"affiliation">>], [[A]]} -> @@ -360,11 +360,11 @@ get_entity_subscriptions(Host, Owner) -> "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 + Reply = case catch ejabberd_sql: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]), + Node = nodetree_tree_sql:raw_to_node(Host, [N, <<"">>, T, I]), Jid = decode_jid(J), case decode_subscriptions(S) of [] -> @@ -411,11 +411,11 @@ get_entity_subscriptions_for_send_last(Host, Owner) -> "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 + Reply = case catch ejabberd_sql: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]), + Node = nodetree_tree_sql:raw_to_node(Host, [N, <<"">>, T, I]), Jid = decode_jid(J), case decode_subscriptions(S) of [] -> @@ -435,7 +435,7 @@ 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 " + ejabberd_sql:sql_query_t([<<"select jid, subscriptions from pubsub_state " "where nodeid='">>, Nidx, <<"';">>]) of {selected, [<<"jid">>, <<"subscriptions">>], RItems} -> @@ -461,7 +461,7 @@ 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 " + ejabberd_sql:sql_query_t([<<"select subscriptions from pubsub_state where " "nodeid='">>, Nidx, <<"' and jid='">>, J, <<"';">>]) of {selected, [<<"subscriptions">>], [[S]]} -> @@ -507,14 +507,14 @@ 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(), + %%{result, SubId} = pubsub_subscription_sql:subscribe_node(Owner, Nidx, []), + SubId = pubsub_subscription_sql: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), + %%pubsub_subscription_sql:unsubscribe_node(SubState#pubsub_state.stateid, Nidx, SubId), NewSubs = [{S, Sid} || {S, Sid} <- SubState#pubsub_state.subscriptions, SubId =/= Sid], @@ -561,7 +561,7 @@ get_nodes_helper(NodeTree, #pubsub_state{stateid = {_, N}, subscriptions = Subs} get_states(Nidx) -> case catch - ejabberd_odbc:sql_query_t([<<"select jid, affiliation, subscriptions " + ejabberd_sql:sql_query_t([<<"select jid, affiliation, subscriptions " "from pubsub_state where nodeid='">>, Nidx, <<"';">>]) of {selected, @@ -591,7 +591,7 @@ get_state(Nidx, JID) -> get_state_without_itemids(Nidx, JID) -> J = encode_jid(JID), case catch - ejabberd_odbc:sql_query_t([<<"select jid, affiliation, subscriptions " + ejabberd_sql:sql_query_t([<<"select jid, affiliation, subscriptions " "from pubsub_state where jid='">>, J, <<"' and nodeid='">>, Nidx, <<"';">>]) of {selected, @@ -613,14 +613,14 @@ set_state(Nidx, State) -> 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, + ejabberd_sql: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) " + ejabberd_sql:sql_query_t([<<"insert into pubsub_state(nodeid, jid, affiliation, subscriptions) " "values('">>, Nidx, <<"', '">>, J, <<"', '">>, A, <<"', '">>, S, <<"');">>]) end, @@ -628,13 +628,13 @@ set_state(Nidx, State) -> del_state(Nidx, JID) -> J = encode_jid(JID), - catch ejabberd_odbc:sql_query_t([<<"delete from pubsub_state where jid='">>, + catch ejabberd_sql:sql_query_t([<<"delete from pubsub_state where jid='">>, J, <<"' and nodeid='">>, Nidx, <<"';">>]), ok. %get_items(Nidx, _From) -> % case catch -% ejabberd_odbc:sql_query_t([<<"select itemid, publisher, creation, modification, payload " +% ejabberd_sql:sql_query_t([<<"select itemid, publisher, creation, modification, payload " % "from pubsub_item where nodeid='">>, Nidx, % <<"' order by modification desc;">>]) % of @@ -647,7 +647,7 @@ del_state(Nidx, JID) -> get_items(Nidx, From, none) -> MaxItems = case catch - ejabberd_odbc:sql_query_t([<<"select val from pubsub_node_option " + ejabberd_sql:sql_query_t([<<"select val from pubsub_node_option " "where nodeid='">>, Nidx, <<"' and name='max_items';">>]) of {selected, [<<"val">>], [[Value]]} -> @@ -659,7 +659,7 @@ get_items(Nidx, From, none) -> 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)), + Max = ejabberd_sql:escape(jlib:i2l(M)), {Way, Order} = case Direction of % aft -> {<<"<">>, <<"desc">>}; % before when I == <<>> -> {<<"is not">>, <<"asc">>}; @@ -672,11 +672,11 @@ get_items(Nidx, _From, [AttrName, Id] = case I of undefined when IncIndex =/= undefined -> case catch - ejabberd_odbc:sql_query_t([<<"select modification from pubsub_item pi " + ejabberd_sql: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)), <<" );">>]) + ejabberd_sql:escape(jlib:i2l(IncIndex)), <<" );">>]) of {selected, [_], [[O]]} -> [<<"modification">>, <<"'", O/binary, "'">>]; @@ -688,27 +688,36 @@ get_items(Nidx, _From, <<>> -> [<<"modification">>, <<"null">>]; I -> - [A, B] = str:tokens(ejabberd_odbc:escape(jlib:i2l(I)), <<"@">>), + [A, B] = str:tokens(ejabberd_sql:escape(jlib:i2l(I)), <<"@">>), [A, <<"'", B/binary, "'">>] end, Count = case catch - ejabberd_odbc:sql_query_t([<<"select count(*) from pubsub_item where nodeid='">>, Nidx, <<"';">>]) + ejabberd_sql: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 + Query = fun(mssql, _) -> + ejabberd_sql:sql_query_t( + [<<"select top ">>, jlib:i2l(Max), + <<" itemid, publisher, creation, modification, payload " + "from pubsub_item where nodeid='">>, Nidx, + <<"' and ">>, AttrName, <<" ">>, Way, <<" ">>, Id, <<" order by ">>, + AttrName, <<" ">>, Order, <<";">>]); + (_, _) -> + ejabberd_sql: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), <<" ;">>]) + end, + case catch ejabberd_sql:sql_query_t(Query) 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 " + ejabberd_sql:sql_query_t([<<"select count(*) from pubsub_item " "where nodeid='">>, Nidx, <<"' and ">>, AttrName, <<" > '">>, F, <<"';">>]) of @@ -760,11 +769,20 @@ get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId, 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 + Limit = jlib:i2l(Count), + Query = fun(mssql, _) -> + ejabberd_sql:sql_query_t( + [<<"select top ">>, Limit, + <<" itemid, publisher, creation, modification, payload " + "from pubsub_item where nodeid='">>, Nidx, + <<"' order by modification desc ;">>]); + (_, _) -> + ejabberd_sql:sql_query_t( + [<<"select itemid, publisher, creation, modification, payload " + "from pubsub_item where nodeid='">>, Nidx, + <<"' order by modification desc limit ">>, Limit, <<";">>]) + end, + case catch ejabberd_sql:sql_query_t(Query) of {selected, [<<"itemid">>, <<"publisher">>, <<"creation">>, <<"modification">>, <<"payload">>], RItems} -> {result, [raw_to_item(Nidx, RItem) || RItem <- RItems]}; @@ -773,17 +791,19 @@ get_last_items(Nidx, _From, Count) -> end. get_item(Nidx, ItemId) -> - I = ejabberd_odbc:escape(ItemId), + I = ejabberd_sql:escape(ItemId), case catch - ejabberd_odbc:sql_query_t([<<"select itemid, publisher, creation, " + ejabberd_sql: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} + {selected, _, []} -> + {error, ?ERR_ITEM_NOT_FOUND}; + {'EXIT', _} -> + {error, ?ERRT_INTERNAL_SERVER_ERROR(?MYLANG, <<"Database failure">>)} end. get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId) -> @@ -819,17 +839,17 @@ get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, _Sub set_item(Item) -> {ItemId, Nidx} = Item#pubsub_item.itemid, - I = ejabberd_odbc:escape(ItemId), + I = ejabberd_sql: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], <<>>)), + XML = ejabberd_sql: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, + ejabberd_sql:sql_query_t([<<"update pubsub_item set publisher='">>, P, <<"', modification='">>, S(M), <<"', payload='">>, XML, <<"' where nodeid='">>, Nidx, <<"' and itemid='">>, I, <<"';">>]) @@ -838,7 +858,7 @@ set_item(Item) -> ok; _ -> catch - ejabberd_odbc:sql_query_t([<<"insert into pubsub_item (nodeid, itemid, " + ejabberd_sql:sql_query_t([<<"insert into pubsub_item (nodeid, itemid, " "publisher, creation, modification, payload) " "values('">>, Nidx, <<"', '">>, I, <<"', '">>, P, <<"', '">>, S(C), <<"', '">>, S(M), @@ -847,8 +867,8 @@ set_item(Item) -> ok. del_item(Nidx, ItemId) -> - I = ejabberd_odbc:escape(ItemId), - catch ejabberd_odbc:sql_query_t([<<"delete from pubsub_item where itemid='">>, + I = ejabberd_sql:escape(ItemId), + catch ejabberd_sql:sql_query_t([<<"delete from pubsub_item where itemid='">>, I, <<"' and nodeid='">>, Nidx, <<"';">>]). del_items(_, []) -> @@ -856,9 +876,9 @@ del_items(_, []) -> del_items(Nidx, [ItemId]) -> del_item(Nidx, ItemId); del_items(Nidx, ItemIds) -> - I = str:join([[<<"'">>, ejabberd_odbc:escape(X), <<"'">>] || X <- ItemIds], <<",">>), + I = str:join([[<<"'">>, ejabberd_sql:escape(X), <<"'">>] || X <- ItemIds], <<",">>), catch - ejabberd_odbc:sql_query_t([<<"delete from pubsub_item where itemid in (">>, + ejabberd_sql:sql_query_t([<<"delete from pubsub_item where itemid in (">>, I, <<") and nodeid='">>, Nidx, <<"';">>]). get_item_name(_Host, _Node, Id) -> @@ -883,7 +903,7 @@ 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 " + ejabberd_sql:sql_query_t([<<"select itemid from pubsub_item where " "nodeid='">>, Nidx, <<"' and publisher like '">>, SJID, <<"%' order by modification desc;">>]) of @@ -896,7 +916,7 @@ itemids(Nidx, SJID) -> select_affiliation_subscriptions(Nidx, JID) -> J = encode_jid(JID), case catch - ejabberd_odbc:sql_query_t([<<"select affiliation,subscriptions from " + ejabberd_sql:sql_query_t([<<"select affiliation,subscriptions from " "pubsub_state where nodeid='">>, Nidx, <<"' and jid='">>, J, <<"';">>]) of @@ -917,7 +937,7 @@ 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='">>, + ejabberd_sql:sql_query_t([<<"update pubsub_state set affiliation='">>, A, <<"' where nodeid='">>, Nidx, <<"' and jid='">>, J, <<"';">>]) of @@ -925,7 +945,7 @@ update_affiliation(Nidx, JID, Affiliation) -> ok; _ -> catch - ejabberd_odbc:sql_query_t([<<"insert into pubsub_state(nodeid, jid, affiliation, subscriptions) " + ejabberd_sql:sql_query_t([<<"insert into pubsub_state(nodeid, jid, affiliation, subscriptions) " "values('">>, Nidx, <<"', '">>, J, <<"', '">>, A, <<"', '');">>]) end. @@ -933,14 +953,14 @@ 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, + ejabberd_sql: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) " + ejabberd_sql:sql_query_t([<<"insert into pubsub_state(nodeid, jid, affiliation, subscriptions) " "values('">>, Nidx, <<"', '">>, J, <<"', 'n', '">>, S, <<"');">>]) end. @@ -989,14 +1009,14 @@ decode_subscriptions(Subscriptions) -> -> binary() ). encode_jid(JID) -> - ejabberd_odbc:escape(jid:to_string(JID)). + ejabberd_sql: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). +encode_host(Host) -> ejabberd_sql:escape(Host). -spec(encode_affiliation/1 :: ( Arg :: atom()) @@ -1041,7 +1061,7 @@ raw_to_item(Nidx, [ItemId, SJID, Creation, Modification, XML]) -> [T1, T2, T3] = str:tokens(Str, <<":">>), {jlib:l2i(T1), jlib:l2i(T2), jlib:l2i(T3)} end, - Payload = case xml_stream:parse_element(XML) of + Payload = case fxml_stream:parse_element(XML) of {error, _Reason} -> []; El -> [El] end, diff --git a/src/node_hometree_odbc.erl b/src/node_hometree_sql.erl index 6ac5c37b8..4ce6f8554 100644 --- a/src/node_hometree_odbc.erl +++ b/src/node_hometree_sql.erl @@ -1,5 +1,5 @@ %%%---------------------------------------------------------------------- -%%% File : node_hometree_odbc.erl +%%% File : node_hometree_sql.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> @@ -23,7 +23,7 @@ %%% %%%---------------------------------------------------------------------- --module(node_hometree_odbc). +-module(node_hometree_sql). -behaviour(gen_pubsub_node). -author('christophe.romain@process-one.net'). @@ -45,17 +45,17 @@ get_entity_subscriptions_for_send_last/2, get_last_items/3]). init(Host, ServerHost, Opts) -> - node_flat_odbc:init(Host, ServerHost, Opts), + node_flat_sql: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) -> - node_flat_odbc:terminate(Host, ServerHost). + node_flat_sql:terminate(Host, ServerHost). options() -> - [{odbc, true}, {rsm, true} | node_hometree:options()]. + [{sql, true}, {rsm, true} | node_hometree:options()]. features() -> [<<"rsm">> | node_hometree:features()]. @@ -64,92 +64,92 @@ create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) -> node_hometree:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). create_node(Nidx, Owner) -> - node_flat_odbc:create_node(Nidx, Owner). + node_flat_sql:create_node(Nidx, Owner). delete_node(Nodes) -> - node_flat_odbc:delete_node(Nodes). + node_flat_sql:delete_node(Nodes). subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options) -> - node_flat_odbc:subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, + node_flat_sql: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). + node_flat_sql: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). + node_flat_sql:publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload). remove_extra_items(Nidx, MaxItems, ItemIds) -> - node_flat_odbc:remove_extra_items(Nidx, MaxItems, ItemIds). + node_flat_sql:remove_extra_items(Nidx, MaxItems, ItemIds). delete_item(Nidx, Publisher, PublishModel, ItemId) -> - node_flat_odbc:delete_item(Nidx, Publisher, PublishModel, ItemId). + node_flat_sql:delete_item(Nidx, Publisher, PublishModel, ItemId). purge_node(Nidx, Owner) -> - node_flat_odbc:purge_node(Nidx, Owner). + node_flat_sql:purge_node(Nidx, Owner). get_entity_affiliations(Host, Owner) -> - node_flat_odbc:get_entity_affiliations(Host, Owner). + node_flat_sql:get_entity_affiliations(Host, Owner). get_node_affiliations(Nidx) -> - node_flat_odbc:get_node_affiliations(Nidx). + node_flat_sql:get_node_affiliations(Nidx). get_affiliation(Nidx, Owner) -> - node_flat_odbc:get_affiliation(Nidx, Owner). + node_flat_sql:get_affiliation(Nidx, Owner). set_affiliation(Nidx, Owner, Affiliation) -> - node_flat_odbc:set_affiliation(Nidx, Owner, Affiliation). + node_flat_sql:set_affiliation(Nidx, Owner, Affiliation). get_entity_subscriptions(Host, Owner) -> - node_flat_odbc:get_entity_subscriptions(Host, Owner). + node_flat_sql:get_entity_subscriptions(Host, Owner). get_entity_subscriptions_for_send_last(Host, Owner) -> - node_flat_odbc:get_entity_subscriptions_for_send_last(Host, Owner). + node_flat_sql:get_entity_subscriptions_for_send_last(Host, Owner). get_node_subscriptions(Nidx) -> - node_flat_odbc:get_node_subscriptions(Nidx). + node_flat_sql:get_node_subscriptions(Nidx). get_subscriptions(Nidx, Owner) -> - node_flat_odbc:get_subscriptions(Nidx, Owner). + node_flat_sql:get_subscriptions(Nidx, Owner). set_subscriptions(Nidx, Owner, Subscription, SubId) -> - node_flat_odbc:set_subscriptions(Nidx, Owner, Subscription, SubId). + node_flat_sql:set_subscriptions(Nidx, Owner, Subscription, SubId). get_pending_nodes(Host, Owner) -> - node_flat_odbc:get_pending_nodes(Host, Owner). + node_flat_sql:get_pending_nodes(Host, Owner). get_states(Nidx) -> - node_flat_odbc:get_states(Nidx). + node_flat_sql:get_states(Nidx). get_state(Nidx, JID) -> - node_flat_odbc:get_state(Nidx, JID). + node_flat_sql:get_state(Nidx, JID). set_state(State) -> - node_flat_odbc:set_state(State). + node_flat_sql:set_state(State). get_items(Nidx, From, RSM) -> - node_flat_odbc:get_items(Nidx, From, RSM). + node_flat_sql:get_items(Nidx, From, RSM). get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM) -> - node_flat_odbc:get_items(Nidx, JID, AccessModel, + node_flat_sql:get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM). get_item(Nidx, ItemId) -> - node_flat_odbc:get_item(Nidx, ItemId). + node_flat_sql:get_item(Nidx, ItemId). get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> - node_flat_odbc:get_item(Nidx, ItemId, JID, + node_flat_sql:get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId). set_item(Item) -> - node_flat_odbc:set_item(Item). + node_flat_sql:set_item(Item). get_item_name(Host, Node, Id) -> - node_flat_odbc:get_item_name(Host, Node, Id). + node_flat_sql:get_item_name(Host, Node, Id). get_last_items(Nidx, From, Count) -> - node_flat_odbc:get_last_items(Nidx, From, Count). + node_flat_sql:get_last_items(Nidx, From, Count). node_to_path(Node) -> node_hometree:node_to_path(Node). diff --git a/src/node_mb.erl b/src/node_mb.erl index 1213805c4..0bfb51ecc 100644 --- a/src/node_mb.erl +++ b/src/node_mb.erl @@ -78,7 +78,8 @@ options() -> {max_payload_size, ?MAX_PAYLOAD_SIZE}, {send_last_published_item, on_sub_and_presence}, {deliver_notifications, true}, - {presence_based_delivery, true}]. + {presence_based_delivery, true}, + {itemreply, none}]. features() -> [<<"create-nodes">>, diff --git a/src/node_mix.erl b/src/node_mix.erl new file mode 100644 index 000000000..b57d58493 --- /dev/null +++ b/src/node_mix.erl @@ -0,0 +1,168 @@ +%%%------------------------------------------------------------------- +%%% @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}, + {itemreply, none}]. + +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_sql.erl b/src/node_mix_sql.erl new file mode 100644 index 000000000..b5b9a94eb --- /dev/null +++ b/src/node_mix_sql.erl @@ -0,0 +1,171 @@ +%%%------------------------------------------------------------------- +%%% @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_sql). + +-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_sql:init(Host, ServerHost, Opts). + +terminate(Host, ServerHost) -> + node_flat_sql: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}, + {itemreply, none}]. + +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_sql:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). + +create_node(Nidx, Owner) -> + node_flat_sql:create_node(Nidx, Owner). + +delete_node(Removed) -> + node_flat_sql:delete_node(Removed). + +subscribe_node(Nidx, Sender, Subscriber, AccessModel, + SendLast, PresenceSubscription, RosterGroup, Options) -> + node_flat_sql:subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, + PresenceSubscription, RosterGroup, Options). + +unsubscribe_node(Nidx, Sender, Subscriber, SubId) -> + node_flat_sql:unsubscribe_node(Nidx, Sender, Subscriber, SubId). + +publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload) -> + node_flat_sql:publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload). + +remove_extra_items(Nidx, MaxItems, ItemIds) -> + node_flat_sql:remove_extra_items(Nidx, MaxItems, ItemIds). + +delete_item(Nidx, Publisher, PublishModel, ItemId) -> + node_flat_sql:delete_item(Nidx, Publisher, PublishModel, ItemId). + +purge_node(Nidx, Owner) -> + node_flat_sql:purge_node(Nidx, Owner). + +get_entity_affiliations(Host, Owner) -> + node_flat_sql:get_entity_affiliations(Host, Owner). + +get_node_affiliations(Nidx) -> + node_flat_sql:get_node_affiliations(Nidx). + +get_affiliation(Nidx, Owner) -> + node_flat_sql:get_affiliation(Nidx, Owner). + +set_affiliation(Nidx, Owner, Affiliation) -> + node_flat_sql:set_affiliation(Nidx, Owner, Affiliation). + +get_entity_subscriptions(Host, Owner) -> + node_flat_sql:get_entity_subscriptions(Host, Owner). + +get_node_subscriptions(Nidx) -> + node_flat_sql:get_node_subscriptions(Nidx). + +get_subscriptions(Nidx, Owner) -> + node_flat_sql:get_subscriptions(Nidx, Owner). + +set_subscriptions(Nidx, Owner, Subscription, SubId) -> + node_flat_sql:set_subscriptions(Nidx, Owner, Subscription, SubId). + +get_pending_nodes(Host, Owner) -> + node_flat_sql:get_pending_nodes(Host, Owner). + +get_states(Nidx) -> + node_flat_sql:get_states(Nidx). + +get_state(Nidx, JID) -> + node_flat_sql:get_state(Nidx, JID). + +set_state(State) -> + node_flat_sql:set_state(State). + +get_items(Nidx, From, RSM) -> + node_flat_sql:get_items(Nidx, From, RSM). + +get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM) -> + node_flat_sql:get_items(Nidx, JID, AccessModel, + PresenceSubscription, RosterGroup, SubId, RSM). + +get_item(Nidx, ItemId) -> + node_flat_sql:get_item(Nidx, ItemId). + +get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> + node_flat_sql:get_item(Nidx, ItemId, JID, AccessModel, + PresenceSubscription, RosterGroup, SubId). + +set_item(Item) -> + node_flat_sql:set_item(Item). + +get_item_name(Host, Node, Id) -> + node_flat_sql:get_item_name(Host, Node, Id). + +node_to_path(Node) -> + node_flat_sql:node_to_path(Node). + +path_to_node(Path) -> + node_flat_sql:path_to_node(Path). + +get_entity_subscriptions_for_send_last(Host, Owner) -> + node_flat_sql:get_entity_subscriptions_for_send_last(Host, Owner). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== diff --git a/src/node_online.erl b/src/node_online.erl index 1e9e29533..ea492fc85 100644 --- a/src/node_online.erl +++ b/src/node_online.erl @@ -76,7 +76,8 @@ options() -> {max_payload_size, ?MAX_PAYLOAD_SIZE}, {send_last_published_item, on_sub_and_presence}, {deliver_notifications, true}, - {presence_based_delivery, true}]. + {presence_based_delivery, true}, + {itemreply, none}]. features() -> node_flat:features(). diff --git a/src/node_pep.erl b/src/node_pep.erl index 726146b90..2ac4da627 100644 --- a/src/node_pep.erl +++ b/src/node_pep.erl @@ -72,7 +72,8 @@ options() -> {max_payload_size, ?MAX_PAYLOAD_SIZE}, {send_last_published_item, on_sub_and_presence}, {deliver_notifications, true}, - {presence_based_delivery, true}]. + {presence_based_delivery, true}, + {itemreply, none}]. features() -> [<<"create-nodes">>, @@ -257,7 +258,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_sql.erl index 6eb0de040..c5b31d15f 100644 --- a/src/node_pep_odbc.erl +++ b/src/node_pep_sql.erl @@ -1,5 +1,5 @@ %%%---------------------------------------------------------------------- -%%% File : node_pep_odbc.erl +%%% File : node_pep_sql.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> @@ -26,7 +26,7 @@ %%% @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). +-module(node_pep_sql). -behaviour(gen_pubsub_node). -author('christophe.romain@process-one.net'). @@ -49,16 +49,16 @@ get_entity_subscriptions_for_send_last/2, get_last_items/3]). init(Host, ServerHost, Opts) -> - node_flat_odbc:init(Host, ServerHost, Opts), + node_flat_sql:init(Host, ServerHost, Opts), complain_if_modcaps_disabled(ServerHost), ok. terminate(Host, ServerHost) -> - node_flat_odbc:terminate(Host, ServerHost), + node_flat_sql:terminate(Host, ServerHost), ok. options() -> - [{odbc, true}, {rsm, true} | node_pep:options()]. + [{sql, true}, {rsm, true} | node_pep:options()]. features() -> [<<"rsm">> | node_pep:features()]. @@ -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_flat_odbc:create_node(Nidx, Owner), + node_flat_sql:create_node(Nidx, Owner), {result, {default, broadcast}}. delete_node(Nodes) -> - {result, {_, _, Result}} = node_flat_odbc:delete_node(Nodes), + {result, {_, _, Result}} = node_flat_sql:delete_node(Nodes), {result, {[], Result}}. subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options) -> - node_flat_odbc:subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, + node_flat_sql:subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options). unsubscribe_node(Nidx, Sender, Subscriber, SubId) -> - case node_flat_odbc:unsubscribe_node(Nidx, Sender, Subscriber, SubId) of + case node_flat_sql:unsubscribe_node(Nidx, Sender, Subscriber, SubId) of {error, Error} -> {error, Error}; {result, _} -> {result, []} end. publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload) -> - node_flat_odbc:publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload). + node_flat_sql:publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload). remove_extra_items(Nidx, MaxItems, ItemIds) -> - node_flat_odbc:remove_extra_items(Nidx, MaxItems, ItemIds). + node_flat_sql:remove_extra_items(Nidx, MaxItems, ItemIds). delete_item(Nidx, Publisher, PublishModel, ItemId) -> - node_flat_odbc:delete_item(Nidx, Publisher, PublishModel, ItemId). + node_flat_sql:delete_item(Nidx, Publisher, PublishModel, ItemId). purge_node(Nidx, Owner) -> - node_flat_odbc:purge_node(Nidx, Owner). + node_flat_sql:purge_node(Nidx, Owner). get_entity_affiliations(_Host, Owner) -> OwnerKey = jid:tolower(jid:remove_resource(Owner)), - node_flat_odbc:get_entity_affiliations(OwnerKey, Owner). + node_flat_sql:get_entity_affiliations(OwnerKey, Owner). get_node_affiliations(Nidx) -> - node_flat_odbc:get_node_affiliations(Nidx). + node_flat_sql:get_node_affiliations(Nidx). get_affiliation(Nidx, Owner) -> - node_flat_odbc:get_affiliation(Nidx, Owner). + node_flat_sql:get_affiliation(Nidx, Owner). set_affiliation(Nidx, Owner, Affiliation) -> - node_flat_odbc:set_affiliation(Nidx, Owner, Affiliation). + node_flat_sql:set_affiliation(Nidx, Owner, Affiliation). get_entity_subscriptions(_Host, Owner) -> 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), + Host = node_flat_sql:encode_host(element(2, SubKey)), + SJ = node_flat_sql:encode_jid(SubKey), + GJ = node_flat_sql:encode_jid(GenKey), Query = case SubKey of GenKey -> [<<"select host, node, type, i.nodeid, jid, " @@ -129,16 +129,16 @@ get_entity_subscriptions(_Host, Owner) -> "where i.nodeid = n.nodeid and jid " "in ('">>, SJ, <<"', '">>, GJ, <<"') and host like '%@">>, Host, <<"';">>] end, - Reply = case catch ejabberd_odbc:sql_query_t(Query) of + Reply = case catch ejabberd_sql:sql_query_t(Query) of {selected, [<<"host">>, <<"node">>, <<"type">>, <<"nodeid">>, <<"jid">>, <<"subscriptions">>], RItems} -> lists:map(fun ([H, N, T, I, J, S]) -> - O = node_flat_odbc:decode_jid(H), - Node = nodetree_tree_odbc:raw_to_node(O, [N, <<"">>, T, I]), + O = node_flat_sql:decode_jid(H), + Node = nodetree_tree_sql:raw_to_node(O, [N, <<"">>, T, I]), {Node, - node_flat_odbc:decode_subscriptions(S), - node_flat_odbc:decode_jid(J)} + node_flat_sql:decode_subscriptions(S), + node_flat_sql:decode_jid(J)} end, RItems); _ -> @@ -149,9 +149,9 @@ get_entity_subscriptions(_Host, Owner) -> get_entity_subscriptions_for_send_last(_Host, Owner) -> 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), + Host = node_flat_sql:encode_host(element(2, SubKey)), + SJ = node_flat_sql:encode_jid(SubKey), + GJ = node_flat_sql:encode_jid(GenKey), Query = case SubKey of GenKey -> [<<"select host, node, type, i.nodeid, jid, " @@ -168,16 +168,16 @@ get_entity_subscriptions_for_send_last(_Host, Owner) -> "val='on_sub_and_presence' and jid in ", "('">>, SJ, <<"', '">>, GJ, <<"') and host like '%@">>, Host, <<"';">>] end, - Reply = case catch ejabberd_odbc:sql_query_t(Query) of + Reply = case catch ejabberd_sql:sql_query_t(Query) of {selected, [<<"host">>, <<"node">>, <<"type">>, <<"nodeid">>, <<"jid">>, <<"subscriptions">>], RItems} -> lists:map(fun ([H, N, T, I, J, S]) -> - O = node_flat_odbc:decode_jid(H), - Node = nodetree_tree_odbc:raw_to_node(O, [N, <<"">>, T, I]), + O = node_flat_sql:decode_jid(H), + Node = nodetree_tree_sql:raw_to_node(O, [N, <<"">>, T, I]), {Node, - node_flat_odbc:decode_subscriptions(S), - node_flat_odbc:decode_jid(J)} + node_flat_sql:decode_subscriptions(S), + node_flat_sql:decode_jid(J)} end, RItems); _ -> @@ -186,54 +186,54 @@ get_entity_subscriptions_for_send_last(_Host, Owner) -> {result, Reply}. get_node_subscriptions(Nidx) -> - node_flat_odbc:get_node_subscriptions(Nidx). + node_flat_sql:get_node_subscriptions(Nidx). get_subscriptions(Nidx, Owner) -> - node_flat_odbc:get_subscriptions(Nidx, Owner). + node_flat_sql:get_subscriptions(Nidx, Owner). set_subscriptions(Nidx, Owner, Subscription, SubId) -> - node_flat_odbc:set_subscriptions(Nidx, Owner, Subscription, SubId). + node_flat_sql:set_subscriptions(Nidx, Owner, Subscription, SubId). get_pending_nodes(Host, Owner) -> - node_flat_odbc:get_pending_nodes(Host, Owner). + node_flat_sql:get_pending_nodes(Host, Owner). get_states(Nidx) -> - node_flat_odbc:get_states(Nidx). + node_flat_sql:get_states(Nidx). get_state(Nidx, JID) -> - node_flat_odbc:get_state(Nidx, JID). + node_flat_sql:get_state(Nidx, JID). set_state(State) -> - node_flat_odbc:set_state(State). + node_flat_sql:set_state(State). get_items(Nidx, From, RSM) -> - node_flat_odbc:get_items(Nidx, From, RSM). + node_flat_sql:get_items(Nidx, From, RSM). get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM) -> - node_flat_odbc:get_items(Nidx, JID, AccessModel, + node_flat_sql:get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM). get_last_items(Nidx, JID, Count) -> - node_flat_odbc:get_last_items(Nidx, JID, Count). + node_flat_sql:get_last_items(Nidx, JID, Count). get_item(Nidx, ItemId) -> - node_flat_odbc:get_item(Nidx, ItemId). + node_flat_sql:get_item(Nidx, ItemId). get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> - node_flat_odbc:get_item(Nidx, ItemId, JID, AccessModel, + node_flat_sql:get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId). set_item(Item) -> - node_flat_odbc:set_item(Item). + node_flat_sql:set_item(Item). get_item_name(Host, Node, Id) -> - node_flat_odbc:get_item_name(Host, Node, Id). + node_flat_sql:get_item_name(Host, Node, Id). node_to_path(Node) -> - node_flat_odbc:node_to_path(Node). + node_flat_sql:node_to_path(Node). path_to_node(Path) -> - node_flat_odbc:path_to_node(Path). + node_flat_sql:path_to_node(Path). %%% %%% Internal diff --git a/src/node_private.erl b/src/node_private.erl index de80d870f..79dc5ed81 100644 --- a/src/node_private.erl +++ b/src/node_private.erl @@ -65,7 +65,8 @@ options() -> {max_payload_size, ?MAX_PAYLOAD_SIZE}, {send_last_published_item, never}, {deliver_notifications, false}, - {presence_based_delivery, false}]. + {presence_based_delivery, false}, + {itemreply, none}]. features() -> [<<"create-nodes">>, diff --git a/src/node_public.erl b/src/node_public.erl index df4f1c080..cdb8abca3 100644 --- a/src/node_public.erl +++ b/src/node_public.erl @@ -65,7 +65,8 @@ options() -> {max_payload_size, ?MAX_PAYLOAD_SIZE}, {send_last_published_item, never}, {deliver_notifications, true}, - {presence_based_delivery, false}]. + {presence_based_delivery, false}, + {itemreply, none}]. features() -> [<<"create-nodes">>, diff --git a/src/nodetree_dag.erl b/src/nodetree_dag.erl index 8ac56b27d..a105db832 100644 --- a/src/nodetree_dag.erl +++ b/src/nodetree_dag.erl @@ -69,13 +69,13 @@ create_node(Key, Node, Type, Owner, Options, Parents) -> Other -> Other end; _ -> - {error, ?ERR_CONFLICT} + {error, ?ERRT_CONFLICT(?MYLANG, <<"Node already exists">>)} end. delete_node(Key, Node) -> case find_node(Key, Node) of false -> - {error, ?ERR_ITEM_NOT_FOUND}; + {error, ?ERRT_ITEM_NOT_FOUND(?MYLANG, <<"Node not found">>)}; Record -> lists:foreach(fun (#pubsub_node{options = Opts} = Child) -> NewOpts = remove_config_parent(Node, Opts), @@ -99,7 +99,7 @@ get_node(Host, Node, _From) -> get_node(Host, Node) -> case find_node(Host, Node) of - false -> {error, ?ERR_ITEM_NOT_FOUND}; + false -> {error, ?ERRT_ITEM_NOT_FOUND(?MYLANG, <<"Node not found">>)}; Record -> Record end. @@ -115,7 +115,7 @@ get_nodes(Key) -> get_parentnodes(Host, Node, _From) -> case find_node(Host, Node) of false -> - {error, ?ERR_ITEM_NOT_FOUND}; + {error, ?ERRT_ITEM_NOT_FOUND(?MYLANG, <<"Node not found">>)}; #pubsub_node{parents = Parents} -> Q = qlc:q([N || #pubsub_node{nodeid = {NHost, NNode}} = N @@ -139,7 +139,7 @@ get_subnodes(Host, <<>>) -> get_subnodes_helper(Host, <<>>); get_subnodes(Host, Node) -> case find_node(Host, Node) of - false -> {error, ?ERR_ITEM_NOT_FOUND}; + false -> {error, ?ERRT_ITEM_NOT_FOUND(?MYLANG, <<"Node not found">>)}; _ -> get_subnodes_helper(Host, Node) end. @@ -238,7 +238,7 @@ validate_parentage(Key, Owners, [<<>> | T]) -> validate_parentage(Key, Owners, [ParentID | T]) -> case find_node(Key, ParentID) of false -> - {error, ?ERR_ITEM_NOT_FOUND}; + {error, ?ERRT_ITEM_NOT_FOUND(?MYLANG, <<"Node not found">>)}; #pubsub_node{owners = POwners, options = POptions} -> NodeType = find_opt(node_type, ?DEFAULT_NODETYPE, POptions), MutualOwners = [O || O <- Owners, PO <- POwners, O == PO], diff --git a/src/nodetree_tree.erl b/src/nodetree_tree.erl index e3f7e251e..69b50ff9f 100644 --- a/src/nodetree_tree.erl +++ b/src/nodetree_tree.erl @@ -74,15 +74,15 @@ get_node(Host, Node, _From) -> get_node(Host, Node). get_node(Host, Node) -> - case catch mnesia:read({pubsub_node, {Host, Node}}) of + case mnesia:read({pubsub_node, {Host, Node}}) of [Record] when is_record(Record, pubsub_node) -> Record; - _ -> {error, ?ERR_ITEM_NOT_FOUND} + _ -> {error, ?ERRT_ITEM_NOT_FOUND(?MYLANG, <<"Node not found">>)} end. get_node(Nidx) -> - case catch mnesia:index_read(pubsub_node, Nidx, #pubsub_node.id) of + case mnesia:index_read(pubsub_node, Nidx, #pubsub_node.id) of [Record] when is_record(Record, pubsub_node) -> Record; - _ -> {error, ?ERR_ITEM_NOT_FOUND} + _ -> {error, ?ERRT_ITEM_NOT_FOUND(?MYLANG, <<"Node not found">>)} end. get_nodes(Host, _From) -> @@ -147,7 +147,7 @@ get_subnodes_tree(Host, Node) -> create_node(Host, Node, Type, Owner, Options, Parents) -> BJID = jid:tolower(jid:remove_resource(Owner)), - case catch mnesia:read({pubsub_node, {Host, Node}}) of + case mnesia:read({pubsub_node, {Host, Node}}) of [] -> ParentExists = case Host of {_U, _S, _R} -> @@ -160,7 +160,7 @@ create_node(Host, Node, Type, Owner, Options, Parents) -> true; [Parent | _] -> case catch mnesia:read({pubsub_node, {Host, Parent}}) of - [#pubsub_node{owners = [{[], Host, []}]}] -> + [#pubsub_node{owners = [{<<>>, Host, <<>>}]}] -> true; [#pubsub_node{owners = Owners}] -> lists:member(BJID, Owners); @@ -183,7 +183,7 @@ create_node(Host, Node, Type, Owner, Options, Parents) -> {error, ?ERR_FORBIDDEN} end; _ -> - {error, ?ERR_CONFLICT} + {error, ?ERRT_CONFLICT(?MYLANG, <<"Node already exists">>)} end. delete_node(Host, Node) -> diff --git a/src/nodetree_tree_odbc.erl b/src/nodetree_tree_sql.erl index ef1c20b6b..b56543395 100644 --- a/src/nodetree_tree_odbc.erl +++ b/src/nodetree_tree_sql.erl @@ -1,5 +1,5 @@ %%%---------------------------------------------------------------------- -%%% File : nodetree_tree_odbc.erl +%%% File : nodetree_tree_sql.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> @@ -33,7 +33,7 @@ %%% useable and useful as is. Please, send us comments, feedback and %%% improvements.</p> --module(nodetree_tree_odbc). +-module(nodetree_tree_sql). -behaviour(gen_pubsub_nodetree). -author('christophe.romain@process-one.net'). @@ -55,7 +55,7 @@ terminate(_Host, _ServerHost) -> ok. options() -> - [{odbc, true} | nodetree_tree:options()]. + [{sql, true} | nodetree_tree:options()]. set_node(Record) when is_record(Record, pubsub_node) -> {Host, Node} = Record#pubsub_node.nodeid, @@ -64,16 +64,16 @@ set_node(Record) when is_record(Record, pubsub_node) -> [First | _] -> First end, Type = Record#pubsub_node.type, - H = node_flat_odbc:encode_host(Host), - N = ejabberd_odbc:escape(Node), - P = ejabberd_odbc:escape(Parent), + H = node_flat_sql:encode_host(Host), + N = ejabberd_sql:escape(Node), + P = ejabberd_sql:escape(Parent), Nidx = case nodeidx(Host, Node) of {result, OldNidx} -> catch - ejabberd_odbc:sql_query_t([<<"delete from pubsub_node_option where " + ejabberd_sql:sql_query_t([<<"delete from pubsub_node_option where " "nodeid='">>, OldNidx, <<"';">>]), catch - ejabberd_odbc:sql_query_t([<<"update pubsub_node set host='">>, + ejabberd_sql:sql_query_t([<<"update pubsub_node set host='">>, H, <<"' node='">>, N, <<"' parent='">>, P, <<"' type='">>, Type, @@ -82,7 +82,7 @@ set_node(Record) when is_record(Record, pubsub_node) -> OldNidx; _ -> catch - ejabberd_odbc:sql_query_t([<<"insert into pubsub_node(host, node, " + ejabberd_sql:sql_query_t([<<"insert into pubsub_node(host, node, " "parent, type) values('">>, H, <<"', '">>, N, <<"', '">>, P, <<"', '">>, Type, <<"');">>]), @@ -93,15 +93,16 @@ set_node(Record) when is_record(Record, pubsub_node) -> end, case Nidx of none -> - {error, ?ERR_INTERNAL_SERVER_ERROR}; + Txt = <<"Node index not found">>, + {error, ?ERRT_INTERNAL_SERVER_ERROR(?MYLANG, Txt)}; _ -> lists:foreach(fun ({Key, Value}) -> SKey = iolist_to_binary(atom_to_list(Key)), - SValue = ejabberd_odbc:escape( + SValue = ejabberd_sql:escape( list_to_binary( lists:flatten(io_lib:fwrite("~p", [Value])))), catch - ejabberd_odbc:sql_query_t([<<"insert into pubsub_node_option(nodeid, " + ejabberd_sql:sql_query_t([<<"insert into pubsub_node_option(nodeid, " "name, val) values('">>, Nidx, <<"', '">>, SKey, <<"', '">>, SValue, <<"');">>]) @@ -114,10 +115,10 @@ get_node(Host, Node, _From) -> get_node(Host, Node). get_node(Host, Node) -> - H = node_flat_odbc:encode_host(Host), - N = ejabberd_odbc:escape(Node), + H = node_flat_sql:encode_host(Host), + N = ejabberd_sql:escape(Node), case catch - ejabberd_odbc:sql_query_t([<<"select node, parent, type, nodeid from " + ejabberd_sql:sql_query_t([<<"select node, parent, type, nodeid from " "pubsub_node where host='">>, H, <<"' and node='">>, N, <<"';">>]) of @@ -125,14 +126,14 @@ get_node(Host, Node) -> [<<"node">>, <<"parent">>, <<"type">>, <<"nodeid">>], [RItem]} -> raw_to_node(Host, RItem); {'EXIT', _Reason} -> - {error, ?ERR_INTERNAL_SERVER_ERROR}; + {error, ?ERRT_INTERNAL_SERVER_ERROR(?MYLANG, <<"Database failure">>)}; _ -> - {error, ?ERR_ITEM_NOT_FOUND} + {error, ?ERRT_ITEM_NOT_FOUND(?MYLANG, <<"Node not found">>)} end. get_node(Nidx) -> case catch - ejabberd_odbc:sql_query_t([<<"select host, node, parent, type from " + ejabberd_sql:sql_query_t([<<"select host, node, parent, type from " "pubsub_node where nodeid='">>, Nidx, <<"';">>]) of @@ -140,18 +141,18 @@ get_node(Nidx) -> [<<"host">>, <<"node">>, <<"parent">>, <<"type">>], [[Host, Node, Parent, Type]]} -> raw_to_node(Host, [Node, Parent, Type, Nidx]); {'EXIT', _Reason} -> - {error, ?ERR_INTERNAL_SERVER_ERROR}; + {error, ?ERRT_INTERNAL_SERVER_ERROR(?MYLANG, <<"Database failure">>)}; _ -> - {error, ?ERR_ITEM_NOT_FOUND} + {error, ?ERRT_ITEM_NOT_FOUND(?MYLANG, <<"Node not found">>)} end. get_nodes(Host, _From) -> get_nodes(Host). get_nodes(Host) -> - H = node_flat_odbc:encode_host(Host), + H = node_flat_sql:encode_host(Host), case catch - ejabberd_odbc:sql_query_t([<<"select node, parent, type, nodeid from " + ejabberd_sql:sql_query_t([<<"select node, parent, type, nodeid from " "pubsub_node where host='">>, H, <<"';">>]) of {selected, @@ -176,10 +177,10 @@ get_subnodes(Host, Node, _From) -> get_subnodes(Host, Node). get_subnodes(Host, Node) -> - H = node_flat_odbc:encode_host(Host), - N = ejabberd_odbc:escape(Node), + H = node_flat_sql:encode_host(Host), + N = ejabberd_sql:escape(Node), case catch - ejabberd_odbc:sql_query_t([<<"select node, parent, type, nodeid from " + ejabberd_sql:sql_query_t([<<"select node, parent, type, nodeid from " "pubsub_node where host='">>, H, <<"' and parent='">>, N, <<"';">>]) of @@ -194,10 +195,10 @@ get_subnodes_tree(Host, Node, _From) -> get_subnodes_tree(Host, Node). get_subnodes_tree(Host, Node) -> - H = node_flat_odbc:encode_host(Host), - N = ejabberd_odbc:escape(Node), + H = node_flat_sql:encode_host(Host), + N = ejabberd_sql:escape(Node), case catch - ejabberd_odbc:sql_query_t([<<"select node, parent, type, nodeid from " + ejabberd_sql:sql_query_t([<<"select node, parent, type, nodeid from " "pubsub_node where host='">>, H, <<"' and node like '">>, N, <<"%';">>]) of @@ -211,7 +212,7 @@ get_subnodes_tree(Host, Node) -> create_node(Host, Node, Type, Owner, Options, Parents) -> BJID = jid:tolower(jid:remove_resource(Owner)), case nodeidx(Host, Node) of - {error, ?ERR_ITEM_NOT_FOUND} -> + {error, not_found} -> ParentExists = case Host of {_U, _S, _R} -> %% This is special case for PEP handling @@ -248,23 +249,23 @@ create_node(Host, Node, Type, Owner, Options, Parents) -> {error, ?ERR_FORBIDDEN} end; {result, _} -> - {error, ?ERR_CONFLICT}; - Error -> - Error + {error, ?ERRT_CONFLICT(?MYLANG, <<"Node already exists">>)}; + {error, db_fail} -> + {error, ?ERRT_INTERNAL_SERVER_ERROR(?MYLANG, <<"Database failure">>)} end. delete_node(Host, Node) -> - H = node_flat_odbc:encode_host(Host), - N = ejabberd_odbc:escape(Node), + H = node_flat_sql:encode_host(Host), + N = ejabberd_sql:escape(Node), Removed = get_subnodes_tree(Host, Node), - catch ejabberd_odbc:sql_query_t([<<"delete from pubsub_node where host='">>, + catch ejabberd_sql:sql_query_t([<<"delete from pubsub_node where host='">>, H, <<"' and node like '">>, N, <<"%';">>]), Removed. %% helpers raw_to_node(Host, [Node, Parent, Type, Nidx]) -> Options = case catch - ejabberd_odbc:sql_query_t([<<"select name,val from pubsub_node_option " + ejabberd_sql:sql_query_t([<<"select name,val from pubsub_node_option " "where nodeid='">>, Nidx, <<"';">>]) of {selected, [<<"name">>, <<"val">>], ROptions} -> @@ -275,7 +276,7 @@ raw_to_node(Host, [Node, Parent, Type, Nidx]) -> {RKey, RValue} end, ROptions), - Module = jlib:binary_to_atom(<<"node_", Type/binary, "_odbc">>), + Module = jlib:binary_to_atom(<<"node_", Type/binary, "_sql">>), StdOpts = Module:options(), lists:foldl(fun ({Key, Value}, Acc) -> lists:keyreplace(Key, 1, Acc, {Key, Value}) @@ -293,21 +294,21 @@ raw_to_node(Host, [Node, Parent, Type, Nidx]) -> id = Nidx, type = Type, options = Options}. nodeidx(Host, Node) -> - H = node_flat_odbc:encode_host(Host), - N = ejabberd_odbc:escape(Node), + H = node_flat_sql:encode_host(Host), + N = ejabberd_sql:escape(Node), case catch - ejabberd_odbc:sql_query_t([<<"select nodeid from pubsub_node where " + ejabberd_sql:sql_query_t([<<"select nodeid from pubsub_node where " "host='">>, H, <<"' and node='">>, N, <<"';">>]) of {selected, [<<"nodeid">>], [[Nidx]]} -> {result, Nidx}; {'EXIT', _Reason} -> - {error, ?ERR_INTERNAL_SERVER_ERROR}; + {error, db_fail}; _ -> - {error, ?ERR_ITEM_NOT_FOUND} + {error, not_found} end. nodeowners(Nidx) -> - {result, Res} = node_flat_odbc:get_node_affiliations(Nidx), + {result, Res} = node_flat_sql:get_node_affiliations(Nidx), [LJID || {LJID, Aff} <- Res, Aff =:= owner]. diff --git a/src/odbc_queries.erl b/src/odbc_queries.erl deleted file mode 100644 index 2f488a0bd..000000000 --- a/src/odbc_queries.erl +++ /dev/null @@ -1,659 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : odbc_queries.erl -%%% Author : Mickael Remond <mremond@process-one.net> -%%% Purpose : ODBC queries dependind on back-end -%%% Created : 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(odbc_queries). - --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, - 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, - 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, - 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_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, opt_type/1]). - --include("ejabberd.hrl"). --include("logger.hrl"). - -%% Almost a copy of string:join/2. -%% We use this version because string:join/2 is relatively -%% new function (introduced in R12B-0). -join([], _Sep) -> []; -join([H | T], Sep) -> [H, [[Sep, X] || X <- T]]. - -get_db_type() -> generic. - -%% Safe atomic update. -update_t(Table, Fields, Vals, Where) -> - UPairs = lists:zipwith(fun (A, B) -> - <<A/binary, "='", B/binary, "'">> - end, - Fields, Vals), - case ejabberd_odbc:sql_query_t([<<"update ">>, Table, - <<" set ">>, join(UPairs, <<", ">>), - <<" where ">>, Where, <<";">>]) - of - {updated, 1} -> ok; - _ -> - Res = ejabberd_odbc:sql_query_t([<<"insert into ">>, Table, - <<"(">>, join(Fields, <<", ">>), - <<") values ('">>, join(Vals, <<"', '">>), - <<"');">>]), - case Res of - {updated,1} -> ok; - _ -> Res - end - end. - -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} -> ok; - _ -> - Res = ejabberd_odbc:sql_query(LServer, - [<<"insert into ">>, Table, <<"(">>, - join(Fields, <<", ">>), <<") values ('">>, - join(Vals, <<"', '">>), <<"');">>]), - case Res of - {updated,1} -> ok; - _ -> Res - end - end. - -%% F can be either a fun or a list of queries -%% TODO: We should probably move the list of queries transaction -%% wrapper from the ejabberd_odbc module to this one (odbc_queries) -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, <<"'">>]). - -set_last_t(LServer, Username, Seconds, State) -> - update(LServer, <<"last">>, - [<<"username">>, <<"seconds">>, <<"state">>], - [Username, Seconds, State], - [<<"username='">>, Username, <<"'">>]). - -del_last(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"delete from last where username='">>, Username, - <<"'">>]). - -get_password(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"select password from users where username='">>, - Username, <<"';">>]). - -get_password_scram(LServer, Username) -> - 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, - 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, <<"');">>]). - -add_user_scram(LServer, Username, - 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, - <<"';">>]). - -del_user_return_password(_LServer, Username, Pass) -> - 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, - <<"';">>]), - P. - -list_users(LServer) -> - ejabberd_odbc:sql_query(LServer, - [<<"select username from users">>]). - -list_users(LServer, [{from, Start}, {to, End}]) - when is_integer(Start) and is_integer(End) -> - list_users(LServer, - [{limit, End - Start + 1}, {offset, Start - 1}]); -list_users(LServer, - [{prefix, Prefix}, {from, Start}, {to, End}]) - when is_binary(Prefix) and is_integer(Start) and - is_integer(End) -> - list_users(LServer, - [{prefix, Prefix}, {limit, End - Start + 1}, - {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]))]); -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]))]). - -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">>]) - end; - _ -> - ejabberd_odbc:sql_query(LServer, - [<<"select count(*) 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]))]); -users_number(LServer, []) -> - users_number(LServer). - - -add_spool_sql(Username, XML) -> - [<<"insert into spool(username, xml) values ('">>, - Username, <<"', '">>, XML, <<"');">>]. - -add_spool(LServer, Queries) -> - ejabberd_odbc:sql_transaction(LServer, Queries). - -get_and_del_spool_msg_t(LServer, Username) -> - 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, <<"';">>]), - Result - end, - ejabberd_odbc:sql_transaction(LServer, F). - -del_spool_msg(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"delete from spool where username='">>, Username, - <<"';">>]). - -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_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, - <<"';">>]). - -del_roster_sql(Username, SJID) -> - [[<<"delete from rosterusers where " - "username='">>, - Username, <<"' and jid='">>, SJID, <<"';">>], - [<<"delete from rostergroups where " - "username='">>, - Username, <<"' and jid='">>, SJID, <<"';">>]]. - -update_roster(_LServer, Username, 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). - -update_roster_sql(Username, SJID, ItemVals, - ItemGroups) -> - [[<<"delete from rosterusers where " - "username='">>, - Username, <<"' and jid='">>, SJID, <<"';">>], - [<<"insert into rosterusers( " - " username, jid, nick, " - " subscription, ask, askmessage, " - " server, subscribe, type) " - "values ('">>, - join(ItemVals, <<"', '">>), <<"');">>], - [<<"delete from rostergroups where " - "username='">>, - Username, <<"' and jid='">>, SJID, <<"';">>]] - ++ - [[<<"insert into rostergroups( " - " username, jid, grp) values ('">>, - 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, <<"'">>]). - -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_sql(Username, LXMLNS, SData) -> - [[<<"delete from private_storage where username='">>, - Username, <<"' and namespace='">>, LXMLNS, <<"';">>], - [<<"insert into private_storage(username, " - "namespace, data) values ('">>, - 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, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"select namespace, data from private_storage " - "where username='">>, Username, <<"';">>]). - -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, <<"';">>]). - -get_default_privacy_list(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"select name from privacy_default_list " - "where username='">>, - Username, <<"';">>]). - -get_default_privacy_list_t(Username) -> - ejabberd_odbc:sql_query_t([<<"select name from privacy_default_list " - "where username='">>, - Username, <<"';">>]). - -get_privacy_list_names(LServer, Username) -> - ejabberd_odbc:sql_query(LServer, - [<<"select name from privacy_list where " - "username='">>, - Username, <<"';">>]). - -get_privacy_list_names_t(Username) -> - ejabberd_odbc:sql_query_t([<<"select name from privacy_list where " - "username='">>, - Username, <<"';">>]). - -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_t(Username, SName) -> - ejabberd_odbc:sql_query_t([<<"select id from privacy_list where username='">>, - Username, <<"' and name='">>, SName, <<"';">>]). - -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_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;">>]). - -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, <<"';">>]). - -remove_privacy_list(Username, SName) -> - ejabberd_odbc:sql_query_t([<<"delete from privacy_list where username='">>, - Username, <<"' and name='">>, SName, <<"';">>]). - -add_privacy_list(Username, SName) -> - ejabberd_odbc:sql_query_t([<<"insert into privacy_list(username, name) " - "values ('">>, - Username, <<"', '">>, SName, <<"');">>]). - -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, <<"', '">>), - <<"');">>]) - 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, <<"';">>]). - -escape($\000) -> <<"\\0">>; -escape($\n) -> <<"\\n">>; -escape($\t) -> <<"\\t">>; -escape($\b) -> <<"\\b">>; -escape($\r) -> <<"\\r">>; -escape($') -> <<"''">>; -escape($") -> <<"\\\"">>; -escape($\\) -> <<"\\\\">>; -escape(C) -> <<C>>. - -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, <<"'">>]). - -set_roster_version(LUser, Version) -> - update_t(<<"roster_version">>, - [<<"username">>, <<"version">>], [LUser, Version], - [<<"username = '">>, LUser, <<"'">>]). - -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 000000000..204cfec2f --- /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_sql.erl index cfdeda1ef..b910a5e7d 100644 --- a/src/pubsub_db_odbc.erl +++ b/src/pubsub_db_sql.erl @@ -1,5 +1,5 @@ %%%---------------------------------------------------------------------- -%%% File : pubsub_db_odbc.erl +%%% File : pubsub_db_sql.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> @@ -23,7 +23,7 @@ %%% %%%---------------------------------------------------------------------- --module(pubsub_db_odbc). +-module(pubsub_db_sql). -author("pablo.polvorin@process-one.net"). @@ -37,16 +37,16 @@ %% -spec read_subscription(SubID :: string()) -> {ok, #pubsub_subscription{}} | notfound. read_subscription(SubID) -> case - ejabberd_odbc:sql_query_t([<<"select opt_name, opt_value from pubsub_subscr" + ejabberd_sql:sql_query_t([<<"select opt_name, opt_value from pubsub_subscr" "iption_opt where subid = '">>, - ejabberd_odbc:escape(SubID), <<"'">>]) + ejabberd_sql:escape(SubID), <<"'">>]) of {selected, [<<"opt_name">>, <<"opt_value">>], []} -> notfound; {selected, [<<"opt_name">>, <<"opt_value">>], Options} -> {ok, #pubsub_subscription{subid = SubID, - options = lists:map(fun subscription_opt_from_odbc/1, Options)}} + options = lists:map(fun subscription_opt_from_sql/1, Options)}} end. %% -spec delete_subscription(SubID :: string()) -> ok. @@ -54,19 +54,19 @@ delete_subscription(SubID) -> %% -spec update_subscription(#pubsub_subscription{}) -> ok . %% -spec add_subscription(#pubsub_subscription{}) -> ok. %% -------------- Internal utilities ----------------------- - ejabberd_odbc:sql_query_t([<<"delete from pubsub_subscription_opt " + ejabberd_sql:sql_query_t([<<"delete from pubsub_subscription_opt " "where subid = '">>, - ejabberd_odbc:escape(SubID), <<"'">>]), + ejabberd_sql:escape(SubID), <<"'">>]), ok. update_subscription(#pubsub_subscription{subid = SubId} = Sub) -> delete_subscription(SubId), add_subscription(Sub). add_subscription(#pubsub_subscription{subid = SubId, options = Opts}) -> - EscapedSubId = ejabberd_odbc:escape(SubId), + EscapedSubId = ejabberd_sql:escape(SubId), lists:foreach(fun (Opt) -> - {OdbcOptName, OdbcOptValue} = subscription_opt_to_odbc(Opt), - ejabberd_odbc:sql_query_t([<<"insert into pubsub_subscription_opt(subid, " + {OdbcOptName, OdbcOptValue} = subscription_opt_to_sql(Opt), + ejabberd_sql:sql_query_t([<<"insert into pubsub_subscription_opt(subid, " "opt_name, opt_value)values ('">>, EscapedSubId, <<"','">>, OdbcOptName, <<"','">>, @@ -75,67 +75,67 @@ add_subscription(#pubsub_subscription{subid = SubId, options = Opts}) -> Opts), ok. -subscription_opt_from_odbc({<<"DELIVER">>, Value}) -> - {deliver, odbc_to_boolean(Value)}; -subscription_opt_from_odbc({<<"DIGEST">>, Value}) -> - {digest, odbc_to_boolean(Value)}; -subscription_opt_from_odbc({<<"DIGEST_FREQUENCY">>, Value}) -> - {digest_frequency, odbc_to_integer(Value)}; -subscription_opt_from_odbc({<<"EXPIRE">>, Value}) -> - {expire, odbc_to_timestamp(Value)}; -subscription_opt_from_odbc({<<"INCLUDE_BODY">>, Value}) -> - {include_body, odbc_to_boolean(Value)}; +subscription_opt_from_sql({<<"DELIVER">>, Value}) -> + {deliver, sql_to_boolean(Value)}; +subscription_opt_from_sql({<<"DIGEST">>, Value}) -> + {digest, sql_to_boolean(Value)}; +subscription_opt_from_sql({<<"DIGEST_FREQUENCY">>, Value}) -> + {digest_frequency, sql_to_integer(Value)}; +subscription_opt_from_sql({<<"EXPIRE">>, Value}) -> + {expire, sql_to_timestamp(Value)}; +subscription_opt_from_sql({<<"INCLUDE_BODY">>, Value}) -> + {include_body, sql_to_boolean(Value)}; %%TODO: might be > than 1 show_values value??. %% need to use compact all in only 1 opt. -subscription_opt_from_odbc({<<"SHOW_VALUES">>, Value}) -> +subscription_opt_from_sql({<<"SHOW_VALUES">>, Value}) -> {show_values, Value}; -subscription_opt_from_odbc({<<"SUBSCRIPTION_TYPE">>, Value}) -> +subscription_opt_from_sql({<<"SUBSCRIPTION_TYPE">>, Value}) -> {subscription_type, case Value of <<"items">> -> items; <<"nodes">> -> nodes end}; -subscription_opt_from_odbc({<<"SUBSCRIPTION_DEPTH">>, Value}) -> +subscription_opt_from_sql({<<"SUBSCRIPTION_DEPTH">>, Value}) -> {subscription_depth, case Value of <<"all">> -> all; - N -> odbc_to_integer(N) + N -> sql_to_integer(N) end}. -subscription_opt_to_odbc({deliver, Bool}) -> - {<<"DELIVER">>, boolean_to_odbc(Bool)}; -subscription_opt_to_odbc({digest, Bool}) -> - {<<"DIGEST">>, boolean_to_odbc(Bool)}; -subscription_opt_to_odbc({digest_frequency, Int}) -> - {<<"DIGEST_FREQUENCY">>, integer_to_odbc(Int)}; -subscription_opt_to_odbc({expire, Timestamp}) -> - {<<"EXPIRE">>, timestamp_to_odbc(Timestamp)}; -subscription_opt_to_odbc({include_body, Bool}) -> - {<<"INCLUDE_BODY">>, boolean_to_odbc(Bool)}; -subscription_opt_to_odbc({show_values, Values}) -> +subscription_opt_to_sql({deliver, Bool}) -> + {<<"DELIVER">>, boolean_to_sql(Bool)}; +subscription_opt_to_sql({digest, Bool}) -> + {<<"DIGEST">>, boolean_to_sql(Bool)}; +subscription_opt_to_sql({digest_frequency, Int}) -> + {<<"DIGEST_FREQUENCY">>, integer_to_sql(Int)}; +subscription_opt_to_sql({expire, Timestamp}) -> + {<<"EXPIRE">>, timestamp_to_sql(Timestamp)}; +subscription_opt_to_sql({include_body, Bool}) -> + {<<"INCLUDE_BODY">>, boolean_to_sql(Bool)}; +subscription_opt_to_sql({show_values, Values}) -> {<<"SHOW_VALUES">>, Values}; -subscription_opt_to_odbc({subscription_type, Type}) -> +subscription_opt_to_sql({subscription_type, Type}) -> {<<"SUBSCRIPTION_TYPE">>, case Type of items -> <<"items">>; nodes -> <<"nodes">> end}; -subscription_opt_to_odbc({subscription_depth, Depth}) -> +subscription_opt_to_sql({subscription_depth, Depth}) -> {<<"SUBSCRIPTION_DEPTH">>, case Depth of all -> <<"all">>; - N -> integer_to_odbc(N) + N -> integer_to_sql(N) end}. -integer_to_odbc(N) -> iolist_to_binary(integer_to_list(N)). +integer_to_sql(N) -> iolist_to_binary(integer_to_list(N)). -boolean_to_odbc(true) -> <<"1">>; -boolean_to_odbc(false) -> <<"0">>. +boolean_to_sql(true) -> <<"1">>; +boolean_to_sql(false) -> <<"0">>. -timestamp_to_odbc(T) -> jlib:now_to_utc_string(T). +timestamp_to_sql(T) -> jlib:now_to_utc_string(T). -odbc_to_integer(N) -> jlib:binary_to_integer(N). +sql_to_integer(N) -> jlib:binary_to_integer(N). -odbc_to_boolean(B) -> B == <<"1">>. +sql_to_boolean(B) -> B == <<"1">>. -odbc_to_timestamp(T) -> jlib:datetime_string_to_timestamp(T). +sql_to_timestamp(T) -> jlib:datetime_string_to_timestamp(T). diff --git a/src/pubsub_subscription.erl b/src/pubsub_subscription.erl index 9918a2c35..f990f6e38 100644 --- a/src/pubsub_subscription.erl +++ b/src/pubsub_subscription.erl @@ -126,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) -> @@ -237,31 +237,40 @@ var_xfield(?PUBSUB_SUBSCRIPTION_TYPE) -> subscription_type; var_xfield(?PUBSUB_SUBSCRIPTION_DEPTH) -> subscription_depth; var_xfield(_) -> {error, badarg}. -val_xfield(deliver, [Val]) -> xopt_to_bool(Val); -val_xfield(digest, [Val]) -> xopt_to_bool(Val); -val_xfield(digest_frequency, [Val]) -> +val_xfield(deliver = Opt, [Val]) -> xopt_to_bool(Opt, Val); +val_xfield(digest = Opt, [Val]) -> xopt_to_bool(Opt, Val); +val_xfield(digest_frequency = Opt, [Val]) -> case catch jlib:binary_to_integer(Val) of N when is_integer(N) -> N; - _ -> {error, ?ERR_NOT_ACCEPTABLE} + _ -> + Txt = <<"Value of '~s' should be integer">>, + ErrTxt = iolist_to_binary(io_lib:format(Txt, [Opt])), + {error, ?ERRT_NOT_ACCEPTABLE(?MYLANG, ErrTxt)} end; val_xfield(expire, [Val]) -> jlib:datetime_string_to_timestamp(Val); -val_xfield(include_body, [Val]) -> xopt_to_bool(Val); +val_xfield(include_body = Opt, [Val]) -> xopt_to_bool(Opt, Val); val_xfield(show_values, Vals) -> Vals; val_xfield(subscription_type, [<<"items">>]) -> items; val_xfield(subscription_type, [<<"nodes">>]) -> nodes; val_xfield(subscription_depth, [<<"all">>]) -> all; -val_xfield(subscription_depth, [Depth]) -> +val_xfield(subscription_depth = Opt, [Depth]) -> case catch jlib:binary_to_integer(Depth) of N when is_integer(N) -> N; - _ -> {error, ?ERR_NOT_ACCEPTABLE} + _ -> + Txt = <<"Value of '~s' should be integer">>, + ErrTxt = iolist_to_binary(io_lib:format(Txt, [Opt])), + {error, ?ERRT_NOT_ACCEPTABLE(?MYLANG, ErrTxt)} end. %% Convert XForm booleans to Erlang booleans. -xopt_to_bool(<<"0">>) -> false; -xopt_to_bool(<<"1">>) -> true; -xopt_to_bool(<<"false">>) -> false; -xopt_to_bool(<<"true">>) -> true; -xopt_to_bool(_) -> {error, ?ERR_NOT_ACCEPTABLE}. +xopt_to_bool(_, <<"0">>) -> false; +xopt_to_bool(_, <<"1">>) -> true; +xopt_to_bool(_, <<"false">>) -> false; +xopt_to_bool(_, <<"true">>) -> true; +xopt_to_bool(Option, _) -> + Txt = <<"Value of '~s' should be boolean">>, + ErrTxt = iolist_to_binary(io_lib:format(Txt, [Option])), + {error, ?ERRT_NOT_ACCEPTABLE(?MYLANG, ErrTxt)}. -spec(get_option_xfield/3 :: ( diff --git a/src/pubsub_subscription_odbc.erl b/src/pubsub_subscription_sql.erl index 6791c4ac7..1f82aa008 100644 --- a/src/pubsub_subscription_odbc.erl +++ b/src/pubsub_subscription_sql.erl @@ -1,5 +1,5 @@ %%%---------------------------------------------------------------------- -%%% File : pubsub_subscription_odbc.erl +%%% File : pubsub_subscription_sql.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> @@ -24,7 +24,7 @@ %%% %%%---------------------------------------------------------------------- --module(pubsub_subscription_odbc). +-module(pubsub_subscription_sql). -author("pablo.polvorin@process-one.net"). %% API @@ -66,7 +66,7 @@ -define(SUBSCRIPTION_DEPTH_VALUE_ONE_LABEL, <<"Receive notification from direct child nodes only">>). -define(SUBSCRIPTION_DEPTH_VALUE_ALL_LABEL, <<"Receive notification from all descendent nodes">>). --define(DB_MOD, pubsub_db_odbc). +-define(DB_MOD, pubsub_db_sql). %%==================================================================== %% API %%==================================================================== @@ -151,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) -> @@ -200,31 +200,40 @@ var_xfield(?PUBSUB_SUBSCRIPTION_TYPE) -> subscription_type; var_xfield(?PUBSUB_SUBSCRIPTION_DEPTH) -> subscription_depth; var_xfield(_) -> {error, badarg}. -val_xfield(deliver, [Val]) -> xopt_to_bool(Val); -val_xfield(digest, [Val]) -> xopt_to_bool(Val); -val_xfield(digest_frequency, [Val]) -> +val_xfield(deliver = Opt, [Val]) -> xopt_to_bool(Opt, Val); +val_xfield(digest = Opt, [Val]) -> xopt_to_bool(Opt, Val); +val_xfield(digest_frequency = Opt, [Val]) -> case catch jlib:binary_to_integer(Val) of N when is_integer(N) -> N; - _ -> {error, ?ERR_NOT_ACCEPTABLE} + _ -> + Txt = <<"Value of '~s' should be integer">>, + ErrTxt = iolist_to_binary(io_lib:format(Txt, [Opt])), + {error, ?ERRT_NOT_ACCEPTABLE(?MYLANG, ErrTxt)} end; val_xfield(expire, [Val]) -> jlib:datetime_string_to_timestamp(Val); -val_xfield(include_body, [Val]) -> xopt_to_bool(Val); +val_xfield(include_body = Opt, [Val]) -> xopt_to_bool(Opt, Val); val_xfield(show_values, Vals) -> Vals; val_xfield(subscription_type, [<<"items">>]) -> items; val_xfield(subscription_type, [<<"nodes">>]) -> nodes; val_xfield(subscription_depth, [<<"all">>]) -> all; -val_xfield(subscription_depth, [Depth]) -> +val_xfield(subscription_depth = Opt, [Depth]) -> case catch jlib:binary_to_integer(Depth) of N when is_integer(N) -> N; - _ -> {error, ?ERR_NOT_ACCEPTABLE} + _ -> + Txt = <<"Value of '~s' should be integer">>, + ErrTxt = iolist_to_binary(io_lib:format(Txt, [Opt])), + {error, ?ERRT_NOT_ACCEPTABLE(?MYLANG, ErrTxt)} end. %% Convert XForm booleans to Erlang booleans. -xopt_to_bool(<<"0">>) -> false; -xopt_to_bool(<<"1">>) -> true; -xopt_to_bool(<<"false">>) -> false; -xopt_to_bool(<<"true">>) -> true; -xopt_to_bool(_) -> {error, ?ERR_NOT_ACCEPTABLE}. +xopt_to_bool(_, <<"0">>) -> false; +xopt_to_bool(_, <<"1">>) -> true; +xopt_to_bool(_, <<"false">>) -> false; +xopt_to_bool(_, <<"true">>) -> true; +xopt_to_bool(Option, _) -> + Txt = <<"Value of '~s' should be boolean">>, + ErrTxt = iolist_to_binary(io_lib:format(Txt, [Option])), + {error, ?ERRT_NOT_ACCEPTABLE(?MYLANG, ErrTxt)}. %% Return a field for an XForm for Key, with data filled in, if %% applicable, from Options. diff --git a/src/sql_queries.erl b/src/sql_queries.erl new file mode 100644 index 000000000..98530a4cf --- /dev/null +++ b/src/sql_queries.erl @@ -0,0 +1,644 @@ +%%%---------------------------------------------------------------------- +%%% File : sql_queries.erl +%%% Author : Mickael Remond <mremond@process-one.net> +%%% Purpose : ODBC queries dependind on back-end +%%% Created : 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(sql_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, + 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/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, + 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_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/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 +%% new function (introduced in R12B-0). +join([], _Sep) -> []; +join([H | T], Sep) -> [H, [[Sep, X] || X <- T]]. + +get_db_type() -> generic. + +%% Safe atomic update. +update_t(Table, Fields, Vals, Where) -> + UPairs = lists:zipwith(fun (A, B) -> + <<A/binary, "='", B/binary, "'">> + end, + Fields, Vals), + case ejabberd_sql:sql_query_t([<<"update ">>, Table, + <<" set ">>, join(UPairs, <<", ">>), + <<" where ">>, Where, <<";">>]) + of + {updated, 1} -> ok; + _ -> + Res = ejabberd_sql:sql_query_t([<<"insert into ">>, Table, + <<"(">>, join(Fields, <<", ">>), + <<") values ('">>, join(Vals, <<"', '">>), + <<"');">>]), + case Res of + {updated,1} -> ok; + _ -> Res + end + end. + +update(LServer, Table, Fields, Vals, Where) -> + UPairs = lists:zipwith(fun (A, B) -> + <<A/binary, "='", B/binary, "'">> + end, + Fields, Vals), + case ejabberd_sql:sql_query(LServer, + [<<"update ">>, Table, <<" set ">>, + join(UPairs, <<", ">>), <<" where ">>, Where, + <<";">>]) + of + {updated, 1} -> ok; + _ -> + Res = ejabberd_sql:sql_query(LServer, + [<<"insert into ">>, Table, <<"(">>, + join(Fields, <<", ">>), <<") values ('">>, + join(Vals, <<"', '">>), <<"');">>]), + case Res of + {updated,1} -> ok; + _ -> Res + end + end. + +%% F can be either a fun or a list of queries +%% TODO: We should probably move the list of queries transaction +%% wrapper from the ejabberd_sql module to this one (sql_queries) +sql_transaction(LServer, F) -> + ejabberd_sql:sql_transaction(LServer, F). + +get_last(LServer, LUser) -> + ejabberd_sql:sql_query( + LServer, + ?SQL("select @(seconds)d, @(state)s from last" + " where username=%(LUser)s")). + +set_last_t(LServer, LUser, TimeStamp, Status) -> + ?SQL_UPSERT(LServer, "last", + ["!username=%(LUser)s", + "seconds=%(TimeStamp)d", + "state=%(Status)s"]). + +del_last(LServer, LUser) -> + ejabberd_sql:sql_query( + LServer, + ?SQL("delete from last where username=%(LUser)s")). + +get_password(LServer, LUser) -> + ejabberd_sql:sql_query( + LServer, + ?SQL("select @(password)s from users where username=%(LUser)s")). + +get_password_scram(LServer, LUser) -> + ejabberd_sql:sql_query( + LServer, + ?SQL("select @(password)s, @(serverkey)s, @(salt)s, @(iterationcount)d" + " from users" + " where username=%(LUser)s")). + +set_password_t(LServer, LUser, Password) -> + ejabberd_sql: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_sql: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_sql:sql_query( + LServer, + ?SQL("insert into users(username, password) " + "values (%(LUser)s, %(Password)s)")). + +add_user_scram(LServer, LUser, + StoredKey, ServerKey, Salt, IterationCount) -> + ejabberd_sql: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_sql:sql_query( + LServer, + ?SQL("delete from users where username=%(LUser)s")). + +del_user_return_password(_LServer, LUser, Password) -> + P = + ejabberd_sql:sql_query_t( + ?SQL("select @(password)s from users where username=%(LUser)s")), + ejabberd_sql:sql_query_t( + ?SQL("delete from users" + " where username=%(LUser)s and password=%(Password)s")), + P. + +list_users(LServer) -> + ejabberd_sql:sql_query( + LServer, + ?SQL("select @(username)s from users")). + +list_users(LServer, [{from, Start}, {to, End}]) + when is_integer(Start) and is_integer(End) -> + list_users(LServer, + [{limit, End - Start + 1}, {offset, Start - 1}]); +list_users(LServer, + [{prefix, Prefix}, {from, Start}, {to, End}]) + when is_binary(Prefix) and is_integer(Start) and + is_integer(End) -> + list_users(LServer, + [{prefix, Prefix}, {limit, End - Start + 1}, + {offset, Start - 1}]); +list_users(LServer, [{limit, Limit}, {offset, Offset}]) + when is_integer(Limit) and is_integer(Offset) -> + ejabberd_sql: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) -> + SPrefix = ejabberd_sql:escape_like_arg(Prefix), + SPrefix2 = <<SPrefix/binary, $%>>, + ejabberd_sql: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) -> + ejabberd_sql: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_sql:sql_query_t( + ?SQL("select @(reltuples :: bigint)d from pg_class" + " where oid = 'users'::regclass::oid")); + _ -> + ejabberd_sql:sql_query_t( + ?SQL("select @(count(*))d from users")) + end; + (_Type, _) -> + ejabberd_sql:sql_query_t( + ?SQL("select @(count(*))d from users")) + end). + +users_number(LServer, [{prefix, Prefix}]) + when is_binary(Prefix) -> + SPrefix = ejabberd_sql:escape_like_arg(Prefix), + SPrefix2 = <<SPrefix/binary, $%>>, + ejabberd_sql:sql_query( + LServer, + ?SQL("select @(count(*))d from users " + "where username like %(SPrefix2)s")); +users_number(LServer, []) -> + users_number(LServer). + +add_spool_sql(Username, XML) -> + [<<"insert into spool(username, xml) values ('">>, + Username, <<"', '">>, XML, <<"');">>]. + +add_spool(LServer, Queries) -> + ejabberd_sql:sql_transaction(LServer, Queries). + +get_and_del_spool_msg_t(LServer, LUser) -> + F = fun () -> + Result = + ejabberd_sql:sql_query_t( + ?SQL("select @(username)s, @(xml)s from spool where " + "username=%(LUser)s order by seq;")), + ejabberd_sql:sql_query_t( + ?SQL("delete from spool where username=%(LUser)s;")), + Result + end, + ejabberd_sql:sql_transaction(LServer, F). + +del_spool_msg(LServer, LUser) -> + ejabberd_sql:sql_query( + LServer, + ?SQL("delete from spool where username=%(LUser)s")). + +get_roster(LServer, LUser) -> + ejabberd_sql: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, LUser) -> + ejabberd_sql:sql_query( + LServer, + ?SQL("select @(jid)s, @(grp)s from rostergroups where " + "username=%(LUser)s")). + +get_roster_groups(_LServer, LUser, SJID) -> + ejabberd_sql:sql_query_t( + ?SQL("select @(grp)s from rostergroups" + " where username=%(LUser)s and jid=%(SJID)s")). + +del_user_roster_t(LServer, LUser) -> + ejabberd_sql:sql_transaction( + LServer, + fun () -> + ejabberd_sql:sql_query_t( + ?SQL("delete from rosterusers where username=%(LUser)s")), + ejabberd_sql:sql_query_t( + ?SQL("delete from rostergroups where username=%(LUser)s")) + end). + +get_roster_by_jid(_LServer, LUser, SJID) -> + ejabberd_sql: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_sql:sql_query( + LServer, + ?SQL("select @(grp)s from rostergroups" + " where username=%(LUser)s and jid=%(SJID)s")). + +del_roster(_LServer, LUser, SJID) -> + ejabberd_sql:sql_query_t( + ?SQL("delete from rosterusers" + " where username=%(LUser)s and jid=%(SJID)s")), + ejabberd_sql:sql_query_t( + ?SQL("delete from rostergroups" + " where username=%(LUser)s and jid=%(SJID)s")). + +del_roster_sql(Username, SJID) -> + [[<<"delete from rosterusers where " + "username='">>, + Username, <<"' and jid='">>, SJID, <<"';">>], + [<<"delete from rostergroups where " + "username='">>, + Username, <<"' and jid='">>, SJID, <<"';">>]]. + +update_roster(_LServer, LUser, SJID, ItemVals, + ItemGroups) -> + roster_subscribe(ItemVals), + ejabberd_sql:sql_query_t( + ?SQL("delete from rostergroups" + " where username=%(LUser)s and jid=%(SJID)s")), + lists:foreach( + fun(ItemGroup) -> + ejabberd_sql: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) -> + [[<<"delete from rosterusers where " + "username='">>, + Username, <<"' and jid='">>, SJID, <<"';">>], + [<<"insert into rosterusers( " + " username, jid, nick, " + " subscription, ask, askmessage, " + " server, subscribe, type) " + "values ('">>, + join(ItemVals, <<"', '">>), <<"');">>], + [<<"delete from rostergroups where " + "username='">>, + Username, <<"' and jid='">>, SJID, <<"';">>]] + ++ + [[<<"insert into rostergroups( " + " username, jid, grp) values ('">>, + join(ItemGroup, <<"', '">>), <<"');">>] + || ItemGroup <- ItemGroups]. + +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_sql:sql_query( + LServer, + ?SQL("select @(subscription)s from rosterusers " + "where username=%(LUser)s and jid=%(SJID)s")). + +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='">>, + Username, <<"' and namespace='">>, LXMLNS, <<"';">>], + [<<"insert into private_storage(username, " + "namespace, data) values ('">>, + Username, <<"', '">>, LXMLNS, <<"', '">>, SData, + <<"');">>]]. + +get_private_data(LServer, LUser, XMLNS) -> + ejabberd_sql:sql_query( + LServer, + ?SQL("select @(data)s from private_storage" + " where username=%(LUser)s and namespace=%(XMLNS)s")). + +get_private_data(LServer, LUser) -> + ejabberd_sql:sql_query( + LServer, + ?SQL("select @(namespace)s, @(data)s from private_storage" + " where username=%(LUser)s")). + +del_user_private_storage(LServer, LUser) -> + ejabberd_sql: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_sql: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_sql:sql_query( + LServer, + ?SQL("select @(vcard)s from vcard where username=%(LUser)s")). + +get_default_privacy_list(LServer, LUser) -> + ejabberd_sql:sql_query( + LServer, + ?SQL("select @(name)s from privacy_default_list " + "where username=%(LUser)s")). + +get_default_privacy_list_t(LUser) -> + ejabberd_sql:sql_query_t( + ?SQL("select @(name)s from privacy_default_list " + "where username=%(LUser)s")). + +get_privacy_list_names(LServer, LUser) -> + ejabberd_sql:sql_query( + LServer, + ?SQL("select @(name)s from privacy_list" + " where username=%(LUser)s")). + +get_privacy_list_names_t(LUser) -> + ejabberd_sql:sql_query_t( + ?SQL("select @(name)s from privacy_list" + " where username=%(LUser)s")). + +get_privacy_list_id(LServer, LUser, Name) -> + ejabberd_sql:sql_query( + LServer, + ?SQL("select @(id)d from privacy_list" + " where username=%(LUser)s and name=%(Name)s")). + +get_privacy_list_id_t(LUser, Name) -> + ejabberd_sql:sql_query_t( + ?SQL("select @(id)d from privacy_list" + " where username=%(LUser)s and name=%(Name)s")). + +get_privacy_list_data(LServer, LUser, Name) -> + ejabberd_sql: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_sql: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_sql: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_sql: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_sql:sql_query( + LServer, + ?SQL("delete from privacy_default_list" + " where username=%(LUser)s")). + +remove_privacy_list(LUser, Name) -> + ejabberd_sql:sql_query_t( + ?SQL("delete from privacy_list where" + " username=%(LUser)s and name=%(Name)s")). + +add_privacy_list(LUser, Name) -> + ejabberd_sql:sql_query_t( + ?SQL("insert into privacy_list(username, name) " + "values (%(LUser)s, %(Name)s)")). + +set_privacy_list(ID, RItems) -> + ejabberd_sql: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_sql: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, LUser) -> + ejabberd_sql:sql_query( + LServer, + ?SQL("delete from privacy_list where username=%(LUser)s")), + %US = <<LUser/binary, "@", LServer/binary>>, + %ejabberd_sql:sql_query( + % LServer, + % ?SQL("delete from privacy_list_data where value=%(US)s")), + ejabberd_sql: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">>; +escape($\b) -> <<"\\b">>; +escape($\r) -> <<"\\r">>; +escape($') -> <<"''">>; +escape($") -> <<"\\\"">>; +escape($\\) -> <<"\\\\">>; +escape(C) -> <<C>>. + +%% Count number of records in a table given a where clause +count_records_where(LServer, Table, WhereClause) -> + ejabberd_sql:sql_query(LServer, + [<<"select count(*) from ">>, Table, <<" ">>, + WhereClause, <<";">>]). + +get_roster_version(LServer, LUser) -> + ejabberd_sql: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, <<"'">>]). + +opt_type(sql_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(_) -> [sql_type, pgsql_users_number_estimate]. |