diff options
Diffstat (limited to 'src')
100 files changed, 4437 insertions, 2526 deletions
diff --git a/src/acme_challenge.erl b/src/acme_challenge.erl index a65765f74..f4fde4e73 100644 --- a/src/acme_challenge.erl +++ b/src/acme_challenge.erl @@ -3,7 +3,9 @@ -export ([key_authorization/2, solve_challenge/3, process/2, - acme_handler/0 + register_hooks/1, + unregister_hooks/1, + acme_handler/3 ]). %% Challenge Types %% ================ @@ -19,9 +21,14 @@ -include("ejabberd_acme.hrl"). %% This is the default endpoint for the http challenge -%% This function is called by the http_listener -acme_handler() -> - {[<<".well-known">>],acme_challenge}. +%% This hooks is called from ejabberd_http +acme_handler(Handlers, _Host, Request) -> + case Request#request.path of + [<<".well-known">>|_] -> + [{[<<".well-known">>],acme_challenge}|Handlers]; + _ -> + Handlers + end. %% TODO: Maybe validate request here?? process(LocalPath, _Request) -> @@ -30,6 +37,13 @@ process(LocalPath, _Request) -> [{<<"Content-Type">>, <<"text/plain">>}], Result}. +register_hooks(_Domain) -> + ?INFO_MSG("Registering hook for ACME HTTP headers", []), + ejabberd_hooks:add(http_request_handlers, ?MODULE, acme_handler, 50). + +unregister_hooks(_Domain) -> + ?INFO_MSG("Unregistering hook for ACME HTTP headers", []), + ejabberd_hooks:delete(http_request_handlers, ?MODULE, acme_handler, 50). -spec key_authorization(bitstring(), jose_jwk:key()) -> bitstring(). key_authorization(Token, Key) -> diff --git a/src/ejabberd.erl b/src/ejabberd.erl index 4edab98f1..7e1a1106c 100644 --- a/src/ejabberd.erl +++ b/src/ejabberd.erl @@ -37,7 +37,7 @@ -protocol({xep, 270, '1.0'}). -export([start/0, stop/0, start_app/1, start_app/2, - get_pid_file/0, check_app/1]). + get_pid_file/0, check_app/1, module_name/1]). -include("logger.hrl"). @@ -148,3 +148,29 @@ get_module_file(App, Mod) -> Dir -> filename:join([Dir, BaseName ++ ".beam"]) end. + +module_name([Dir, _, <<H,_/binary>> | _] = Mod) when H >= 65, H =< 90 -> + Module = str:join([elixir_name(M) || M<-tl(Mod)], <<>>), + Prefix = case elixir_name(Dir) of + <<"Ejabberd">> -> <<"Elixir.Ejabberd.">>; + Lib -> <<"Elixir.Ejabberd.", Lib/binary, ".">> + end, + misc:binary_to_atom(<<Prefix/binary, Module/binary>>); +module_name([<<"ejabberd">> | _] = Mod) -> + Module = str:join([erlang_name(M) || M<-Mod], $_), + misc:binary_to_atom(Module); +module_name(Mod) when is_list(Mod) -> + Module = str:join([erlang_name(M) || M<-tl(Mod)], $_), + misc:binary_to_atom(Module). + +elixir_name(Atom) when is_atom(Atom) -> + elixir_name(misc:atom_to_binary(Atom)); +elixir_name(<<H,T/binary>>) when H >= 65, H =< 90 -> + <<H, T/binary>>; +elixir_name(<<H,T/binary>>) -> + <<(H-32), T/binary>>. + +erlang_name(Atom) when is_atom(Atom) -> + misc:atom_to_binary(Atom); +erlang_name(Bin) when is_binary(Bin) -> + Bin. diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 6f616a342..e9636f1e5 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -221,6 +221,7 @@ create_new_account(CAUrl, Contact, PrivateKey) -> -spec create_new_authorization(url(), bitstring(), jose_jwk:key()) -> {'ok', proplist()} | no_return(). create_new_authorization(CAUrl, DomainName, PrivateKey) -> + acme_challenge:register_hooks(DomainName), try {ok, Dirs, Nonce0} = ejabberd_acme_comm:directory(CAUrl), Req0 = [{<<"identifier">>, @@ -246,6 +247,8 @@ create_new_authorization(CAUrl, DomainName, PrivateKey) -> ?ERROR_MSG("Error: ~p getting an authorization for domain: ~p~n", [{E,R}, DomainName]), throw({error, DomainName, authorization}) + after + acme_challenge:unregister_hooks(DomainName) end. -spec create_new_certificate(url(), {bitstring(), [bitstring()]}, jose_jwk:key()) -> diff --git a/src/ejabberd_app.erl b/src/ejabberd_app.erl index 64edf508c..3743a8f04 100644 --- a/src/ejabberd_app.erl +++ b/src/ejabberd_app.erl @@ -60,7 +60,9 @@ start(normal, _Args) -> lists:foreach(fun erlang:garbage_collect/1, processes()), {ok, SupPid}; Err -> - Err + ?CRITICAL_MSG("Failed to start ejabberd application: ~p", [Err]), + timer:sleep(1000), + halt("Refer to ejabberd log files to diagnose the problem") end; start(_, _) -> {error, badarg}. @@ -146,7 +148,8 @@ start_apps() -> ejabberd:start_app(fast_yaml), ejabberd:start_app(fast_tls), ejabberd:start_app(xmpp), - ejabberd:start_app(cache_tab). + ejabberd:start_app(cache_tab), + start_eimp(). setup_if_elixir_conf_used() -> case ejabberd_config:is_using_elixir_config() of @@ -170,3 +173,11 @@ start_elixir_application() -> _ -> ok end. + +-ifdef(GRAPHICS). +start_eimp() -> + ejabberd:start_app(eimp). +-else. +start_eimp() -> + ok. +-endif. diff --git a/src/ejabberd_auth.erl b/src/ejabberd_auth.erl index b34925ff0..f49bef43b 100644 --- a/src/ejabberd_auth.erl +++ b/src/ejabberd_auth.erl @@ -35,7 +35,7 @@ check_password/6, check_password_with_authmodule/4, check_password_with_authmodule/6, try_register/3, get_users/0, get_users/1, password_to_scram/1, - get_users/2, export/1, import_info/0, + get_users/2, import_info/0, count_users/1, import/5, import_start/2, count_users/2, get_password/2, get_password_s/2, get_password_with_authmodule/2, @@ -735,8 +735,8 @@ auth_modules(Server) -> LServer = jid:nameprep(Server), Default = ejabberd_config:default_db(LServer, ?MODULE), Methods = ejabberd_config:get_option({auth_method, LServer}, [Default]), - [misc:binary_to_atom(<<"ejabberd_auth_", - (misc:atom_to_binary(M))/binary>>) + [ejabberd:module_name([<<"ejabberd">>, <<"auth">>, + misc:atom_to_binary(M)]) || M <- Methods]. -spec match_passwords(password(), password(), @@ -798,9 +798,6 @@ validate_credentials(User, Server, Password) -> end end. -export(Server) -> - ejabberd_auth_mnesia:export(Server). - import_info() -> [{<<"users">>, 3}]. diff --git a/src/ejabberd_auth_mnesia.erl b/src/ejabberd_auth_mnesia.erl index 690152674..7705f62e1 100644 --- a/src/ejabberd_auth_mnesia.erl +++ b/src/ejabberd_auth_mnesia.erl @@ -34,16 +34,13 @@ -export([start/1, stop/1, set_password/3, try_register/3, get_users/2, init_db/0, count_users/2, get_password/2, - remove_user/2, store_type/1, export/1, import/2, + remove_user/2, store_type/1, import/2, plain_password_required/1, use_cache/1]). -export([need_transform/1, transform/1]). -include("ejabberd.hrl"). -include("logger.hrl"). --include("ejabberd_sql_pt.hrl"). - --record(passwd, {us = {<<"">>, <<"">>} :: {binary(), binary()} | '$1', - password = <<"">> :: binary() | scram() | '_'}). +-include("ejabberd_auth.hrl"). -record(reg_users_counter, {vhost = <<"">> :: binary(), count = 0 :: integer() | '$1'}). @@ -272,29 +269,6 @@ transform(#passwd{password = Password} = P) when is_record(Password, scram) -> P. -export(_Server) -> - [{passwd, - fun(Host, #passwd{us = {LUser, LServer}, password = Password}) - when LServer == Host, - is_binary(Password) -> - [?SQL("delete from users where username=%(LUser)s;"), - ?SQL("insert into users(username, password) " - "values (%(LUser)s, %(Password)s);")]; - (Host, #passwd{us = {LUser, LServer}, password = #scram{} = Scram}) - when LServer == Host -> - StoredKey = Scram#scram.storedkey, - ServerKey = Scram#scram.serverkey, - Salt = Scram#scram.salt, - IterationCount = Scram#scram.iterationcount, - [?SQL("delete from users where username=%(LUser)s;"), - ?SQL("insert into users(username, password, serverkey, salt, " - "iterationcount) " - "values (%(LUser)s, %(StoredKey)s, %(ServerKey)s," - " %(Salt)s, %(IterationCount)d);")]; - (_Host, _R) -> - [] - end}]. - import(LServer, [LUser, Password, _TimeStamp]) -> mnesia:dirty_write( #passwd{us = {LUser, LServer}, password = Password}). diff --git a/src/ejabberd_auth_riak.erl b/src/ejabberd_auth_riak.erl index fccaba102..3cdb74258 100644 --- a/src/ejabberd_auth_riak.erl +++ b/src/ejabberd_auth_riak.erl @@ -40,9 +40,7 @@ -include("ejabberd.hrl"). -include("ejabberd_sql_pt.hrl"). - --record(passwd, {us = {<<"">>, <<"">>} :: {binary(), binary()} | '$1', - password = <<"">> :: binary() | scram() | '_'}). +-include("ejabberd_auth.hrl"). start(_Host) -> ok. @@ -108,9 +106,12 @@ export(_Server) -> [{passwd, fun(Host, #passwd{us = {LUser, LServer}, password = Password}) when LServer == Host -> - [?SQL("delete from users where username=%(LUser)s;"), - ?SQL("insert into users(username, password) " - "values (%(LUser)s, %(Password)s);")]; + [?SQL("delete from users where username=%(LUser)s and %(LServer)H;"), + ?SQL_INSERT( + "users", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "password=%(Password)s"])]; (_Host, _R) -> [] end}]. diff --git a/src/ejabberd_auth_sql.erl b/src/ejabberd_auth_sql.erl index 0d7c7b375..3f328c4a1 100644 --- a/src/ejabberd_auth_sql.erl +++ b/src/ejabberd_auth_sql.erl @@ -35,11 +35,12 @@ -export([start/1, stop/1, set_password/3, try_register/3, get_users/2, count_users/2, get_password/2, remove_user/2, store_type/1, plain_password_required/1, - convert_to_scram/1, opt_type/1]). + convert_to_scram/1, opt_type/1, export/1]). -include("ejabberd.hrl"). -include("logger.hrl"). -include("ejabberd_sql_pt.hrl"). +-include("ejabberd_auth.hrl"). -define(SALT_LENGTH, 16). @@ -60,11 +61,11 @@ set_password(User, Server, Password) -> F = fun() -> if is_record(Password, scram) -> set_password_scram_t( - User, + User, Server, Password#scram.storedkey, Password#scram.serverkey, Password#scram.salt, Password#scram.iterationcount); true -> - set_password_t(User, Password) + set_password_t(User, Server, Password) end end, case ejabberd_sql:sql_transaction(Server, F) of @@ -132,20 +133,22 @@ remove_user(User, Server) -> -define(BATCH_SIZE, 1000). -set_password_scram_t(LUser, +set_password_scram_t(LUser, LServer, StoredKey, ServerKey, Salt, IterationCount) -> ?SQL_UPSERT_T( "users", ["!username=%(LUser)s", + "!server_host=%(LServer)s", "password=%(StoredKey)s", "serverkey=%(ServerKey)s", "salt=%(Salt)s", "iterationcount=%(IterationCount)d"]). -set_password_t(LUser, Password) -> +set_password_t(LUser, LServer, Password) -> ?SQL_UPSERT_T( "users", ["!username=%(LUser)s", + "!server_host=%(LServer)s", "password=%(Password)s"]). get_password_scram(LServer, LUser) -> @@ -153,32 +156,39 @@ get_password_scram(LServer, LUser) -> LServer, ?SQL("select @(password)s, @(serverkey)s, @(salt)s, @(iterationcount)d" " from users" - " where username=%(LUser)s")). + " where username=%(LUser)s and %(LServer)H")). 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)")). + ?SQL_INSERT( + "users", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "password=%(StoredKey)s", + "serverkey=%(ServerKey)s", + "salt=%(Salt)s", + "iterationcount=%(IterationCount)d"])). add_user(LServer, LUser, Password) -> ejabberd_sql:sql_query( LServer, - ?SQL("insert into users(username, password) " - "values (%(LUser)s, %(Password)s)")). + ?SQL_INSERT( + "users", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "password=%(Password)s"])). del_user(LServer, LUser) -> ejabberd_sql:sql_query( LServer, - ?SQL("delete from users where username=%(LUser)s")). + ?SQL("delete from users where username=%(LUser)s and %(LServer)H")). list_users(LServer, []) -> ejabberd_sql:sql_query( LServer, - ?SQL("select @(username)s from users")); + ?SQL("select @(username)s from users where %(LServer)H")); list_users(LServer, [{from, Start}, {to, End}]) when is_integer(Start) and is_integer(End) -> list_users(LServer, @@ -195,6 +205,7 @@ list_users(LServer, [{limit, Limit}, {offset, Offset}]) ejabberd_sql:sql_query( LServer, ?SQL("select @(username)s from users " + "where %(LServer)H " "order by username " "limit %(Limit)d offset %(Offset)d")); list_users(LServer, @@ -206,7 +217,7 @@ list_users(LServer, ejabberd_sql:sql_query( LServer, ?SQL("select @(username)s from users " - "where username like %(SPrefix2)s escape '^' " + "where username like %(SPrefix2)s escape '^' and %(LServer)H " "order by username " "limit %(Limit)d offset %(Offset)d")). @@ -223,11 +234,11 @@ users_number(LServer) -> " where oid = 'users'::regclass::oid")); _ -> ejabberd_sql:sql_query_t( - ?SQL("select @(count(*))d from users")) + ?SQL("select @(count(*))d from users where %(LServer)H")) end; (_Type, _) -> ejabberd_sql:sql_query_t( - ?SQL("select @(count(*))d from users")) + ?SQL("select @(count(*))d from users where %(LServer)H")) end). users_number(LServer, [{prefix, Prefix}]) @@ -237,7 +248,7 @@ users_number(LServer, [{prefix, Prefix}]) ejabberd_sql:sql_query( LServer, ?SQL("select @(count(*))d from users " - "where username like %(SPrefix2)s escape '^'")); + "where username like %(SPrefix2)s escape '^' and %(LServer)H")); users_number(LServer, []) -> users_number(LServer). @@ -253,7 +264,7 @@ convert_to_scram(Server) -> case ejabberd_sql:sql_query_t( ?SQL("select @(username)s, @(password)s" " from users" - " where iterationcount=0" + " where iterationcount=0 and %(LServer)H" " limit %(BatchSize)d")) of {selected, []} -> ok; @@ -269,7 +280,7 @@ convert_to_scram(Server) -> _ -> Scram = ejabberd_auth:password_to_scram(Password), set_password_scram_t( - LUser, + LUser, LServer, Scram#scram.storedkey, Scram#scram.serverkey, Scram#scram.salt, @@ -288,6 +299,36 @@ convert_to_scram(Server) -> end end. +export(_Server) -> + [{passwd, + fun(Host, #passwd{us = {LUser, LServer}, password = Password}) + when LServer == Host, + is_binary(Password) -> + [?SQL("delete from users where username=%(LUser)s and %(LServer)H;"), + ?SQL_INSERT( + "users", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "password=%(Password)s"])]; + (Host, #passwd{us = {LUser, LServer}, password = #scram{} = Scram}) + when LServer == Host -> + StoredKey = Scram#scram.storedkey, + ServerKey = Scram#scram.serverkey, + Salt = Scram#scram.salt, + IterationCount = Scram#scram.iterationcount, + [?SQL("delete from users where username=%(LUser)s and %(LServer)H;"), + ?SQL_INSERT( + "users", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "password=%(StoredKey)s", + "serverkey=%(ServerKey)s", + "salt=%(Salt)s", + "iterationcount=%(IterationCount)d"])]; + (_Host, _R) -> + [] + end}]. + -spec opt_type(pgsql_users_number_estimate) -> fun((boolean()) -> boolean()); (atom()) -> [atom()]. opt_type(pgsql_users_number_estimate) -> diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl index fe60f344e..93bb50836 100644 --- a/src/ejabberd_c2s.erl +++ b/src/ejabberd_c2s.erl @@ -302,10 +302,7 @@ tls_options(#{lserver := LServer, tls_options := DefaultOpts, TLSOpts1 = case {Encrypted, proplists:get_value(certfile, DefaultOpts)} of {true, CertFile} when CertFile /= undefined -> DefaultOpts; {_, _} -> - case ejabberd_config:get_option( - {domain_certfile, LServer}, - ejabberd_config:get_option( - {c2s_certfile, LServer})) of + case get_certfile(LServer) of undefined -> DefaultOpts; CertFile -> lists:keystore(certfile, 1, DefaultOpts, {certfile, CertFile}) @@ -411,7 +408,7 @@ bind(R, #{user := U, server := S, access := Access, lang := Lang, ejabberd_hooks:run(forbidden_session_hook, LServer, [JID]), ?INFO_MSG("(~s) Forbidden c2s session for ~s", [SockMod:pp(Socket), jid:encode(JID)]), - Txt = <<"Denied by ACL">>, + Txt = <<"Access denied by service policy">>, {error, xmpp:err_not_allowed(Txt, Lang), State} end end. @@ -658,7 +655,7 @@ process_presence_out(#{user := User, server := Server, lserver := LServer, MyBareJID = jid:remove_resource(JID), case acl:match_rule(LServer, Access, MyBareJID) of deny -> - ErrText = <<"Denied by ACL">>, + ErrText = <<"Access denied by service policy">>, Err = xmpp:err_forbidden(ErrText, Lang), send_error(State, Pres, Err); allow -> @@ -928,6 +925,17 @@ format_reason(_, {shutdown, _}) -> format_reason(_, _) -> <<"internal server error">>. +-spec get_certfile(binary()) -> file:filename_all(). +get_certfile(LServer) -> + case ejabberd_pkix:get_certfile(LServer) of + {ok, CertFile} -> + CertFile; + error -> + ejabberd_config:get_option( + {domain_certfile, LServer}, + ejabberd_config:get_option({c2s_certfile, LServer})) + end. + transform_listen_option(Opt, Opts) -> [Opt|Opts]. @@ -941,7 +949,11 @@ transform_listen_option(Opt, Opts) -> (resource_conflict) -> fun((resource_conflict()) -> resource_conflict()); (disable_sasl_mechanisms) -> fun((binary() | [binary()]) -> [binary()]); (atom()) -> [atom()]. -opt_type(c2s_certfile) -> fun misc:try_read_file/1; +opt_type(c2s_certfile = Opt) -> + fun(File) -> + ?WARNING_MSG("option '~s' is deprecated, use 'certfiles' instead", [Opt]), + misc:try_read_file(File) + end; opt_type(c2s_ciphers) -> fun iolist_to_binary/1; opt_type(c2s_dhfile) -> fun misc:try_read_file/1; opt_type(c2s_cafile) -> fun misc:try_read_file/1; @@ -986,8 +998,10 @@ opt_type(_) -> (atom()) -> [atom()]. listen_opt_type(access) -> fun acl:access_rules_validator/1; listen_opt_type(shaper) -> fun acl:shaper_rules_validator/1; -listen_opt_type(certfile) -> +listen_opt_type(certfile = Opt) -> fun(S) -> + ?WARNING_MSG("Listening option '~s' for ~s is deprecated, use " + "'certfiles' global option instead", [Opt, ?MODULE]), ejabberd_pkix:add_certfile(S), iolist_to_binary(S) end; diff --git a/src/ejabberd_config.erl b/src/ejabberd_config.erl index 5d3bc8680..4b7c15806 100644 --- a/src/ejabberd_config.erl +++ b/src/ejabberd_config.erl @@ -1417,8 +1417,11 @@ opt_type(cache_life_time) -> (infinity) -> infinity; (unlimited) -> infinity end; -opt_type(domain_certfile) -> - fun misc:try_read_file/1; +opt_type(domain_certfile = Opt) -> + fun(File) -> + ?WARNING_MSG("option '~s' is deprecated, use 'certfiles' instead", [Opt]), + misc:try_read_file(File) + end; opt_type(shared_key) -> fun iolist_to_binary/1; opt_type(node_start) -> diff --git a/src/ejabberd_http.erl b/src/ejabberd_http.erl index 3ba316852..0bc0d8fc4 100644 --- a/src/ejabberd_http.erl +++ b/src/ejabberd_http.erl @@ -136,9 +136,8 @@ init({SockMod, Socket}, Opts) -> true -> [{[], ejabberd_xmlrpc}]; false -> [] end, - Acme = [acme_challenge:acme_handler()], DefinedHandlers = proplists:get_value(request_handlers, Opts, []), - RequestHandlers = Acme ++ DefinedHandlers ++ Captcha ++ Register ++ + RequestHandlers = DefinedHandlers ++ Captcha ++ Register ++ Admin ++ Bind ++ XMLRPC, ?DEBUG("S: ~p~n", [RequestHandlers]), @@ -267,10 +266,11 @@ process_header(State, Data) -> add_header(Name, Value, State)}; {ok, http_eoh} when State#state.request_host == undefined -> - ?WARNING_MSG("An HTTP request without 'Host' HTTP " - "header was received.", - []), - throw(http_request_no_host_header); + ?DEBUG("An HTTP request without 'Host' HTTP " + "header was received.", []), + {State1, Out} = process_request(State), + send_text(State1, Out), + process_header(State, {ok, {http_error, <<>>}}); {ok, http_eoh} -> ?DEBUG("(~w) http query: ~w ~p~n", [State#state.socket, State#state.request_method, @@ -419,6 +419,10 @@ extract_path_query(#state{request_method = Method, extract_path_query(State) -> {State, false}. +process_request(#state{request_host = undefined, + custom_headers = CustomHeaders} = State) -> + {State, make_text_output(State, 400, CustomHeaders, + <<"Missing Host header">>)}; process_request(#state{request_method = Method, request_auth = Auth, request_lang = Lang, @@ -461,7 +465,9 @@ process_request(#state{request_method = Method, opts = Options, headers = RequestHeaders, ip = IP}, - Res = case process(RequestHandlers, Request, Socket, SockMod, Trail) of + RequestHandlers1 = ejabberd_hooks:run_fold( + http_request_handlers, RequestHandlers, [Host, Request]), + Res = case process(RequestHandlers1, Request, Socket, SockMod, Trail) of El when is_record(El, xmlel) -> make_xhtml_output(State, 200, CustomHeaders, El); {Status, Headers, El} diff --git a/src/ejabberd_iq.erl b/src/ejabberd_iq.erl new file mode 100644 index 000000000..7d2751dcb --- /dev/null +++ b/src/ejabberd_iq.erl @@ -0,0 +1,176 @@ +%%%------------------------------------------------------------------- +%%% File : ejabberd_iq.erl +%%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% Purpose : +%%% Created : 10 Nov 2017 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2017 ProcessOne +%%% +%%% This program is free software; you can redistribute it and/or +%%% modify it under the terms of the GNU General Public License as +%%% published by the Free Software Foundation; either version 2 of the +%%% License, or (at your option) any later version. +%%% +%%% This program is distributed in the hope that it will be useful, +%%% but WITHOUT ANY WARRANTY; without even the implied warranty of +%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +%%% General Public License for more details. +%%% +%%% You should have received a copy of the GNU General Public License along +%%% with this program; if not, write to the Free Software Foundation, Inc., +%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +%%% +%%%------------------------------------------------------------------- + +-module(ejabberd_iq). + +-behaviour(gen_server). + +%% API +-export([start_link/0, route/4, dispatch/1]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-include("xmpp.hrl"). +-include("logger.hrl"). + +-record(state, {expire = infinity :: timeout()}). +-type state() :: #state{}. + +%%%=================================================================== +%%% API +%%%=================================================================== +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +-spec route(iq(), atom() | pid(), term(), non_neg_integer()) -> ok. +route(#iq{type = T} = IQ, Proc, Ctx, Timeout) when T == set; T == get -> + Expire = current_time() + Timeout, + Rnd = randoms:get_string(), + ID = encode_id(Expire, Rnd), + ets:insert(?MODULE, {{Expire, Rnd}, Proc, Ctx}), + gen_server:cast(?MODULE, {restart_timer, Expire}), + ejabberd_router:route(IQ#iq{id = ID}). + +-spec dispatch(iq()) -> boolean(). +dispatch(#iq{type = T, id = ID} = IQ) when T == error; T == result -> + case decode_id(ID) of + {ok, Expire, Rnd, Node} -> + ejabberd_cluster:send({?MODULE, Node}, {route, IQ, {Expire, Rnd}}); + error -> + false + end; +dispatch(_) -> + false. + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== +init([]) -> + ets:new(?MODULE, [named_table, ordered_set, public]), + {ok, #state{}}. + +handle_call(Request, From, State) -> + {stop, {unexpected_call, Request, From}, State}. + +handle_cast({restart_timer, Expire}, State) -> + State1 = State#state{expire = min(Expire, State#state.expire)}, + noreply(State1); +handle_cast(Msg, State) -> + ?WARNING_MSG("unexpected cast: ~p", [Msg]), + noreply(State). + +handle_info({route, IQ, Key}, State) -> + case ets:lookup(?MODULE, Key) of + [{_, Proc, Ctx}] -> + callback(Proc, IQ, Ctx), + ets:delete(?MODULE, Key); + [] -> + ok + end, + noreply(State); +handle_info(timeout, State) -> + Expire = clean(ets:first(?MODULE)), + noreply(State#state{expire = Expire}); +handle_info(Info, State) -> + ?WARNING_MSG("unexpected info: ~p", [Info]), + noreply(State). + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +-spec current_time() -> non_neg_integer(). +current_time() -> + p1_time_compat:system_time(milli_seconds). + +-spec clean({non_neg_integer(), binary()} | '$end_of_table') + -> non_neg_integer() | infinity. +clean({Expire, _} = Key) -> + case current_time() of + Time when Time >= Expire -> + case ets:lookup(?MODULE, Key) of + [{_, Proc, Ctx}] -> + callback(Proc, timeout, Ctx), + ets:delete(?MODULE, Key); + [] -> + ok + end, + clean(ets:next(?MODULE, Key)); + _ -> + Expire + end; +clean('$end_of_table') -> + infinity. + +-spec noreply(state()) -> {noreply, state()} | {noreply, state(), non_neg_integer()}. +noreply(#state{expire = Expire} = State) -> + case Expire of + infinity -> + {noreply, State}; + _ -> + Timeout = max(0, Expire - current_time()), + {noreply, State, Timeout} + end. + +-spec encode_id(non_neg_integer(), binary()) -> binary(). +encode_id(Expire, Rnd) -> + ExpireBin = integer_to_binary(Expire), + Node = atom_to_binary(node(), utf8), + CheckSum = calc_checksum(<<ExpireBin/binary, Rnd/binary, Node/binary>>), + <<"rr-", ExpireBin/binary, $-, Rnd/binary, $-, CheckSum/binary, $-, Node/binary>>. + +-spec decode_id(binary()) -> {ok, non_neg_integer(), binary(), atom()} | error. +decode_id(<<"rr-", ID/binary>>) -> + try + [ExpireBin, Tail] = binary:split(ID, <<"-">>), + [Rnd, Rest] = binary:split(Tail, <<"-">>), + [CheckSum, NodeBin] = binary:split(Rest, <<"-">>), + CheckSum = calc_checksum(<<ExpireBin/binary, Rnd/binary, NodeBin/binary>>), + Node = erlang:binary_to_existing_atom(NodeBin, utf8), + Expire = binary_to_integer(ExpireBin), + {ok, Expire, Rnd, Node} + catch _:{badmatch, _} -> + error + end; +decode_id(_) -> + error. + +-spec calc_checksum(binary()) -> binary(). +calc_checksum(Data) -> + Key = ejabberd_config:get_option(shared_key), + base64:encode(crypto:hash(sha, <<Data/binary, Key/binary>>)). + +-spec callback(atom() | pid(), #iq{} | timeout, term()) -> any(). +callback(undefined, IQRes, Fun) -> + Fun(IQRes); +callback(Proc, IQRes, Ctx) -> + Proc ! {iq_reply, IQRes, Ctx}. diff --git a/src/ejabberd_local.erl b/src/ejabberd_local.erl index c1b21d508..cc1d6a2eb 100644 --- a/src/ejabberd_local.erl +++ b/src/ejabberd_local.erl @@ -32,17 +32,21 @@ %% API -export([start/0, start_link/0]). --export([route/1, route_iq/2, route_iq/3, process_iq/1, - process_iq_reply/1, get_features/1, - register_iq_handler/5, register_iq_response_handler/4, - register_iq_response_handler/5, unregister_iq_handler/2, - unregister_iq_response_handler/2, bounce_resource_packet/1, +-export([route/1, process_iq/1, + get_features/1, + register_iq_handler/5, + unregister_iq_handler/2, + bounce_resource_packet/1, host_up/1, host_down/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). +%% deprecated functions: use ejabberd_router:route_iq/3,4 +-export([route_iq/2, route_iq/3]). +-deprecated([{route_iq, 2}, {route_iq, 3}]). + -include("ejabberd.hrl"). -include("logger.hrl"). -include_lib("stdlib/include/ms_transform.hrl"). @@ -50,18 +54,8 @@ -record(state, {}). --record(iq_response, {id = <<"">> :: binary(), - module :: atom(), - function :: atom() | fun(), - timer = make_ref() :: reference()}). - -define(IQTABLE, local_iqtable). -%% This value is used in SIP and Megaco for a transaction lifetime. --define(IQ_TIMEOUT, 32000). - --type ping_timeout() :: non_neg_integer() | undefined. - %%==================================================================== %% API %%==================================================================== @@ -99,17 +93,8 @@ process_iq(#iq{type = T, lang = Lang, sub_els = SubEls} = Packet) end, Err = xmpp:err_bad_request(Txt, Lang), ejabberd_router:route_error(Packet, Err); -process_iq(#iq{type = T} = Packet) when T == result; T == error -> - process_iq_reply(Packet). - --spec process_iq_reply(iq()) -> any(). -process_iq_reply(#iq{id = ID} = IQ) -> - case get_iq_callback(ID) of - {ok, undefined, Function} -> Function(IQ), ok; - {ok, Module, Function} -> - Module:Function(IQ), ok; - _ -> nothing - end. +process_iq(#iq{type = T}) when T == result; T == error -> + ok. -spec route(stanza()) -> any(). route(Packet) -> @@ -119,43 +104,13 @@ route(Packet) -> [xmpp:pp(Packet), {E, {R, erlang:get_stacktrace()}}]) end. --spec route_iq(iq(), function()) -> any(). -route_iq(IQ, F) -> - route_iq(IQ, F, undefined). - --spec route_iq(iq(), function(), ping_timeout()) -> any(). -route_iq(#iq{from = From, type = Type} = IQ, F, Timeout) - when is_function(F) -> - Packet = if Type == set; Type == get -> - ID = randoms:get_string(), - Host = From#jid.lserver, - register_iq_response_handler(Host, ID, undefined, F, Timeout), - IQ#iq{id = ID}; - true -> - IQ - end, - ejabberd_router:route(Packet). - --spec register_iq_response_handler(binary(), binary(), module(), - atom() | function()) -> any(). -register_iq_response_handler(Host, ID, Module, - Function) -> - register_iq_response_handler(Host, ID, Module, Function, - undefined). - --spec register_iq_response_handler(binary(), binary(), module(), - atom() | function(), ping_timeout()) -> any(). -register_iq_response_handler(_Host, ID, Module, - Function, Timeout0) -> - Timeout = case Timeout0 of - undefined -> ?IQ_TIMEOUT; - N when is_integer(N), N > 0 -> N - end, - TRef = erlang:start_timer(Timeout, ?MODULE, ID), - mnesia:dirty_write(#iq_response{id = ID, - module = Module, - function = Function, - timer = TRef}). +-spec route_iq(iq(), function()) -> ok. +route_iq(IQ, Fun) -> + route_iq(IQ, Fun, undefined). + +-spec route_iq(iq(), function(), undefined | non_neg_integer()) -> ok. +route_iq(IQ, Fun, Timeout) -> + ejabberd_router:route_iq(IQ, Fun, undefined, Timeout). -spec register_iq_handler(binary(), binary(), module(), function(), gen_iq_handler:opts()) -> ok. @@ -163,10 +118,6 @@ register_iq_handler(Host, XMLNS, Module, Fun, Opts) -> gen_server:cast(?MODULE, {register_iq_handler, Host, XMLNS, Module, Fun, Opts}). --spec unregister_iq_response_handler(binary(), binary()) -> ok. -unregister_iq_response_handler(_Host, ID) -> - catch get_iq_callback(ID), ok. - -spec unregister_iq_handler(binary(), binary()) -> ok. unregister_iq_handler(Host, XMLNS) -> gen_server:cast(?MODULE, {unregister_iq_handler, Host, XMLNS}). @@ -204,9 +155,6 @@ init([]) -> catch ets:new(?IQTABLE, [named_table, public, ordered_set, {read_concurrency, true}]), update_table(), - ejabberd_mnesia:create(?MODULE, iq_response, - [{ram_copies, [node()]}, - {attributes, record_info(fields, iq_response)}]), {ok, #state{}}. handle_call(_Request, _From, State) -> @@ -232,9 +180,6 @@ handle_cast(_Msg, State) -> {noreply, State}. handle_info({route, Packet}, State) -> route(Packet), {noreply, State}; -handle_info({timeout, _TRef, ID}, State) -> - process_iq_timeout(ID), - {noreply, State}; handle_info(Info, State) -> ?WARNING_MSG("unexpected info: ~p", [Info]), {noreply, State}. @@ -269,15 +214,8 @@ do_route(Packet) -> -spec update_table() -> ok. update_table() -> - case catch mnesia:table_info(iq_response, attributes) of - [id, module, function] -> - mnesia:delete_table(iq_response), - ok; - [id, module, function, timer] -> - ok; - {'EXIT', _} -> - ok - end. + catch mnesia:delete_table(iq_response), + ok. host_up(Host) -> Owner = case whereis(?MODULE) of @@ -296,41 +234,3 @@ host_down(Host) -> ejabberd_router:unregister_route(Host, Owner), ejabberd_hooks:delete(local_send_to_resource_hook, Host, ?MODULE, bounce_resource_packet, 100). - --spec get_iq_callback(binary()) -> {ok, module(), atom() | function()} | error. -get_iq_callback(ID) -> - case mnesia:dirty_read(iq_response, ID) of - [#iq_response{module = Module, timer = TRef, - function = Function}] -> - cancel_timer(TRef), - mnesia:dirty_delete(iq_response, ID), - {ok, Module, Function}; - _ -> - error - end. - --spec process_iq_timeout(binary()) -> any(). -process_iq_timeout(ID) -> - spawn(fun process_iq_timeout/0) ! ID. - --spec process_iq_timeout() -> any(). -process_iq_timeout() -> - receive - ID -> - case get_iq_callback(ID) of - {ok, undefined, Function} -> - Function(timeout); - _ -> - ok - end - after 5000 -> - ok - end. - --spec cancel_timer(reference()) -> ok. -cancel_timer(TRef) -> - case erlang:cancel_timer(TRef) of - false -> - receive {timeout, TRef, _} -> ok after 0 -> ok end; - _ -> ok - end. diff --git a/src/ejabberd_mnesia.erl b/src/ejabberd_mnesia.erl index 16e385011..34691545a 100644 --- a/src/ejabberd_mnesia.erl +++ b/src/ejabberd_mnesia.erl @@ -68,6 +68,8 @@ init([]) -> _ -> ok end, ejabberd:start_app(mnesia, permanent), + ?DEBUG("Waiting for Mnesia tables synchronization...", []), + mnesia:wait_for_tables(mnesia:system_info(local_tables), infinity), Schema = read_schema_file(), {ok, #state{schema = Schema}}; false -> diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl index 3e3fc3082..df4e4bc21 100644 --- a/src/ejabberd_oauth.erl +++ b/src/ejabberd_oauth.erl @@ -436,7 +436,7 @@ process(_Handlers, ?INPUT(<<"hidden">>, <<"scope">>, Scope), ?INPUT(<<"hidden">>, <<"state">>, State), ?BR, - ?LABEL(<<"ttl">>, [?CT(<<"Token TTL">>), ?CT(<<": ">>)]), + ?LABEL(<<"ttl">>, [?CT(<<"Token TTL">>), ?C(<<": ">>)]), ?XAE(<<"select">>, [{<<"name">>, <<"ttl">>}], [ ?XAC(<<"option">>, [{<<"value">>, <<"3600">>}],<<"1 Hour">>), @@ -632,120 +632,19 @@ web_head() -> ]. css() -> - <<" - body { - margin: 0; - padding: 0; - - font-family: sans-serif; - color: #fff; - } - - h1 { - font-size: 3em; - color: #444; - } - - p { - line-height: 1.5em; - color: #888; - } - - a { - color: #fff; - } - a:hover, - a:active { - text-decoration: underline; - } - - em { - display: inline-block; - padding: 0 5px; - - background: #f4f4f4; - border-radius: 5px; - - font-style: normal; - font-weight: bold; - color: #444; - } - - form { - color: #444; - } - label { - display: block; - font-weight: bold; - } - - input[type=text], - input[type=password] { - margin-bottom: 1em; - padding: 0.4em; - - max-width: 330px; - width: 100%; - - border: 1px solid #c4c4c4; - border-radius: 5px; - outline: 0; - - font-size: 1.2em; - } - input[type=text]:focus, - input[type=password]:focus, - input[type=text]:active, - input[type=password]:active { - border-color: #41AFCA; - } - - input[type=submit] { - font-size: 1em; - } - - .container { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - - background: #424A55; - background-image: -webkit-linear-gradient(270deg, rgba(48,52,62,0) 24%, #30353e 100%); - background-image: linear-gradient(-180deg, rgba(48,52,62,0) 24%, #30353e 100%); - } - - .section { - padding: 3em; - } - .white.section { - background: #fff; - border-bottom: 4px solid #41AFCA; - } - - .white.section a { - text-decoration: none; - color: #41AFCA; - } - .white.section a:hover, - .white.section a:active { - text-decoration: underline; - } - - .container > .section { - background: #424A55; - } - - .block { - margin: 0 auto; - max-width: 900px; - width: 100%; - } -">>. + case misc:read_css("oauth.css") of + {ok, Data} -> Data; + {error, _} -> <<>> + end. logo() -> - <<"data:image/png;base64,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">>. + case misc:read_img("oauth-logo.png") of + {ok, Img} -> + B64Img = base64:encode(Img), + <<"data:image/png;base64,", B64Img/binary>>; + {error, _} -> + <<>> + end. -spec opt_type(oauth_expire) -> fun((non_neg_integer()) -> non_neg_integer()); (oauth_access) -> fun((any()) -> any()); diff --git a/src/ejabberd_pkix.erl b/src/ejabberd_pkix.erl index 89b33b8aa..68b8226c8 100644 --- a/src/ejabberd_pkix.erl +++ b/src/ejabberd_pkix.erl @@ -27,19 +27,22 @@ %% API -export([start_link/0, add_certfile/1, format_error/1, opt_type/1, - get_certfile/1, try_certfile/1, route_registered/1]). + get_certfile/1, try_certfile/1, route_registered/1, + config_reloaded/0]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -include_lib("public_key/include/public_key.hrl"). -include("logger.hrl"). --include("jid.hrl"). -record(state, {validate = true :: boolean(), - certs = #{}}). --record(cert_state, {domains = [] :: [binary()]}). + notify = false :: boolean(), + paths = [] :: [file:filename()], + certs = #{} :: map(), + keys = [] :: [public_key:private_key()]}). +-type state() :: #state{}. -type cert() :: #'OTPCertificate'{}. -type priv_key() :: public_key:private_key(). -type pub_key() :: #'RSAPublicKey'{} | {integer(), #'Dss-Parms'{}} | #'ECPoint'{}. @@ -62,8 +65,8 @@ add_certfile(Path) -> -spec try_certfile(filename:filename()) -> binary(). try_certfile(Path0) -> Path = prep_path(Path0), - case mk_cert_state(Path, false) of - {ok, _} -> Path; + case load_certfile(Path) of + {ok, _, _} -> Path; {error, _} -> erlang:error(badarg) end. @@ -78,14 +81,14 @@ format_error(not_pem) -> format_error(not_der) -> "failed to decode from DER format"; format_error(encrypted) -> - "encrypted certificate found in the chain"; + "encrypted certificate"; format_error({bad_cert, cert_expired}) -> "certificate is no longer valid as its expiration date has passed"; format_error({bad_cert, invalid_issuer}) -> "certificate issuer name does not match the name of the " - "issuer certificate in the chain"; + "issuer certificate"; format_error({bad_cert, invalid_signature}) -> - "certificate was not signed by its issuer certificate in the chain"; + "certificate was not signed by its issuer certificate"; format_error({bad_cert, name_not_permitted}) -> "invalid Subject Alternative Name extension"; format_error({bad_cert, missing_basic_constraint}) -> @@ -95,7 +98,7 @@ format_error({bad_cert, invalid_key_usage}) -> "certificate key is used in an invalid way according " "to the key-usage extension"; format_error({bad_cert, selfsigned_peer}) -> - "self-signed certificate in the chain"; + "self-signed certificate"; format_error({bad_cert, unknown_sig_algo}) -> "certificate is signed using unknown algorithm"; format_error({bad_cert, unknown_ca}) -> @@ -139,18 +142,27 @@ get_certfile(Domain) -> start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). +config_reloaded() -> + gen_server:cast(?MODULE, config_reloaded). + opt_type(ca_path) -> fun(Path) -> iolist_to_binary(Path) end; +opt_type(certfiles) -> + fun(CertList) -> + [binary_to_list(Path) || Path <- CertList] + end; opt_type(_) -> - [ca_path]. + [ca_path, certfiles]. %%%=================================================================== %%% gen_server callbacks %%%=================================================================== init([]) -> + Notify = start_fs(), process_flag(trap_exit, true), - ets:new(?MODULE, [named_table, public, bag]), + ets:new(?MODULE, [named_table, public]), ejabberd_hooks:add(route_registered, ?MODULE, route_registered, 50), + ejabberd_hooks:add(config_reloaded, ?MODULE, config_reloaded, 30), Validate = case os:type() of {win32, _} -> false; _ -> @@ -161,35 +173,80 @@ init([]) -> if Validate -> check_ca_dir(); true -> ok end, - State = #state{validate = Validate}, - {ok, add_certfiles(State)}. + State = #state{validate = Validate, notify = Notify}, + case filelib:ensure_dir(filename:join(certs_dir(), "foo")) of + ok -> + clean_dir(certs_dir()), + case add_certfiles(State) of + {ok, State1} -> + {ok, State1}; + {error, Why} -> + {stop, Why} + end; + {error, Why} -> + ?CRITICAL_MSG("Failed to create directory ~s: ~s", + [certs_dir(), file:format_error(Why)]), + {stop, Why} + end. handle_call({add_certfile, Path}, _, State) -> {Result, NewState} = add_certfile(Path, State), {reply, Result, NewState}; handle_call({route_registered, Host}, _, State) -> - NewState = add_certfiles(Host, State), - case get_certfile(Host) of - {ok, _} -> ok; - error -> - ?WARNING_MSG("No certificate found matching '~s': strictly " - "configured clients or servers will reject " - "connections with this host", [Host]) - end, - {reply, ok, NewState}; + case add_certfiles(Host, State) of + {ok, NewState} -> + case get_certfile(Host) of + {ok, _} -> ok; + error -> + ?WARNING_MSG("No certificate found matching '~s': strictly " + "configured clients or servers will reject " + "connections with this host; obtain " + "a certificate for this (sub)domain from any " + "trusted CA such as Let's Encrypt " + "(www.letsencrypt.org)", + [Host]) + end, + {reply, ok, NewState}; + {error, _} -> + {reply, ok, State} + end; handle_call(_Request, _From, State) -> Reply = ok, {reply, Reply, State}. +handle_cast(config_reloaded, State) -> + State1 = State#state{paths = [], certs = #{}, keys = []}, + case add_certfiles(State1) of + {ok, State2} -> + {noreply, State2}; + {error, _} -> + {noreply, State} + end; handle_cast(_Msg, State) -> {noreply, State}. +handle_info({_, {fs, file_event}, {File, Events}}, State) -> + ?DEBUG("got FS events for ~s: ~p", [File, Events]), + Path = iolist_to_binary(File), + case lists:member(modified, Events) of + true -> + case lists:member(Path, State#state.paths) of + true -> + handle_cast(config_reloaded, State); + false -> + {noreply, State} + end; + false -> + {noreply, State} + end; handle_info(_Info, State) -> ?WARNING_MSG("unexpected info: ~p", [_Info]), {noreply, State}. terminate(_Reason, _State) -> - ejabberd_hooks:delete(route_registered, ?MODULE, route_registered, 50). + ejabberd_hooks:delete(route_registered, ?MODULE, route_registered, 50), + ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 30), + clean_dir(certs_dir()). code_change(_OldVsn, State, _Extra) -> {ok, State}. @@ -197,82 +254,157 @@ code_change(_OldVsn, State, _Extra) -> %%%=================================================================== %%% Internal functions %%%=================================================================== +-spec certfiles_from_config_options() -> [atom()]. +certfiles_from_config_options() -> + [c2s_certfile, s2s_certfile, domain_certfile]. + +-spec get_certfiles_from_config_options(state()) -> [binary()]. +get_certfiles_from_config_options(State) -> + Global = case ejabberd_config:get_option(certfiles) of + undefined -> + []; + Paths -> + lists:flatmap(fun filelib:wildcard/1, Paths) + end, + Local = lists:flatmap( + fun(OptHost) -> + case ejabberd_config:get_option(OptHost) of + undefined -> []; + Path -> [Path] + end + end, [{Opt, Host} + || Opt <- certfiles_from_config_options(), + Host <- ejabberd_config:get_myhosts()]), + [iolist_to_binary(P) || P <- lists:usort(Local ++ Global)]. + +-spec add_certfiles(state()) -> {ok, state()} | {error, bad_cert()}. add_certfiles(State) -> - lists:foldl( - fun(Host, AccState) -> - add_certfiles(Host, AccState) - end, State, ejabberd_config:get_myhosts()). + Paths = get_certfiles_from_config_options(State), + State1 = lists:foldl( + fun(Path, Acc) -> + {_, NewAcc} = add_certfile(Path, Acc), + NewAcc + end, State, Paths), + case build_chain_and_check(State1) of + ok -> {ok, State1}; + {error, _} = Err -> Err + end. +-spec add_certfiles(binary(), state()) -> {ok, state()} | {error, bad_cert()}. add_certfiles(Host, State) -> - NewState = - lists:foldl( - fun(Opt, AccState) -> - case ejabberd_config:get_option({Opt, Host}) of - undefined -> AccState; - Path -> - {_, NewAccState} = add_certfile(Path, AccState), - NewAccState - end - end, State, [c2s_certfile, s2s_certfile, domain_certfile]), - %% Add acme certificate if it exists - case ejabberd_acme:certificate_exists(Host) of - {true, Path} -> - {_, FinalState} = add_certfile(Path, NewState), - FinalState; - false -> - NewState + State1 = lists:foldl( + fun(Opt, AccState) -> + case ejabberd_config:get_option({Opt, Host}) of + undefined -> AccState; + Path -> + {_, NewAccState} = add_certfile(Path, AccState), + NewAccState + end + end, State, certfiles_from_config_options()), + State2 = case ejabberd_acme:certificate_exists(Host) of + {true, Path} -> + {_, State3} = add_certfile(Path, State1), + State3; + false -> + State1 + end, + if State /= State2 -> + case build_chain_and_check(State1) of + ok -> {ok, State1}; + {error, _} = Err -> Err + end; + true -> + {ok, State} end. +-spec add_certfile(file:filename_all(), state()) -> {ok, state()} | + {{error, cert_error()}, state()}. add_certfile(Path, State) -> - case maps:get(Path, State#state.certs, undefined) of - #cert_state{} -> + case lists:member(Path, State#state.paths) of + true -> {ok, State}; - undefined -> - case mk_cert_state(Path, State#state.validate) of - {error, Reason} -> - {{error, Reason}, State}; - {ok, CertState} -> - NewCerts = maps:put(Path, CertState, State#state.certs), - lists:foreach( - fun(Domain) -> - ets:insert(?MODULE, {Domain, Path}) - end, CertState#cert_state.domains), - {ok, State#state{certs = NewCerts}} + false -> + case load_certfile(Path) of + {ok, Certs, Keys} -> + NewCerts = lists:foldl( + fun(Cert, Acc) -> + maps:put(Cert, Path, Acc) + end, State#state.certs, Certs), + {ok, State#state{paths = [Path|State#state.paths], + certs = NewCerts, + keys = Keys ++ State#state.keys}}; + {error, Why} = Err -> + ?ERROR_MSG("failed to read certificate from ~s: ~s", + [Path, format_error(Why)]), + {Err, State} end end. -mk_cert_state(Path, Validate) -> - case check_certfile(Path, Validate) of - {ok, Ds} -> - {ok, #cert_state{domains = Ds}}; - {invalid, Ds, {bad_cert, _} = Why} -> - ?WARNING_MSG("certificate from ~s is invalid: ~s", - [Path, format_error(Why)]), - {ok, #cert_state{domains = Ds}}; - {error, Why} = Err -> - ?ERROR_MSG("failed to read certificate from ~s: ~s", +-spec build_chain_and_check(state()) -> ok | {error, bad_cert()}. +build_chain_and_check(State) -> + ?DEBUG("Rebuilding certificate chains from ~s", + [str:join(State#state.paths, <<", ">>)]), + CertPaths = get_cert_paths(maps:keys(State#state.certs)), + case match_cert_keys(CertPaths, State#state.keys) of + {ok, Chains} -> + CertFilesWithDomains = store_certs(Chains, []), + ets:delete_all_objects(?MODULE), + lists:foreach( + fun({Path, Domain}) -> + ets:insert(?MODULE, {Domain, Path}) + end, CertFilesWithDomains), + Errors = validate(CertPaths, State#state.validate), + subscribe(State), + lists:foreach( + fun({Cert, Why}) -> + Path = maps:get(Cert, State#state.certs), + ?WARNING_MSG("Failed to validate certificate from ~s: ~s", + [Path, format_error(Why)]) + end, Errors); + {error, Cert, Why} -> + Path = maps:get(Cert, State#state.certs), + ?ERROR_MSG("Failed to build certificate chain for ~s: ~s", [Path, format_error(Why)]), - Err + {error, Why} end. --spec check_certfile(filename:filename(), boolean()) - -> {ok, [binary()]} | {invalid, [binary()], bad_cert()} | - {error, cert_error() | file:posix()}. -check_certfile(Path, Validate) -> +-spec store_certs([{[cert()], priv_key()}], + [{binary(), binary()}]) -> [{binary(), binary()}]. +store_certs([{Certs, Key}|Chains], Acc) -> + CertPEMs = public_key:pem_encode( + lists:map( + fun(Cert) -> + Type = element(1, Cert), + DER = public_key:pkix_encode(Type, Cert, otp), + {'Certificate', DER, not_encrypted} + end, Certs)), + KeyPEM = public_key:pem_encode( + [{element(1, Key), + public_key:der_encode(element(1, Key), Key), + not_encrypted}]), + PEMs = <<CertPEMs/binary, KeyPEM/binary>>, + Cert = hd(Certs), + Domains = xmpp_stream_pkix:get_cert_domains(Cert), + FileName = filename:join(certs_dir(), str:sha(PEMs)), + case file:write_file(FileName, PEMs) of + ok -> + file:change_mode(FileName, 8#600), + NewAcc = [{FileName, Domain} || Domain <- Domains] ++ Acc, + store_certs(Chains, NewAcc); + {error, Why} -> + ?ERROR_MSG("Failed to write to ~s: ~s", + [FileName, file:format_error(Why)]), + store_certs(Chains, []) + end; +store_certs([], Acc) -> + Acc. + +-spec load_certfile(file:filename_all()) -> {ok, [cert()], [priv_key()]} | + {error, cert_error() | file:posix()}. +load_certfile(Path) -> try {ok, Data} = file:read_file(Path), - {ok, Certs, PrivKeys} = pem_decode(Data), - CertPaths = get_cert_paths(Certs), - Domains = get_domains(CertPaths), - case match_cert_keys(CertPaths, PrivKeys) of - {ok, _} -> - case validate(CertPaths, Validate) of - ok -> {ok, Domains}; - {error, Why} -> {invalid, Domains, Why} - end; - {error, Why} -> - {invalid, Domains, Why} - end + pem_decode(Data) catch _:{badmatch, {error, _} = Err} -> Err end. @@ -290,7 +422,7 @@ pem_decode(Data) -> fun(#'OTPCertificate'{}) -> true; (_) -> false end, Objects) of - {[], _} -> + {[], []} -> {error, not_cert}; {Certs, PrivKeys} -> {ok, Certs, PrivKeys} @@ -340,41 +472,44 @@ decode_certs(PemEntries) -> {error, not_der} end. --spec validate([{path, [cert()]}], boolean()) -> ok | {error, bad_cert()}. -validate([{path, Path}|Paths], true) -> - case validate_path(Path) of - ok -> - validate(Paths, true); - Err -> - Err - end; +-spec validate([{path, [cert()]}], boolean()) -> [{cert(), bad_cert()}]. +validate(Paths, true) -> + lists:flatmap( + fun({path, Path}) -> + case validate_path(Path) of + ok -> + []; + {error, Cert, Reason} -> + [{Cert, Reason}] + end + end, Paths); validate(_, _) -> - ok. + []. --spec validate_path([cert()]) -> ok | {error, bad_cert()}. +-spec validate_path([cert()]) -> ok | {error, cert(), bad_cert()}. validate_path([Cert|_] = Certs) -> case find_local_issuer(Cert) of {ok, IssuerCert} -> try public_key:pkix_path_validation(IssuerCert, Certs, []) of {ok, _} -> ok; - Err -> - Err + {error, Reason} -> + {error, Cert, Reason} catch error:function_clause -> case erlang:get_stacktrace() of [{public_key, pkix_sign_types, _, _}|_] -> - {error, {bad_cert, unknown_sig_algo}}; + {error, Cert, {bad_cert, unknown_sig_algo}}; ST -> %% Bug in public_key application erlang:raise(error, function_clause, ST) end end; - {error, _} = Err -> + {error, Reason} -> case public_key:pkix_is_self_signed(Cert) of true -> - {error, {bad_cert, selfsigned_peer}}; + {error, Cert, {bad_cert, selfsigned_peer}}; false -> - Err + {error, Cert, Reason} end end. @@ -382,6 +517,25 @@ validate_path([Cert|_] = Certs) -> ca_dir() -> ejabberd_config:get_option(ca_path, "/etc/ssl/certs"). +-spec certs_dir() -> string(). +certs_dir() -> + MnesiaDir = mnesia:system_info(directory), + filename:join(MnesiaDir, "certs"). + +-spec clean_dir(file:filename_all()) -> ok. +clean_dir(Dir) -> + ?DEBUG("Cleaning directory ~s", [Dir]), + Files = filelib:wildcard(filename:join(Dir, "*")), + lists:foreach( + fun(Path) -> + case filelib:is_file(Path) of + true -> + file:delete(Path); + false -> + ok + end + end, Files). + -spec check_ca_dir() -> ok. check_ca_dir() -> case filelib:wildcard(filename:join(ca_dir(), "*.0")) of @@ -433,13 +587,13 @@ match_cert_keys(CertPaths, PrivKeys) -> -spec match_cert_keys([{path, [cert()]}], [{pub_key(), priv_key()}], [{cert(), priv_key()}]) - -> {ok, [{cert(), priv_key()}]} | {error, {bad_cert, missing_priv_key}}. + -> {ok, [{[cert()], priv_key()}]} | {error, cert(), {bad_cert, missing_priv_key}}. match_cert_keys([{path, Certs}|CertPaths], KeyPairs, Result) -> [Cert|_] = RevCerts = lists:reverse(Certs), PubKey = pubkey_from_cert(Cert), case lists:keyfind(PubKey, 1, KeyPairs) of false -> - {error, {bad_cert, missing_priv_key}}; + {error, Cert, {bad_cert, missing_priv_key}}; {_, PrivKey} -> match_cert_keys(CertPaths, KeyPairs, [{RevCerts, PrivKey}|Result]) end; @@ -474,15 +628,6 @@ pubkey_from_privkey(#'DSAPrivateKey'{p = P, q = Q, g = G, y = Y}) -> pubkey_from_privkey(#'ECPrivateKey'{publicKey = Key}) -> #'ECPoint'{point = Key}. --spec get_domains([{path, [cert()]}]) -> [binary()]. -get_domains(CertPaths) -> - lists:usort( - lists:flatmap( - fun({path, Certs}) -> - Cert = lists:last(Certs), - xmpp_stream_pkix:get_cert_domains(Cert) - end, CertPaths)). - -spec get_cert_paths([cert()]) -> [{path, [cert()]}]. get_cert_paths(Certs) -> G = digraph:new([acyclic]), @@ -542,3 +687,37 @@ short_name_hash(IssuerID) -> short_name_hash(_) -> "". -endif. + +-spec subscribe(state()) -> ok. +subscribe(#state{notify = true} = State) -> + lists:foreach( + fun(Path) -> + Dir = filename:dirname(Path), + Name = list_to_atom(integer_to_list(erlang:phash2(Dir))), + case fs:start_link(Name, Dir) of + {ok, _} -> + ?DEBUG("Subscribed to FS events from ~s", [Dir]), + fs:subscribe(Name); + {error, _} -> + ok + end + end, State#state.paths); +subscribe(_) -> + ok. + +-spec start_fs() -> boolean(). +start_fs() -> + application:load(fs), + application:set_env(fs, backwards_compatible, false), + case application:ensure_all_started(fs) of + {ok, _} -> true; + {error, {already_loaded, _}} -> true; + {error, Reason} -> + ?ERROR_MSG("Failed to load 'fs' Erlang application: ~p; " + "certificates change detection will be disabled. " + "You should now manually run `ejabberdctl " + "reload_config` whenever certificates are changed " + "on disc", + [Reason]), + false + end. diff --git a/src/ejabberd_redis.erl b/src/ejabberd_redis.erl index 56948ec83..76ae10ace 100644 --- a/src/ejabberd_redis.erl +++ b/src/ejabberd_redis.erl @@ -45,7 +45,7 @@ -define(SERVER, ?MODULE). -define(PROCNAME, 'ejabberd_redis_client'). -define(TR_STACK, redis_transaction_stack). --define(DEFAULT_MAX_QUEUE, 5000). +-define(DEFAULT_MAX_QUEUE, 10000). -define(MAX_RETRIES, 1). -define(CALL_TIMEOUT, 60*1000). %% 60 seconds diff --git a/src/ejabberd_router.erl b/src/ejabberd_router.erl index 69413c6de..e29014835 100644 --- a/src/ejabberd_router.erl +++ b/src/ejabberd_router.erl @@ -37,6 +37,9 @@ %% API -export([route/1, route_error/2, + route_iq/2, + route_iq/3, + route_iq/4, register_route/2, register_route/3, register_route/4, @@ -62,6 +65,9 @@ -export([route/3, route_error/4]). -deprecated([{route, 3}, {route_error, 4}]). +%% This value is used in SIP and Megaco for a transaction lifetime. +-define(IQ_TIMEOUT, 32000). + -include("ejabberd.hrl"). -include("logger.hrl"). -include("ejabberd_router.hrl"). @@ -136,6 +142,20 @@ route_error(From, To, Packet, #stanza_error{} = Err) -> route(From, To, xmpp:make_error(Packet, Err)) end. +-spec route_iq(iq(), fun((iq() | timeout) -> any())) -> ok. +route_iq(IQ, Fun) -> + route_iq(IQ, Fun, undefined, ?IQ_TIMEOUT). + +-spec route_iq(iq(), term(), pid() | atom()) -> ok. +route_iq(IQ, State, Proc) -> + route_iq(IQ, State, Proc, ?IQ_TIMEOUT). + +-spec route_iq(iq(), term(), pid() | atom(), undefined | non_neg_integer()) -> ok. +route_iq(IQ, State, Proc, undefined) -> + route_iq(IQ, State, Proc, ?IQ_TIMEOUT); +route_iq(IQ, State, Proc, Timeout) -> + ejabberd_iq:route(IQ, Proc, State, Timeout). + -spec register_route(binary(), binary()) -> ok. register_route(Domain, ServerHost) -> register_route(Domain, ServerHost, undefined). @@ -339,18 +359,23 @@ do_route(OrigPacket) -> drop -> ok; Packet -> - To = xmpp:get_to(Packet), - LDstDomain = To#jid.lserver, - case find_routes(LDstDomain) of - [] -> - ejabberd_s2s:route(Packet); - [Route] -> - do_route(Packet, Route); - Routes -> - From = xmpp:get_from(Packet), - balancing_route(From, To, Packet, Routes) - end, - ok + case ejabberd_iq:dispatch(Packet) of + true -> + ok; + false -> + To = xmpp:get_to(Packet), + LDstDomain = To#jid.lserver, + case find_routes(LDstDomain) of + [] -> + ejabberd_s2s:route(Packet); + [Route] -> + do_route(Packet, Route); + Routes -> + From = xmpp:get_from(Packet), + balancing_route(From, To, Packet, Routes) + end, + ok + end end. -spec do_route(stanza(), #route{}) -> any(). diff --git a/src/ejabberd_s2s.erl b/src/ejabberd_s2s.erl index cb4e5e5ec..0626d62fb 100644 --- a/src/ejabberd_s2s.erl +++ b/src/ejabberd_s2s.erl @@ -198,13 +198,11 @@ dirty_get_connections() -> -spec tls_options(binary(), [proplists:property()]) -> [proplists:property()]. tls_options(LServer, DefaultOpts) -> - TLSOpts1 = case ejabberd_config:get_option( - {domain_certfile, LServer}, - ejabberd_config:get_option( - {s2s_certfile, LServer})) of + TLSOpts1 = case get_certfile(LServer) of undefined -> DefaultOpts; - CertFile -> lists:keystore(certfile, 1, DefaultOpts, - {certfile, CertFile}) + CertFile -> + lists:keystore(certfile, 1, DefaultOpts, + {certfile, CertFile}) end, TLSOpts2 = case ejabberd_config:get_option( {s2s_ciphers, LServer}) of @@ -269,6 +267,17 @@ queue_type(LServer) -> {s2s_queue_type, LServer}, ejabberd_config:default_queue_type(LServer)). +-spec get_certfile(binary()) -> file:filename_all(). +get_certfile(LServer) -> + case ejabberd_pkix:get_certfile(LServer) of + {ok, CertFile} -> + CertFile; + error -> + ejabberd_config:get_option( + {domain_certfile, LServer}, + ejabberd_config:get_option({s2s_certfile, LServer})) + end. + %%==================================================================== %% gen_server callbacks %%==================================================================== @@ -369,7 +378,7 @@ do_route(Packet) -> <<"Server connections to local " "subdomains are forbidden">>, Lang); forbidden -> - xmpp:err_forbidden(<<"Denied by ACL">>, Lang); + xmpp:err_forbidden(<<"Access denied by service policy">>, Lang); internal_server_error -> xmpp:err_internal_server_error() end, @@ -711,7 +720,11 @@ opt_type(route_subdomains) -> end; opt_type(s2s_access) -> fun acl:access_rules_validator/1; -opt_type(s2s_certfile) -> fun misc:try_read_file/1; +opt_type(s2s_certfile = Opt) -> + fun(File) -> + ?WARNING_MSG("option '~s' is deprecated, use 'certfiles' instead", [Opt]), + misc:try_read_file(File) + end; opt_type(s2s_ciphers) -> fun iolist_to_binary/1; opt_type(s2s_dhfile) -> fun misc:try_read_file/1; opt_type(s2s_cafile) -> fun misc:try_read_file/1; @@ -726,7 +739,13 @@ opt_type(s2s_use_starttls) -> (false) -> false; (optional) -> optional; (required) -> required; - (required_trusted) -> required_trusted + (required_trusted) -> + ?WARNING_MSG("The value 'required_trusted' of option " + "'s2s_use_starttls' is deprected and will be " + "unsupported in future releases. Instead, " + "set it to 'required' and make sure " + "mod_s2s_dialback is *NOT* loaded", []), + required_trusted end; opt_type(s2s_zlib) -> fun(B) when is_boolean(B) -> B end; diff --git a/src/ejabberd_s2s_in.erl b/src/ejabberd_s2s_in.erl index 48a650a4e..a949e83d6 100644 --- a/src/ejabberd_s2s_in.erl +++ b/src/ejabberd_s2s_in.erl @@ -359,8 +359,10 @@ change_shaper(#{shaper := ShaperName, server_host := ServerHost} = State, (max_fsm_queue) -> fun((pos_integer()) -> pos_integer()); (atom()) -> [atom()]. listen_opt_type(shaper) -> fun acl:shaper_rules_validator/1; -listen_opt_type(certfile) -> +listen_opt_type(certfile = Opt) -> fun(S) -> + ?WARNING_MSG("Listening option '~s' for ~s is deprecated, use " + "'certfiles' global option instead", [Opt, ?MODULE]), ejabberd_pkix:add_certfile(S), iolist_to_binary(S) end; diff --git a/src/ejabberd_service.erl b/src/ejabberd_service.erl index dd6310fbe..7b5f945d0 100644 --- a/src/ejabberd_service.erl +++ b/src/ejabberd_service.erl @@ -199,7 +199,7 @@ handle_info({route, Packet}, #{access := Access} = State) -> xmpp_stream_in:send(State, Packet); deny -> Lang = xmpp:get_lang(Packet), - Err = xmpp:err_not_allowed(<<"Denied by ACL">>, Lang), + Err = xmpp:err_not_allowed(<<"Access denied by service policy">>, Lang), ejabberd_router:route_error(Packet, Err), State end; diff --git a/src/ejabberd_sip.erl b/src/ejabberd_sip.erl index 2c98aec16..01bb7ffcc 100644 --- a/src/ejabberd_sip.erl +++ b/src/ejabberd_sip.erl @@ -1,5 +1,7 @@ %%%------------------------------------------------------------------- -%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% File : ejabberd_sip.erl +%%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% Purpose : %%% Created : 30 Apr 2017 by Evgeny Khramtsov <ekhramtsov@process-one.net> %%% %%% @@ -20,6 +22,7 @@ %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%------------------------------------------------------------------- + -module(ejabberd_sip). -ifndef(SIP). diff --git a/src/ejabberd_sm.erl b/src/ejabberd_sm.erl index 96dbb4e83..3df1d88e0 100644 --- a/src/ejabberd_sm.erl +++ b/src/ejabberd_sm.erl @@ -137,10 +137,17 @@ route(To, Term) -> -spec route(stanza()) -> ok. route(Packet) -> - try do_route(Packet), ok - catch E:R -> - ?ERROR_MSG("failed to route packet:~n~s~nReason = ~p", - [xmpp:pp(Packet), {E, {R, erlang:get_stacktrace()}}]) + #jid{lserver = LServer} = xmpp:get_to(Packet), + case ejabberd_hooks:run_fold(sm_receive_packet, LServer, Packet, []) of + drop -> + ?DEBUG("hook dropped stanza:~n~s", [xmpp:pp(Packet)]); + Packet1 -> + try do_route(Packet1), ok + catch E:R -> + ?ERROR_MSG("failed to route packet:~n~s~nReason = ~p", + [xmpp:pp(Packet1), + {E, {R, erlang:get_stacktrace()}}]) + end end. -spec open_session(sid(), binary(), binary(), binary(), prio(), info()) -> ok. diff --git a/src/ejabberd_sm_sql.erl b/src/ejabberd_sm_sql.erl index 2b94064ef..55e21040b 100644 --- a/src/ejabberd_sm_sql.erl +++ b/src/ejabberd_sm_sql.erl @@ -74,6 +74,7 @@ set_session(#session{sid = {Now, Pid}, usr = {U, LServer, R}, "!pid=%(PidS)s", "node=%(Node)s", "username=%(U)s", + "server_host=%(LServer)s", "resource=%(R)s", "priority=%(PrioS)s", "info=%(InfoS)s"]) of @@ -107,7 +108,8 @@ get_sessions(LServer) -> case ejabberd_sql:sql_query( LServer, ?SQL("select @(usec)d, @(pid)s, @(node)s, @(username)s," - " @(resource)s, @(priority)s, @(info)s from sm")) of + " @(resource)s, @(priority)s, @(info)s from sm" + " where %(LServer)H")) of {selected, Rows} -> lists:flatmap( fun(Row) -> @@ -125,7 +127,7 @@ get_sessions(LUser, LServer) -> LServer, ?SQL("select @(usec)d, @(pid)s, @(node)s, @(username)s," " @(resource)s, @(priority)s, @(info)s from sm" - " where username=%(LUser)s")) of + " where username=%(LUser)s and %(LServer)H")) of {selected, Rows} -> {ok, lists:flatmap( fun(Row) -> diff --git a/src/ejabberd_sql_pt.erl b/src/ejabberd_sql_pt.erl index e90947a5f..f59e0abe4 100644 --- a/src/ejabberd_sql_pt.erl +++ b/src/ejabberd_sql_pt.erl @@ -26,7 +26,7 @@ -module(ejabberd_sql_pt). %% API --export([parse_transform/2]). +-export([parse_transform/2, format_error/1]). -export([parse/2]). @@ -39,7 +39,9 @@ args = [], res = [], res_vars = [], - res_pos = 0}). + res_pos = 0, + server_host_used = false, + used_vars = []}). -define(QUERY_RECORD, "sql_query"). @@ -48,6 +50,12 @@ -define(MOD, sql__module_). +-ifdef(NEW_SQL_SCHEMA). +-define(USE_NEW_SCHEMA, true). +-else. +-define(USE_NEW_SCHEMA, false). +-endif. + %%==================================================================== %% API %%==================================================================== @@ -57,11 +65,14 @@ %%-------------------------------------------------------------------- parse_transform(AST, _Options) -> %io:format("PT: ~p~nOpts: ~p~n", [AST, Options]), + put(warnings, []), NewAST = top_transform(AST), %io:format("NewPT: ~p~n", [NewAST]), - NewAST. + NewAST ++ get(warnings). +format_error(no_server_host) -> + "server_host field is not used". %%==================================================================== %% Internal functions @@ -80,7 +91,23 @@ transform(Form) -> S = erl_syntax:string_value(Arg), Pos = erl_syntax:get_pos(Arg), ParseRes = parse(S, Pos), - set_pos(make_sql_query(ParseRes), Pos); + UnusedVars = + case ParseRes#state.server_host_used of + {true, SHVar} -> + case ?USE_NEW_SCHEMA of + true -> []; + false -> [SHVar] + end; + false -> + add_warning( + Pos, no_server_host), + [] + end, + set_pos( + add_unused_vars( + make_sql_query(ParseRes), + UnusedVars), + Pos); _ -> throw({error, erl_syntax:get_pos(Form), "?SQL argument must be " @@ -101,8 +128,20 @@ transform(Form) -> parse_upsert( erl_syntax:list_elements(FieldsArg)), Pos = erl_syntax:get_pos(Form), + case lists:keymember( + "server_host", 1, ParseRes) of + true -> + ok; + false -> + add_warning(Pos, no_server_host) + end, + {ParseRes2, UnusedVars} = + filter_upsert_sh(Table, ParseRes), set_pos( - make_sql_upsert(Table, ParseRes, Pos), + add_unused_vars( + make_sql_upsert(Table, ParseRes2, Pos), + UnusedVars + ), Pos); _ -> throw({error, erl_syntax:get_pos(Form), @@ -113,6 +152,41 @@ transform(Form) -> throw({error, erl_syntax:get_pos(Form), "wrong number of ?SQL_UPSERT args"}) end; + {?SQL_INSERT_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_insert( + erl_syntax:list_elements(FieldsArg)), + Pos = erl_syntax:get_pos(Form), + case lists:keymember( + "server_host", 1, ParseRes) of + true -> + ok; + false -> + add_warning(Pos, no_server_host) + end, + {ParseRes2, UnusedVars} = + filter_upsert_sh(Table, ParseRes), + set_pos( + add_unused_vars( + make_sql_insert(Table, ParseRes2), + UnusedVars + ), + Pos); + _ -> + throw({error, erl_syntax:get_pos(Form), + "?SQL_INSERT arguments must be " + "a constant string and a list"}) + end; + _ -> + throw({error, erl_syntax:get_pos(Form), + "wrong number of ?SQL_INSERT args"}) + end; _ -> Form end; @@ -168,7 +242,7 @@ parse1([], Acc, State) -> }; parse1([$@, $( | S], Acc, State) -> State1 = append_string(lists:reverse(Acc), State), - {Name, Type, S1, State2} = parse_name(S, State1), + {Name, Type, S1, State2} = parse_name(S, false, State1), Var = "__V" ++ integer_to_list(State2#state.res_pos), EVar = erl_syntax:variable(Var), Convert = @@ -192,21 +266,46 @@ parse1([$@, $( | S], Acc, State) -> parse1(S1, [], State4); parse1([$%, $( | S], Acc, State) -> State1 = append_string(lists:reverse(Acc), State), - {Name, Type, S1, State2} = parse_name(S, State1), + {Name, Type, S1, State2} = parse_name(S, true, 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}, + case Type of + host -> + State3 = + State2#state{server_host_used = {true, Name}, + used_vars = [Name | State2#state.used_vars]}, + case ?USE_NEW_SCHEMA of + true -> + Convert = + erl_syntax:application( + erl_syntax:record_access( + erl_syntax:variable(?ESCAPE_VAR), + erl_syntax:atom(?ESCAPE_RECORD), + erl_syntax:atom(string)), + [erl_syntax:variable(Name)]), + State3#state{'query' = [{var, Var}, + {str, "server_host="} | + State3#state.'query'], + args = [Convert | State3#state.args], + params = [Var | State3#state.params], + param_pos = State3#state.param_pos + 1}; + false -> + append_string("0=0", State3) + end; + _ -> + 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)]), + State2#state{'query' = [{var, Var} | State2#state.'query'], + args = [Convert | State2#state.args], + params = [Var | State2#state.params], + param_pos = State2#state.param_pos + 1, + used_vars = [Name | State2#state.used_vars]} + end, parse1(S1, [], State4); parse1([C | S], Acc, State) -> parse1(S, [C | Acc], State). @@ -216,32 +315,33 @@ append_string([], State) -> append_string(S, State) -> State#state{query = [{str, S} | State#state.query]}. -parse_name(S, State) -> - parse_name(S, [], 0, State). +parse_name(S, IsArg, State) -> + parse_name(S, [], 0, IsArg, State). -parse_name([], _Acc, _Depth, State) -> +parse_name([], _Acc, _Depth, _IsArg, State) -> throw({error, State#state.loc, "expected ')', found end of string"}); -parse_name([$), T | S], Acc, 0, State) -> +parse_name([$), T | S], Acc, 0, IsArg, State) -> Type = case T of $d -> integer; $s -> string; $b -> boolean; + $H when IsArg -> host; _ -> throw({error, State#state.loc, ["unknown type specifier '", T, "'"]}) end, {lists:reverse(Acc), Type, S, State}; -parse_name([$)], _Acc, 0, State) -> +parse_name([$)], _Acc, 0, _IsArg, 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). +parse_name([$( = C | S], Acc, Depth, IsArg, State) -> + parse_name(S, [C | Acc], Depth + 1, IsArg, State); +parse_name([$) = C | S], Acc, Depth, IsArg, State) -> + parse_name(S, [C | Acc], Depth - 1, IsArg, State); +parse_name([C | S], Acc, Depth, IsArg, State) -> + parse_name(S, [C | Acc], Depth, IsArg, State). make_var(V) -> @@ -444,7 +544,7 @@ make_sql_upsert_insert(Table, ParseRes) -> join_states(Fields, ", "), #state{'query' = [{str, ") VALUES ("}]}, join_states(Vals, ", "), - #state{'query' = [{str, ")"}]} + #state{'query' = [{str, ");"}]} ]), State. @@ -498,6 +598,49 @@ check_upsert(ParseRes, Pos) -> ok. +parse_insert(Fields) -> + {Fs, _} = + lists:foldr( + fun(F, {Acc, Param}) -> + case erl_syntax:type(F) of + string -> + V = erl_syntax:string_value(F), + {_, _, State} = Res = + parse_insert_field( + V, Param, erl_syntax:get_pos(F)), + {[Res | Acc], State#state.param_pos}; + _ -> + throw({error, erl_syntax:get_pos(F), + "?SQL_INSERT field must be " + "a constant string"}) + end + end, {[], 0}, Fields), + Fs. + +parse_insert_field([$! | _S], _ParamPos, Loc) -> + throw({error, Loc, + "?SQL_INSERT fields must not start with \"!\""}); +parse_insert_field([$- | _S], _ParamPos, Loc) -> + throw({error, Loc, + "?SQL_INSERT fields must not start with \"-\""}); +parse_insert_field(S, ParamPos, Loc) -> + {Name, ParseState} = parse_insert_field1(S, [], ParamPos, Loc), + {Name, {true}, ParseState}. + +parse_insert_field1([], _Acc, _ParamPos, Loc) -> + throw({error, Loc, + "?SQL_INSERT fields must have the " + "following form: \"name=value\""}); +parse_insert_field1([$= | S], Acc, ParamPos, Loc) -> + {lists:reverse(Acc), parse(S, ParamPos, Loc)}; +parse_insert_field1([C | S], Acc, ParamPos, Loc) -> + parse_insert_field1(S, [C | Acc], ParamPos, Loc). + + +make_sql_insert(Table, ParseRes) -> + make_sql_query(make_sql_upsert_insert(Table, ParseRes)). + + concat_states(States) -> lists:foldr( fun(ST11, ST2) -> @@ -566,3 +709,40 @@ set_pos(Tree, Pos) -> _ -> Node end end, Tree). + +filter_upsert_sh(Table, ParseRes) -> + case ?USE_NEW_SCHEMA of + true -> + ParseRes; + false -> + lists:foldr( + fun({Field, _Match, ST} = P, {Acc, Vars}) -> + if + Field /= "server_host" orelse Table == "route" -> + {[P | Acc], Vars}; + true -> + {Acc, ST#state.used_vars ++ Vars} + end + end, {[], []}, ParseRes) + end. + +add_unused_vars(Tree, []) -> + Tree; +add_unused_vars(Tree, Vars) -> + erl_syntax:block_expr( + lists:map(fun erl_syntax:variable/1, Vars) ++ [Tree]). + +-ifdef(ENABLE_PT_WARNINGS). + +add_warning(Pos, Warning) -> + Marker = erl_syntax:revert( + erl_syntax:warning_marker({Pos, ?MODULE, Warning})), + put(warnings, [Marker | get(warnings)]), + ok. + +-else. + +add_warning(_Pos, _Warning) -> + ok. + +-endif. diff --git a/src/ejabberd_stun.erl b/src/ejabberd_stun.erl index 35a04ce45..8228a2577 100644 --- a/src/ejabberd_stun.erl +++ b/src/ejabberd_stun.erl @@ -20,8 +20,9 @@ %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - +%%% %%%------------------------------------------------------------------- + -module(ejabberd_stun). -protocol({rfc, 5766}). diff --git a/src/ejabberd_sup.erl b/src/ejabberd_sup.erl index 35527ebd7..463e7ea29 100644 --- a/src/ejabberd_sup.erl +++ b/src/ejabberd_sup.erl @@ -156,6 +156,8 @@ init([]) -> permanent, 5000, worker, [cyrsasl]}, PKIX = {ejabberd_pkix, {ejabberd_pkix, start_link, []}, permanent, 5000, worker, [ejabberd_pkix]}, + IQ = {ejabberd_iq, {ejabberd_iq, start_link, []}, + permanent, 5000, worker, [ejabberd_iq]}, {ok, {{one_for_one, 10, 1}, [Hooks, Cluster, @@ -180,6 +182,7 @@ init([]) -> SQLSupervisor, RiakSupervisor, RedisSupervisor, + IQ, Router, RouterMulticast, Local, diff --git a/src/ejabberd_web_admin.erl b/src/ejabberd_web_admin.erl index 464ea6bfd..b3d72c19b 100644 --- a/src/ejabberd_web_admin.erl +++ b/src/ejabberd_web_admin.erl @@ -372,311 +372,37 @@ get_base_path(Host, Node) -> %%%% css & images additions_js() -> - <<"\nfunction selectAll() {\n for(i=0;i<documen" - "t.forms[0].elements.length;i++)\n { " - "var e = document.forms[0].elements[i];\n " - " if(e.type == 'checkbox')\n { e.checked " - "= true; }\n }\n}\nfunction unSelectAll() " - "{\n for(i=0;i<document.forms[0].elements.len" - "gth;i++)\n { var e = document.forms[0].eleme" - "nts[i];\n if(e.type == 'checkbox')\n " - " { e.checked = false; }\n }\n}\n">>. + case misc:read_js("admin.js") of + {ok, JS} -> JS; + {error, _} -> <<>> + end. css(Host) -> - Base = get_base_path(Host, cluster), - <<"html,body {\n" - " margin: 0;\n" - " padding: 0;\n" - " height: 100%;\n" - " background: #f9f9f9;\n" - " font-family: sans-serif;\n" - "}\n" - "body {\n" - " min-width: 990px;\n" - "}\n" - "a {\n" - " text-decoration: none;\n" - " color: #3eaffa;\n" - "}\n" - "a:hover,\n" - "a:active {\n" - " text-decoration: underline;\n" - "}\n" - "#container {\n" - " position: relative;\n" - " padding: 0;\n" - " margin: 0 auto;\n" - " max-width: 1280px;\n" - " min-height: 100%;\n" - " height: 100%;\n" - " margin-bottom: -30px;\n" - " z-index: 1;\n" - "}\n" - "html>body #container {\n" - " height: auto;\n" - "}\n" - "#header h1 {\n" - " width: 100%;\n" - " height: 50px;\n" - " padding: 0;\n" - " margin: 0;\n" - " background-color: #49cbc1;\n" - "}\n" - "#header h1 a {\n" - " position: absolute;\n" - " top: 0;\n" - " left: 0;\n" - " width: 100%;\n" - " height: 50px;\n" - " padding: 0;\n" - " margin: 0;\n" - " background: url('",Base/binary,"logo.png') 10px center no-repeat transparent;\n" - " background-size: auto 25px;\n" - " display: block;\n" - " text-indent: -9999px;\n" - "}\n" - "#clearcopyright {\n" - " display: block;\n" - " width: 100%;\n" - " height: 30px;\n" - "}\n" - "#copyrightouter {\n" - " position: relative;\n" - " display: table;\n" - " width: 100%;\n" - " height: 30px;\n" - " z-index: 2;\n" - "}\n" - "#copyright {\n" - " display: table-cell;\n" - " vertical-align: bottom;\n" - " width: 100%;\n" - " height: 30px;\n" - "}\n" - "#copyright a {\n" - " font-weight: bold;\n" - " color: #fff;\n" - "}\n" - "#copyright p {\n" - " margin-left: 0;\n" - " margin-right: 0;\n" - " margin-top: 5px;\n" - " margin-bottom: 0;\n" - " padding-left: 0;\n" - " padding-right: 0;\n" - " padding-top: 5px;\n" - " padding-bottom: 5px;\n" - " width: 100%;\n" - " color: #fff;\n" - " background-color: #30353E;\n" - " font-size: 0.75em;\n" - " text-align: center;\n" - "}\n" - "#navigation {\n" - " display: inline-block;\n" - " vertical-align: top;\n" - " width: 30%;\n" - "}\n" - "#navigation ul {\n" - " padding: 0;\n" - " margin: 0;\n" - " width: 90%;\n" - " background: #fff;\n" - "}\n" - "#navigation ul li {\n" - " list-style: none;\n" - " margin: 0;\n" - "\n" - " border-bottom: 1px solid #f9f9f9;\n" - " text-align: left;\n" - "}\n" - "#navigation ul li a {\n" - " margin: 0;\n" - " display: inline-block;\n" - " padding: 10px;\n" - " color: #333;\n" - "}\n" - "ul li #navhead a, ul li #navheadsub a, ul li #navheadsubsub a {\n" - " font-size: 1.5em;\n" - " color: inherit;\n" - "}\n" - "#navitemsub {\n" - " border-left: 0.5em solid #424a55;\n" - "}\n" - "#navitemsubsub {\n" - " border-left: 2em solid #424a55;\n" - "}\n" - "#navheadsub,\n" - "#navheadsubsub {\n" - " padding-left: 0.5em;\n" - "}\n" - "#navhead,\n" - "#navheadsub,\n" - "#navheadsubsub {\n" - " border-top: 3px solid #49cbc1;\n" - " background: #424a55;\n" - " color: #fff;\n" - "}\n" - "#lastactivity li {\n" - " padding: 2px;\n" - " margin-bottom: -1px;\n" - "}\n" - "thead tr td {\n" - " background: #3eaffa;\n" - " color: #fff;\n" - "}\n" - "thead tr td a {\n" - " color: #fff;\n" - "}\n" - "td.copy {\n" - " text-align: center;\n" - "}\n" - "tr.head {\n" - " color: #fff;\n" - " background-color: #3b547a;\n" - " text-align: center;\n" - "}\n" - "tr.oddraw {\n" - " color: #412c75;\n" - " background-color: #ccd4df;\n" - " text-align: center;\n" - "}\n" - "tr.evenraw {\n" - " color: #412c75;\n" - " background-color: #dbe0e8;\n" - " text-align: center;\n" - "}\n" - "td.leftheader {\n" - " color: #412c75;\n" - " background-color: #ccccc1;\n" - " padding-left: 5px;\n" - " padding-top: 2px;\n" - " padding-bottom: 2px;\n" - " margin-top: 0px;\n" - " margin-bottom: 0px;\n" - "}\n" - "td.leftcontent {\n" - " color: #000044;\n" - " background-color: #e6e6df;\n" - " padding-left: 5px;\n" - " padding-right: 5px;\n" - " padding-top: 2px;\n" - " padding-bottom: 2px;\n" - " margin-top: 0px;\n" - " margin-bottom: 0px;\n" - "}\n" - "td.rightcontent {\n" - " color: #000044;\n" - " text-align: justify;\n" - " padding-left: 10px;\n" - " padding-right: 10px;\n" - " padding-bottom: 5px;\n" - "}\n" - "\n" - "h1 {\n" - " color: #000044;\n" - " padding-top: 2px;\n" - " padding-bottom: 2px;\n" - " margin-top: 0px;\n" - " margin-bottom: 0px;\n" - "}\n" - "h2 {\n" - " color: #000044;\n" - " text-align: center;\n" - " padding-top: 2px;\n" - " padding-bottom: 2px;\n" - " margin-top: 0px;\n" - " margin-bottom: 0px;\n" - "}\n" - "h3 {\n" - " color: #000044;\n" - " text-align: left;\n" - " padding-top: 20px;\n" - " padding-bottom: 2px;\n" - " margin-top: 0px;\n" - " margin-bottom: 0px;\n" - "}\n" - "#content ul {\n" - " padding-left: 1.1em;\n" - " margin-top: 1em;\n" - "}\n" - "#content ul li {\n" - " list-style-type: disc;\n" - " padding: 5px;\n" - "}\n" - "#content ul.nolistyle>li {\n" - " list-style-type: none;\n" - "}\n" - "#content {\n" - " display: inline-block;\n" - " vertical-align: top;\n" - " padding-top: 25px;\n" - " width: 70%;\n" - "}\n" - "div.guidelink,\n" - "p[dir=ltr] {\n" - " display: inline-block;\n" - " float: right;\n" - "\n" - " margin: 0;\n" - " margin-right: 1em;\n" - "}\n" - "div.guidelink a,\n" - "p[dir=ltr] a {\n" - " display: inline-block;\n" - " border-radius: 3px;\n" - " padding: 3px;\n" - "\n" - " background: #3eaffa;\n" - "\n" - " text-transform: uppercase;\n" - " font-size: 0.75em;\n" - " color: #fff;\n" - "}\n" - "table {\n" - " margin-top: 1em;\n" - "}\n" - "table tr td {\n" - " padding: 0.5em;\n" - "}\n" - "table tr:nth-child(odd) {\n" - " background: #fff;\n" - "}\n" - "table.withtextareas>tbody>tr>td {\n" - " vertical-align: top;\n" - "}\n" - "textarea {\n" - " margin-bottom: 1em;\n" - "}\n" - "input,\n" - "select {\n" - " font-size: 1em;\n" - "}\n" - "p.result {\n" - " border: 1px;\n" - " border-style: dashed;\n" - " border-color: #FE8A02;\n" - " padding: 1em;\n" - " margin-right: 1em;\n" - " background: #FFE3C9;\n" - "}\n" - "*.alignright {\n" - " text-align: right;\n" - "}">>. + case misc:read_css("admin.css") of + {ok, CSS} -> + Base = get_base_path(Host, cluster), + re:replace(CSS, <<"@BASE@">>, Base, [{return, binary}]); + {error, _} -> + <<>> + end. favicon() -> - base64:decode(<<"AAABAAEAEBAAAAEAIAAoBQAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAA1AwMAQwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwMARQUEA+oFAwCOBAQAaAQEAGkEBABpBAQAaQQEAGoFAgBcBAAAOQAAAAoAAAAAAAAAAAAAAAAAAAAAAAAAAAMDAEIHBgX/BwYF/wcGBf8HBgX/BwYF/wcGBf8HBgX/BwYF/wUFA/wEBAHOBQICXgAAAAAAAAAAAAAAAAAAAAADAwBCBwYF/wcGBf8HBgX/BwYF/wcGBf8HBgX/BwYF/wcGBf8HBgX/BwYF/wcGBf8DAwCUAAAABwAAAAAAAAAAAwMAQgcGBf8HBgX/BwYF/wcGBf8FBQPMBAAAaAQAAD8DAwNOAwMDlgUFA/QHBgX/BwYF/wQEAHkAAAAAAAAAAAMDAEIHBgX/BwYF/wcGBf8EBAGeAAAACAAAAAAAAAASAAAABQAAAAAFBQGxBwYF/wcGBf8FBAPvAAAAKAAAAAADAwBCBwYF/wcGBf8EBAHPAAAADQAAACEFBQGuBQQD8AUEAeEFBQGuBQQB9QcGBf8HBgX/BwYF/wQEAH8AAAAAAwMAQgcGBf8HBgX/BgQAbwAAAAADAwOXBQQB3gUFAdgFBQHZBQQB3QUFAdYFBAHhBQUD/gcGBf8EBAK8AAAAAAMDAEIHBgX/BwYF/wQAAD0AAAAAAAAABQAAAAEAAAABAAAAAQAAAAEAAAAFAAAAEQUFArwKBgX/BQMDxQAAAAADAwBCBwYF/wcGBf8DAwBKAAAAAwYDAFAGAwBVBgMAVAYDAFQFAgJZAAAALwAAAAAFBQGuCgYF/wUDA8QAAAAAAAAAKwUEA/QHBgX/AwMDlgAAAAAFAwOIBwYF/wcGBf8HBgX/BQQB5wAAADMAAAAWBQUD5wcGBf8EBAGbAAAAAAAAAAYFBAG9BwYF/wUFA/EDAABAAAAAAAMDA1QDAwOYBQUAhQAAACQAAAAABAQBnQcGBf8HBgX/AwMATQAAAAAAAAAAAwAAQwUFA/oHBgX/BQQB5QYDA1UAAAAAAAAAAAAAAAAAAAAXAwMAlwcGBf8HBgX/BQUBtQAAAAcAAAAAAAAAAAAAAAAEBABzBQUD/gcGBf8HBgX/BQMDyQQEAZwGBAGqBQQB5AcGBf8HBgX/BAQB0QAAACEAAAAAAAAAAAAAAAAAAAAAAAAAAAUFAmQFBAHlBwYF/wcGBf8HBgX/BwYF/wcGBf8FBQP+BQUBsAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHwUFA40FBAHrBwYF/wUFA/4FAwPGBgMAUgAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==">>). + case misc:read_img("favicon.png") of + {ok, ICO} -> ICO; + {error, _} -> <<>> + end. logo() -> - base64:decode(<<"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V4NgkhURIAJEgAgQASJQGgSWwNI7QYsRqG4ojdU0lAgQASJABIgAESACRIAIEAEiQAR0EYiiaAwobdmGC/8RdIyuluROBIgAESACRIAIEAEiQASIABEgAqVFAEFnmsB1J677Bmh8aYGj4USACBABIkAEiAARIAJEgAgQASJgB4EUgesduGayHe0ohQgQASJABIgAESACRIAIEAEiQARKj0CCwPUl1D2j9IARACJABIgAESACRIAIEAEiQASIABGwi0DMwPWfUW+4Xc0ojQgQASJABIgAESACRIAIEAEiQASIABAYIHDdhfMXEigiQASIABEgAkSACBABIkAEiAARIAK5IdAkcF2Hc+/NTTEKJgJEgAgQASJABIgAESACRIAIEAEiIAg0CFxX4fgsIkQEiAARIAJEgAgQASJABIgAESACRCB3BOoErqtx7IjcFaMCRIAIEAEiQASIABEgAkSACBABIkAEBIE+gesWfJ9NZIgAESACRIAIEAEiQASIABEgAkSACDiDQI/AdT/+/4AzilERIkAEiAARIAJEgAgQASJABIgAESACgkCPwPXviQgRIAJEgAgQASJABIgAESACRIAIEAHnEKgGrk/hc5BzylEhIkAEiAARIAJEgAgQASJABIgAESACCFhHgo4lEkSACBABIkAEiAARIAJEgAgQASJABIgAESACRIAIEAFjCPx/2P3JeG4VmJoAAAAASUVORK5CYII=">>). + case misc:read_img("admin-logo.png") of + {ok, Img} -> Img; + {error, _} -> <<>> + end. logo_fill() -> - base64:decode(<<"iVBORw0KGgoAAAANSUhEUgAAAAYAAAA3BAMAAADdxCZzA" - "AAAAXNSR0IArs4c6QAAAB5QTFRF1nYO/ooC/o4O/pIS/p" - "4q/q5K/rpq/sqM/tam/ubGzn/S/AAAAEFJREFUCNdlw0s" - "RwCAQBUE+gSRHLGABC1jAAhbWAhZwC+88XdXOXb4UlFAr" - "SmwN5ekdJY2BkudEec1QvrVQ/r3xOlK9HsTvertmAAAAA" - "ElFTkSuQmCC">>). + case misc:read_img("admin-logo-fill.png") of + {ok, Img} -> Img; + {error, _} -> <<>> + end. %%%================================== %%%% process_admin diff --git a/src/ejd2sql.erl b/src/ejd2sql.erl index c801eb973..79533421e 100644 --- a/src/ejd2sql.erl +++ b/src/ejd2sql.erl @@ -59,6 +59,7 @@ modules() -> mod_privacy, mod_private, mod_pubsub, + mod_push, mod_roster, mod_shared_roster, mod_vcard]. @@ -73,18 +74,28 @@ export(Server, Output) -> end, Modules), close_output(Output, IO). -export(Server, Output, Module) -> +export(Server, Output, Module1) -> + Module = case Module1 of + mod_pubsub -> pubsub_db; + _ -> Module1 + end, + SQLMod = gen_mod:db_mod(sql, Module), LServer = jid:nameprep(iolist_to_binary(Server)), IO = prepare_output(Output), lists:foreach( fun({Table, ConvertFun}) -> case export(LServer, Table, IO, ConvertFun) of {atomic, ok} -> ok; + {aborted, {no_exists, _}} -> + ?WARNING_MSG("Ignoring export for module ~s: " + "Mnesia table ~s doesn't exist (most likely " + "because the module is unused)", + [Module1, Table]); {aborted, Reason} -> ?ERROR_MSG("Failed export for module ~p and table ~p: ~p", [Module, Table, Reason]) end - end, Module:export(Server)), + end, SQLMod:export(Server)), close_output(Output, IO). delete(Server) -> diff --git a/src/gen_iq_handler.erl b/src/gen_iq_handler.erl index b815a1c19..d34db3588 100644 --- a/src/gen_iq_handler.erl +++ b/src/gen_iq_handler.erl @@ -164,7 +164,7 @@ process_iq(Module, Function, #iq{lang = Lang, sub_els = [El]} = IQ) -> end, Module:Function(IQ#iq{sub_els = [Pkt]}) catch error:{xmpp_codec, Why} -> - Txt = xmpp:format_error(Why), + Txt = xmpp:io_format_error(Why), xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)) end. diff --git a/src/misc.erl b/src/misc.erl index 32699e76b..80824f03e 100644 --- a/src/misc.erl +++ b/src/misc.erl @@ -33,7 +33,8 @@ atom_to_binary/1, binary_to_atom/1, tuple_to_binary/1, l2i/1, i2l/1, i2l/2, expr_to_term/1, term_to_expr/1, now_to_usec/1, usec_to_now/1, encode_pid/1, decode_pid/2, - compile_exprs/2, join_atoms/2, try_read_file/1]). + compile_exprs/2, join_atoms/2, try_read_file/1, have_eimp/0, + css_dir/0, img_dir/0, js_dir/0, read_css/1, read_img/1, read_js/1]). %% Deprecated functions -export([decode_base64/1, encode_base64/1]). @@ -213,6 +214,57 @@ try_read_file(Path) -> erlang:error(badarg) end. +-ifdef(GRAPHICS). +have_eimp() -> true. +-else. +have_eimp() -> false. +-endif. + +-spec css_dir() -> file:filename(). +css_dir() -> + case os:getenv("EJABBERD_CSS_PATH") of + false -> + case code:priv_dir(ejabberd) of + {error, _} -> filename:join(["priv", "css"]); + Path -> filename:join([Path, "css"]) + end; + Path -> Path + end. + +-spec img_dir() -> file:filename(). +img_dir() -> + case os:getenv("EJABBERD_IMG_PATH") of + false -> + case code:priv_dir(ejabberd) of + {error, _} -> filename:join(["priv", "img"]); + Path -> filename:join([Path, "img"]) + end; + Path -> Path + end. + +-spec js_dir() -> file:filename(). +js_dir() -> + case os:getenv("EJABBERD_JS_PATH") of + false -> + case code:priv_dir(ejabberd) of + {error, _} -> filename:join(["priv", "js"]); + Path -> filename:join([Path, "js"]) + end; + Path -> Path + end. + +-spec read_css(file:filename()) -> {ok, binary()} | {error, file:posix()}. +read_css(File) -> + read_file(filename:join(css_dir(), File)). + +-spec read_img(file:filename()) -> {ok, binary()} | {error, file:posix()}. +read_img(File) -> + read_file(filename:join(img_dir(), File)). + +-spec read_js(file:filename()) -> {ok, binary()} | {error, file:posix()}. +read_js(File) -> + read_file(filename:join(js_dir(), File)). + %%%=================================================================== %%% Internal functions %%%=================================================================== @@ -224,3 +276,14 @@ set_node_id(PidStr, NodeBin) -> [H|_] = string:tokens(ExtPidStr, "."), [_|T] = string:tokens(PidStr, "."), erlang:list_to_pid(string:join([H|T], ".")). + +-spec read_file(file:filename()) -> {ok, binary()} | {error, file:posix()}. +read_file(Path) -> + case file:read_file(Path) of + {ok, Data} -> + {ok, Data}; + {error, Why} = Err -> + ?ERROR_MSG("Failed to read file ~s: ~s", + [Path, file:format_error(Why)]), + Err + end. diff --git a/src/mod_admin_extra.erl b/src/mod_admin_extra.erl index 013c342d8..799f0079f 100644 --- a/src/mod_admin_extra.erl +++ b/src/mod_admin_extra.erl @@ -44,7 +44,7 @@ kick_session/4, status_num/2, status_num/1, status_list/2, status_list/1, connected_users_info/0, connected_users_vhost/1, set_presence/7, - get_presence/2, user_sessions_info/2, get_last/2, + get_presence/2, user_sessions_info/2, get_last/2, set_last/4, % Accounts set_password/3, check_password_hash/4, delete_old_users/1, @@ -93,8 +93,13 @@ start(_Host, _Opts) -> ejabberd_commands:register_commands(get_commands_spec()). -stop(_Host) -> - ejabberd_commands:unregister_commands(get_commands_spec()). +stop(Host) -> + case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of + false -> + ejabberd_commands:unregister_commands(get_commands_spec()); + true -> + ok + end. reload(_Host, _NewOpts, _OldOpts) -> ok. @@ -602,9 +607,9 @@ get_commands_spec() -> ]}}}, #ejabberd_commands{name = set_last, tags = [last], desc = "Set last activity information", - longdesc = "Timestamp is the seconds since" + longdesc = "Timestamp is the seconds since " "1970-01-01 00:00:00 UTC, for example: date +%s", - module = mod_last, function = store_last_info, + module = ?MODULE, function = set_last, args = [{user, binary}, {host, binary}, {timestamp, integer}, {status, binary}], args_example = [<<"user1">>,<<"myserver.com">>, 1500045311, <<"GoSleeping">>], args_desc = ["User name", "Server name", "Number of seconds since epoch", "Status message"], @@ -1437,6 +1442,12 @@ get_last(User, Server) -> end, {xmpp_util:encode_timestamp(Now), Status}. +set_last(User, Server, Timestamp, Status) -> + case mod_last:store_last_info(User, Server, Timestamp, Status) of + {ok, _} -> ok; + Error -> Error + end. + %%% %%% Private Storage %%% diff --git a/src/mod_admin_update_sql.erl b/src/mod_admin_update_sql.erl new file mode 100644 index 000000000..2f105d97d --- /dev/null +++ b/src/mod_admin_update_sql.erl @@ -0,0 +1,365 @@ +%%%------------------------------------------------------------------- +%%% File : mod_admin_update_sql.erl +%%% Author : Alexey Shchepin <alexey@process-one.net> +%%% Purpose : Convert SQL DB to the new format +%%% Created : 9 Aug 2017 by Alexey Shchepin <alexey@process-one.net> +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2017 ProcessOne +%%% +%%% This program is free software; you can redistribute it and/or +%%% modify it under the terms of the GNU General Public License as +%%% published by the Free Software Foundation; either version 2 of the +%%% License, or (at your option) any later version. +%%% +%%% This program is distributed in the hope that it will be useful, +%%% but WITHOUT ANY WARRANTY; without even the implied warranty of +%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +%%% General Public License for more details. +%%% +%%% You should have received a copy of the GNU General Public License along +%%% with this program; if not, write to the Free Software Foundation, Inc., +%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +%%% +%%%------------------------------------------------------------------- + +-module(mod_admin_update_sql). +-author('alexey@process-one.net'). + +-behaviour(gen_mod). + +-export([start/2, stop/1, reload/3, mod_opt_type/1, + get_commands_spec/0, depends/2]). + +% Commands API +-export([update_sql/0]). + + +-include("logger.hrl"). +-include("ejabberd.hrl"). +-include("ejabberd_commands.hrl"). +-include("xmpp.hrl"). +-include("ejabberd_sql_pt.hrl"). + +%%% +%%% gen_mod +%%% + +start(_Host, _Opts) -> + ejabberd_commands:register_commands(get_commands_spec()). + +stop(_Host) -> + ejabberd_commands:unregister_commands(get_commands_spec()). + +reload(_Host, _NewOpts, _OldOpts) -> + ok. + +depends(_Host, _Opts) -> + []. + +%%% +%%% Register commands +%%% + +get_commands_spec() -> + [#ejabberd_commands{name = update_sql, tags = [sql], + desc = "Convert SQL DB to the new format", + module = ?MODULE, function = update_sql, + args = [], + args_example = [], + args_desc = [], + result = {res, rescode}, + result_example = ok, + result_desc = "Status code: 0 on success, 1 otherwise"} + ]. + +update_sql() -> + lists:foreach( + fun(Host) -> + case ejabberd_sql_sup:get_pids(Host) of + [] -> + ok; + _ -> + update_sql(Host) + end + end, ?MYHOSTS), + ok. + +-record(state, {host :: binary(), + dbtype :: mysql | pgsql | sqlite | mssql | odbc, + escape}). + +update_sql(Host) -> + LHost = jid:nameprep(Host), + DBType = ejabberd_config:get_option({sql_type, LHost}, undefined), + IsSupported = + case DBType of + pgsql -> true; + _ -> false + end, + if + not IsSupported -> + io:format("Converting ~p DB is not supported~n", [DBType]), + error; + true -> + Escape = + case DBType of + mssql -> fun ejabberd_sql:standard_escape/1; + sqlite -> fun ejabberd_sql:standard_escape/1; + _ -> fun ejabberd_sql:escape/1 + end, + State = #state{host = LHost, + dbtype = DBType, + escape = Escape}, + update_tables(State) + end. + +update_tables(State) -> + add_sh_column(State, "users"), + drop_pkey(State, "users"), + add_pkey(State, "users", ["server_host", "username"]), + drop_sh_default(State, "users"), + + add_sh_column(State, "last"), + drop_pkey(State, "last"), + add_pkey(State, "last", ["server_host", "username"]), + drop_sh_default(State, "last"), + + add_sh_column(State, "rosterusers"), + drop_index(State, "i_rosteru_user_jid"), + drop_index(State, "i_rosteru_username"), + drop_index(State, "i_rosteru_jid"), + create_unique_index(State, "rosterusers", "i_rosteru_sh_user_jid", ["server_host", "username", "jid"]), + create_index(State, "rosterusers", "i_rosteru_sh_username", ["server_host", "username"]), + create_index(State, "rosterusers", "i_rosteru_sh_jid", ["server_host", "jid"]), + drop_sh_default(State, "rosterusers"), + + add_sh_column(State, "rostergroups"), + drop_index(State, "pk_rosterg_user_jid"), + create_index(State, "rostergroups", "i_rosterg_sh_user_jid", ["server_host", "username", "jid"]), + drop_sh_default(State, "rostergroups"), + + add_sh_column(State, "sr_group"), + add_pkey(State, "sr_group", ["server_host", "name"]), + drop_sh_default(State, "sr_group"), + + add_sh_column(State, "sr_user"), + drop_index(State, "i_sr_user_jid_grp"), + drop_index(State, "i_sr_user_jid"), + drop_index(State, "i_sr_user_grp"), + add_pkey(State, "sr_user", ["server_host", "jid", "grp"]), + create_index(State, "sr_user", "i_sr_user_sh_jid", ["server_host", "jid"]), + create_index(State, "sr_user", "i_sr_user_sh_grp", ["server_host", "grp"]), + drop_sh_default(State, "sr_user"), + + add_sh_column(State, "spool"), + drop_index(State, "i_despool"), + create_index(State, "spool", "i_spool_sh_username", ["server_host", "username"]), + drop_sh_default(State, "spool"), + + add_sh_column(State, "archive"), + drop_index(State, "i_username"), + drop_index(State, "i_username_timestamp"), + drop_index(State, "i_timestamp"), + drop_index(State, "i_peer"), + drop_index(State, "i_bare_peer"), + create_index(State, "archive", "i_archive_sh_username_timestamp", ["server_host", "username", "timestamp"]), + create_index(State, "archive", "i_archive_sh_timestamp", ["server_host", "timestamp"]), + create_index(State, "archive", "i_archive_sh_peer", ["server_host", "peer"]), + create_index(State, "archive", "i_archive_sh_bare_peer", ["server_host", "bare_peer"]), + drop_sh_default(State, "archive"), + + add_sh_column(State, "archive_prefs"), + drop_pkey(State, "archive_prefs"), + add_pkey(State, "archive_prefs", ["server_host", "username"]), + drop_sh_default(State, "archive_prefs"), + + add_sh_column(State, "vcard"), + drop_pkey(State, "vcard"), + add_pkey(State, "vcard", ["server_host", "username"]), + drop_sh_default(State, "vcard"), + + add_sh_column(State, "vcard_search"), + drop_pkey(State, "vcard_search"), + drop_index(State, "i_vcard_search_lfn"), + drop_index(State, "i_vcard_search_lfamily"), + drop_index(State, "i_vcard_search_lgiven"), + drop_index(State, "i_vcard_search_lmiddle"), + drop_index(State, "i_vcard_search_lnickname"), + drop_index(State, "i_vcard_search_lbday"), + drop_index(State, "i_vcard_search_lctry"), + drop_index(State, "i_vcard_search_llocality"), + drop_index(State, "i_vcard_search_lemail"), + drop_index(State, "i_vcard_search_lorgname"), + drop_index(State, "i_vcard_search_lorgunit"), + add_pkey(State, "vcard_search", ["server_host", "username"]), + create_index(State, "vcard_search", "i_vcard_search_sh_lfn", ["server_host", "lfn"]), + create_index(State, "vcard_search", "i_vcard_search_sh_lfamily", ["server_host", "lfamily"]), + create_index(State, "vcard_search", "i_vcard_search_sh_lgiven", ["server_host", "lgiven"]), + create_index(State, "vcard_search", "i_vcard_search_sh_lmiddle", ["server_host", "lmiddle"]), + create_index(State, "vcard_search", "i_vcard_search_sh_lnickname", ["server_host", "lnickname"]), + create_index(State, "vcard_search", "i_vcard_search_sh_lbday", ["server_host", "lbday"]), + create_index(State, "vcard_search", "i_vcard_search_sh_lctry", ["server_host", "lctry"]), + create_index(State, "vcard_search", "i_vcard_search_sh_llocality", ["server_host", "llocality"]), + create_index(State, "vcard_search", "i_vcard_search_sh_lemail", ["server_host", "lemail"]), + create_index(State, "vcard_search", "i_vcard_search_sh_lorgname", ["server_host", "lorgname"]), + create_index(State, "vcard_search", "i_vcard_search_sh_lorgunit", ["server_host", "lorgunit"]), + drop_sh_default(State, "vcard_search"), + + add_sh_column(State, "privacy_default_list"), + drop_pkey(State, "privacy_default_list"), + add_pkey(State, "privacy_default_list", ["server_host", "username"]), + drop_sh_default(State, "privacy_default_list"), + + add_sh_column(State, "privacy_list"), + drop_index(State, "i_privacy_list_username"), + drop_index(State, "i_privacy_list_username_name"), + create_index(State, "privacy_list", "i_privacy_list_sh_username", ["server_host", "username"]), + create_unique_index(State, "privacy_list", "i_privacy_list_sh_username_name", ["server_host", "username", "name"]), + drop_sh_default(State, "privacy_list"), + + add_sh_column(State, "private_storage"), + drop_index(State, "i_private_storage_username"), + drop_index(State, "i_private_storage_username_namespace"), + add_pkey(State, "private_storage", ["server_host", "username", "namespace"]), + create_index(State, "private_storage", "i_private_storage_sh_username", ["server_host", "username"]), + drop_sh_default(State, "private_storage"), + + add_sh_column(State, "roster_version"), + drop_pkey(State, "roster_version"), + add_pkey(State, "roster_version", ["server_host", "username"]), + drop_sh_default(State, "roster_version"), + + add_sh_column(State, "muc_room"), + drop_sh_default(State, "muc_room"), + + add_sh_column(State, "muc_registered"), + drop_sh_default(State, "muc_registered"), + + add_sh_column(State, "muc_online_room"), + drop_sh_default(State, "muc_online_room"), + + add_sh_column(State, "muc_online_users"), + drop_sh_default(State, "muc_online_users"), + + add_sh_column(State, "irc_custom"), + drop_sh_default(State, "irc_custom"), + + add_sh_column(State, "motd"), + drop_pkey(State, "motd"), + add_pkey(State, "motd", ["server_host", "username"]), + drop_sh_default(State, "motd"), + + add_sh_column(State, "sm"), + drop_index(State, "i_sm_sid"), + drop_index(State, "i_sm_username"), + add_pkey(State, "sm", ["usec", "pid"]), + create_index(State, "sm", "i_sm_sh_username", ["server_host", "username"]), + drop_sh_default(State, "sm"), + + add_sh_column(State, "carboncopy"), + drop_index(State, "i_carboncopy_ur"), + drop_index(State, "i_carboncopy_user"), + add_pkey(State, "carboncopy", ["server_host", "username", "resource"]), + create_index(State, "carboncopy", "i_carboncopy_sh_user", ["server_host", "username"]), + drop_sh_default(State, "carboncopy"), + + add_sh_column(State, "push_session"), + drop_index(State, "i_push_usn"), + drop_index(State, "i_push_ut"), + add_pkey(State, "push_session", ["server_host", "username", "timestamp"]), + create_index(State, "push_session", "i_push_session_susn", ["server_host", "username", "service", "node"]), + drop_sh_default(State, "push_session"), + + ok. + +add_sh_column(#state{dbtype = pgsql} = State, Table) -> + sql_query( + State#state.host, + ["ALTER TABLE ", Table, " ADD COLUMN server_host text NOT NULL DEFAULT '", + (State#state.escape)(State#state.host), + "';"]); +add_sh_column(#state{dbtype = mysql} = State, Table) -> + sql_query( + State#state.host, + ["ALTER TABLE ", Table, " ADD COLUMN server_host text NOT NULL DEFAULT '", + (State#state.escape)(State#state.host), + "';"]). + +drop_pkey(#state{dbtype = pgsql} = State, Table) -> + sql_query( + State#state.host, + ["ALTER TABLE ", Table, " DROP CONSTRAINT ", Table, "_pkey;"]); +drop_pkey(#state{dbtype = mysql} = State, Table) -> + sql_query( + State#state.host, + ["ALTER TABLE ", Table, " DROP PRIMARY KEY;"]). + +add_pkey(#state{dbtype = pgsql} = State, Table, Cols) -> + SCols = string:join(Cols, ", "), + sql_query( + State#state.host, + ["ALTER TABLE ", Table, " ADD PRIMARY KEY (", SCols, ");"]); +add_pkey(#state{dbtype = mysql} = State, Table, Cols) -> + SCols = string:join(Cols, ", "), + sql_query( + State#state.host, + ["ALTER TABLE ", Table, " ADD PRIMARY KEY (", SCols, ");"]). + +drop_sh_default(#state{dbtype = pgsql} = State, Table) -> + sql_query( + State#state.host, + ["ALTER TABLE ", Table, " ALTER COLUMN server_host DROP DEFAULT;"]); +drop_sh_default(#state{dbtype = mysql} = State, Table) -> + sql_query( + State#state.host, + ["ALTER TABLE ", Table, " ALTER COLUMN server_host DROP DEFAULT;"]). + +drop_index(#state{dbtype = pgsql} = State, Index) -> + sql_query( + State#state.host, + ["DROP INDEX ", Index, ";"]); +drop_index(#state{dbtype = mysql} = State, Index) -> + sql_query( + State#state.host, + ["DROP INDEX ", Index, ";"]). + +create_unique_index(#state{dbtype = pgsql} = State, Table, Index, Cols) -> + SCols = string:join(Cols, ", "), + sql_query( + State#state.host, + ["CREATE UNIQUE INDEX ", Index, " ON ", Table, " USING btree (", + SCols, ");"]); +create_unique_index(#state{dbtype = mysql} = State, Table, Index, Cols) -> + Cols2 = [C ++ "(75)" || C <- Cols], + SCols = string:join(Cols2, ", "), + sql_query( + State#state.host, + ["CREATE UNIQUE INDEX ", Index, " ON ", Table, "(", + SCols, ");"]). + +create_index(#state{dbtype = pgsql} = State, Table, Index, Cols) -> + SCols = string:join(Cols, ", "), + sql_query( + State#state.host, + ["CREATE INDEX ", Index, " ON ", Table, " USING btree (", + SCols, ");"]); +create_index(#state{dbtype = mysql} = State, Table, Index, Cols) -> + Cols2 = [C ++ "(75)" || C <- Cols], + SCols = string:join(Cols2, ", "), + sql_query( + State#state.host, + ["CREATE INDEX ", Index, " ON ", Table, "(", + SCols, ");"]). + +sql_query(Host, Query) -> + io:format("executing \"~s\" on ~s~n", [Query, Host]), + case ejabberd_sql:sql_query(Host, Query) of + {error, Error} -> + io:format("error: ~p~n", [Error]), + ok; + _ -> + ok + end. + +mod_opt_type(_) -> []. diff --git a/src/mod_announce.erl b/src/mod_announce.erl index 39d68406f..b259aced9 100644 --- a/src/mod_announce.erl +++ b/src/mod_announce.erl @@ -237,7 +237,7 @@ disco_identity(Acc, _From, _To, Node, Lang) -> -define(INFO_RESULT(Allow, Feats, Lang), case Allow of deny -> - {error, xmpp:err_forbidden(<<"Denied by ACL">>, Lang)}; + {error, xmpp:err_forbidden(<<"Access denied by service policy">>, Lang)}; allow -> {result, Feats} end). @@ -252,7 +252,7 @@ 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} -> - Txt = <<"Denied by ACL">>, + Txt = <<"Access denied by service policy">>, {error, xmpp:err_forbidden(Txt, Lang)}; _ -> {result, []} @@ -303,7 +303,7 @@ disco_features(Acc, From, #jid{lserver = LServer} = _To, Node, Lang) -> -define(ITEMS_RESULT(Allow, Items, Lang), case Allow of deny -> - {error, xmpp:err_forbidden(<<"Denied by ACL">>, Lang)}; + {error, xmpp:err_forbidden(<<"Access denied by service policy">>, Lang)}; allow -> {result, Items} end). @@ -417,7 +417,7 @@ commands_result(Allow, From, To, Request) -> case Allow of deny -> Lang = Request#adhoc_command.lang, - {error, xmpp:err_forbidden(<<"Denied by ACL">>, Lang)}; + {error, xmpp:err_forbidden(<<"Access denied by service policy">>, Lang)}; allow -> announce_commands(From, To, Request) end. @@ -843,7 +843,7 @@ add_store_hint(El) -> -spec route_forbidden_error(stanza()) -> ok. route_forbidden_error(Packet) -> Lang = xmpp:get_lang(Packet), - Err = xmpp:err_forbidden(<<"Denied by ACL">>, Lang), + Err = xmpp:err_forbidden(<<"Access denied by service policy">>, Lang), ejabberd_router:route_error(Packet, Err). -spec init_cache(module(), binary(), gen_mod:opts()) -> ok. @@ -913,4 +913,12 @@ import(LServer, {sql, _}, DBType, Tab, List) -> mod_opt_type(access) -> fun acl:access_rules_validator/1; mod_opt_type(db_type) -> fun(T) -> ejabberd_config:v_db(?MODULE, T) end; -mod_opt_type(_) -> [access, db_type]. +mod_opt_type(O) when O == cache_life_time; O == cache_size -> + fun (I) when is_integer(I), I > 0 -> I; + (infinity) -> infinity + end; +mod_opt_type(O) when O == use_cache; O == cache_missed -> + fun (B) when is_boolean(B) -> B end; +mod_opt_type(_) -> + [access, db_type, cache_life_time, cache_size, + use_cache, cache_missed]. diff --git a/src/mod_announce_sql.erl b/src/mod_announce_sql.erl index 1dea0ba75..c5c9eb58f 100644 --- a/src/mod_announce_sql.erl +++ b/src/mod_announce_sql.erl @@ -51,6 +51,7 @@ set_motd_users(LServer, USRs) -> ?SQL_UPSERT_T( "motd", ["!username=%(U)s", + "!server_host=%(LServer)s", "xml=''"]) end, USRs) end, @@ -62,20 +63,23 @@ set_motd(LServer, Packet) -> ?SQL_UPSERT_T( "motd", ["!username=''", + "!server_host=%(LServer)s", "xml=%(XML)s"]) end, transaction(LServer, F). delete_motd(LServer) -> F = fun() -> - ejabberd_sql:sql_query_t(?SQL("delete from motd")) + ejabberd_sql:sql_query_t( + ?SQL("delete from motd where %(LServer)H")) end, transaction(LServer, F). get_motd(LServer) -> case catch ejabberd_sql:sql_query( LServer, - ?SQL("select @(xml)s from motd where username=''")) of + ?SQL("select @(xml)s from motd" + " where username='' and %(LServer)H")) of {selected, [{XML}]} -> parse_element(XML); {selected, []} -> @@ -88,7 +92,7 @@ is_motd_user(LUser, LServer) -> case catch ejabberd_sql:sql_query( LServer, ?SQL("select @(username)s from motd" - " where username=%(LUser)s")) of + " where username=%(LUser)s and %(LServer)H")) of {selected, [_|_]} -> {ok, true}; {selected, []} -> @@ -102,6 +106,7 @@ set_motd_user(LUser, LServer) -> ?SQL_UPSERT_T( "motd", ["!username=%(LUser)s", + "!server_host=%(LServer)s", "xml=''"]) end, transaction(LServer, F). @@ -111,16 +116,24 @@ export(_Server) -> fun(Host, #motd{server = LServer, packet = El}) when LServer == Host -> XML = fxml:element_to_binary(El), - [?SQL("delete from motd where username='';"), - ?SQL("insert into motd(username, xml) values ('', %(XML)s);")]; + [?SQL("delete from motd where username='' and %(LServer)H;"), + ?SQL_INSERT( + "motd", + ["username=''", + "server_host=%(LServer)s", + "xml=%(XML)s"])]; (_Host, _R) -> [] end}, {motd_users, fun(Host, #motd_users{us = {LUser, LServer}}) when LServer == Host, LUser /= <<"">> -> - [?SQL("delete from motd where username=%(LUser)s;"), - ?SQL("insert into motd(username, xml) values (%(LUser)s, '');")]; + [?SQL("delete from motd where username=%(LUser)s and %(LServer)H;"), + ?SQL_INSERT( + "motd", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "xml=''"])]; (_Host, _R) -> [] end}]. diff --git a/src/mod_avatar.erl b/src/mod_avatar.erl new file mode 100644 index 000000000..dde58abf1 --- /dev/null +++ b/src/mod_avatar.erl @@ -0,0 +1,450 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% Created : 13 Sep 2017 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2017 ProcessOne +%%% +%%% This program is free software; you can redistribute it and/or +%%% modify it under the terms of the GNU General Public License as +%%% published by the Free Software Foundation; either version 2 of the +%%% License, or (at your option) any later version. +%%% +%%% This program is distributed in the hope that it will be useful, +%%% but WITHOUT ANY WARRANTY; without even the implied warranty of +%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +%%% General Public License for more details. +%%% +%%% You should have received a copy of the GNU General Public License along +%%% with this program; if not, write to the Free Software Foundation, Inc., +%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +%%% +%%%------------------------------------------------------------------- +-module(mod_avatar). +-behaviour(gen_mod). + +%% gen_mod API +-export([start/2, stop/1, reload/3, depends/2, mod_opt_type/1]). +%% Hooks +-export([pubsub_publish_item/6, vcard_iq_convert/1, vcard_iq_publish/1]). + +-include("xmpp.hrl"). +-include("logger.hrl"). +-include("pubsub.hrl"). + +-type convert_rules() :: {default | eimp:img_type(), eimp:img_type()}. + +%%%=================================================================== +%%% API +%%%=================================================================== +start(Host, _Opts) -> + case misc:have_eimp() of + true -> + ejabberd_hooks:add(pubsub_publish_item, Host, ?MODULE, + pubsub_publish_item, 50), + ejabberd_hooks:add(vcard_iq_set, Host, ?MODULE, + vcard_iq_convert, 30), + ejabberd_hooks:add(vcard_iq_set, Host, ?MODULE, + vcard_iq_publish, 100); + false -> + ?CRITICAL_MSG("ejabberd is built without " + "graphics support: reconfigure it with " + "--enable-graphics or disable '~s'", + [?MODULE]), + {error, graphics_not_compiled} + end. + +stop(Host) -> + ejabberd_hooks:delete(pubsub_publish_item, Host, ?MODULE, + pubsub_publish_item, 50), + ejabberd_hooks:delete(vcard_iq_set, Host, ?MODULE, vcard_iq_convert, 30), + ejabberd_hooks:delete(vcard_iq_set, Host, ?MODULE, vcard_iq_publish, 100). + +reload(_Host, _NewOpts, _OldOpts) -> + ok. + +depends(_Host, _Opts) -> + [{mod_vcard, hard}, {mod_vcard_xupdate, hard}, {mod_pubsub, hard}]. + +%%%=================================================================== +%%% Hooks +%%%=================================================================== +pubsub_publish_item(LServer, ?NS_AVATAR_METADATA, + #jid{luser = LUser, lserver = LServer} = From, + #jid{luser = LUser, lserver = LServer} = Host, + ItemId, [Payload|_]) -> + try xmpp:decode(Payload) of + #avatar_meta{info = []} -> + delete_vcard_avatar(From); + #avatar_meta{info = Info} -> + Rules = get_converting_rules(LServer), + case get_meta_info(Info, Rules) of + #avatar_info{type = MimeType, id = ID, url = <<"">>} = I -> + case get_avatar_data(Host, ID) of + {ok, Data} -> + Meta = #avatar_meta{info = [I]}, + Photo = #vcard_photo{type = MimeType, + binval = Data}, + set_vcard_avatar(From, Photo, + #{avatar_meta => {ID, Meta}}); + {error, _} -> + ok + end; + #avatar_info{type = MimeType, url = URL} -> + Photo = #vcard_photo{type = MimeType, + extval = URL}, + set_vcard_avatar(From, Photo, #{}) + end; + _ -> + ?WARNING_MSG("invalid avatar metadata of ~s@~s published " + "with item id ~s", + [LUser, LServer, ItemId]) + catch _:{xmpp_codec, Why} -> + ?WARNING_MSG("failed to decode avatar metadata of ~s@~s: ~s", + [LUser, LServer, xmpp:format_error(Why)]) + end; +pubsub_publish_item(_, _, _, _, _, _) -> + ok. + +-spec vcard_iq_convert(iq()) -> iq() | {stop, stanza_error()}. +vcard_iq_convert(#iq{from = From, lang = Lang, sub_els = [VCard]} = IQ) -> + #jid{luser = LUser, lserver = LServer} = From, + case convert_avatar(LUser, LServer, VCard) of + {ok, MimeType, Data} -> + VCard1 = VCard#vcard_temp{ + photo = #vcard_photo{type = MimeType, + binval = Data}}, + IQ#iq{sub_els = [VCard1]}; + pass -> + IQ; + {error, Reason} -> + stop_with_error(Lang, Reason) + end; +vcard_iq_convert(Acc) -> + Acc. + +-spec vcard_iq_publish(iq()) -> iq() | {stop, stanza_error()}. +vcard_iq_publish(#iq{sub_els = [#vcard_temp{photo = undefined}]} = IQ) -> + publish_avatar(IQ, #avatar_meta{}, <<>>, <<>>, <<>>); +vcard_iq_publish(#iq{sub_els = [#vcard_temp{ + photo = #vcard_photo{ + type = MimeType, + binval = Data}}]} = IQ) + when is_binary(Data), Data /= <<>> -> + SHA1 = str:sha(Data), + M = get_avatar_meta(IQ), + case M of + {ok, SHA1, _} -> + IQ; + {ok, _ItemID, #avatar_meta{info = Info} = Meta} -> + case lists:keyfind(SHA1, #avatar_info.id, Info) of + #avatar_info{} -> + IQ; + false -> + Info1 = lists:filter( + fun(#avatar_info{url = URL}) -> URL /= <<"">> end, + Info), + Meta1 = Meta#avatar_meta{info = Info1}, + publish_avatar(IQ, Meta1, MimeType, Data, SHA1) + end; + {error, _} -> + publish_avatar(IQ, #avatar_meta{}, MimeType, Data, SHA1) + end; +vcard_iq_publish(Acc) -> + Acc. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +-spec get_meta_info([avatar_info()], convert_rules()) -> avatar_info(). +get_meta_info(Info, Rules) -> + case lists:foldl( + fun(_, #avatar_info{} = Acc) -> + Acc; + (#avatar_info{url = URL}, Acc) when URL /= <<"">> -> + Acc; + (#avatar_info{} = I, _) when Rules == [] -> + I; + (#avatar_info{type = MimeType} = I, Acc) -> + T = decode_mime_type(MimeType), + case lists:keymember(T, 2, Rules) of + true -> + I; + false -> + case convert_to_type(T, Rules) of + undefined -> + Acc; + _ -> + [I|Acc] + end + end + end, [], Info) of + #avatar_info{} = I -> I; + [] -> hd(Info); + Is -> hd(lists:reverse(Is)) + end. + +-spec get_avatar_data(jid(), binary()) -> {ok, binary()} | + {error, + notfound | invalid_data | internal_error}. +get_avatar_data(JID, ItemID) -> + {LUser, LServer, _} = LBJID = jid:remove_resource(jid:tolower(JID)), + case mod_pubsub:get_item(LBJID, ?NS_AVATAR_DATA, ItemID) of + #pubsub_item{payload = [Payload|_]} -> + try xmpp:decode(Payload) of + #avatar_data{data = Data} -> + {ok, Data}; + _ -> + ?WARNING_MSG("invalid avatar data detected " + "for ~s@~s with item id ~s", + [LUser, LServer, ItemID]), + {error, invalid_data} + catch _:{xmpp_codec, Why} -> + ?WARNING_MSG("failed to decode avatar data for " + "~s@~s with item id ~s: ~s", + [LUser, LServer, ItemID, + xmpp:format_error(Why)]), + {error, invalid_data} + end; + {error, #stanza_error{reason = 'item-not-found'}} -> + {error, notfound}; + {error, Reason} -> + ?WARNING_MSG("failed to get item for ~s@~s at node ~s " + "with item id ~s: ~p", + [LUser, LServer, ?NS_AVATAR_METADATA, ItemID, Reason]), + {error, internal_error} + end. + +-spec get_avatar_meta(iq()) -> {ok, binary(), avatar_meta()} | + {error, + notfound | invalid_metadata | internal_error}. +get_avatar_meta(#iq{meta = #{avatar_meta := {ItemID, Meta}}}) -> + {ok, ItemID, Meta}; +get_avatar_meta(#iq{from = JID}) -> + {LUser, LServer, _} = LBJID = jid:remove_resource(jid:tolower(JID)), + case mod_pubsub:get_items(LBJID, ?NS_AVATAR_METADATA) of + [#pubsub_item{itemid = {ItemID, _}, payload = [Payload|_]}|_] -> + try xmpp:decode(Payload) of + #avatar_meta{} = Meta -> + {ok, ItemID, Meta}; + _ -> + ?WARNING_MSG("invalid metadata payload detected " + "for ~s@~s with item id ~s", + [LUser, LServer, ItemID]), + {error, invalid_metadata} + catch _:{xmpp_codec, Why} -> + ?WARNING_MSG("failed to decode metadata for " + "~s@~s with item id ~s: ~s", + [LUser, LServer, ItemID, + xmpp:format_error(Why)]), + {error, invalid_metadata} + end; + {error, #stanza_error{reason = 'item-not-found'}} -> + {error, notfound}; + {error, Reason} -> + ?WARNING_MSG("failed to get items for ~s@~s at node ~s: ~p", + [LUser, LServer, ?NS_AVATAR_METADATA, Reason]), + {error, internal_error} + end. + +-spec publish_avatar(iq(), avatar_meta(), binary(), binary(), binary()) -> + iq() | {stop, stanza_error()}. +publish_avatar(#iq{from = JID} = IQ, Meta, <<>>, <<>>, <<>>) -> + {_, LServer, _} = LBJID = jid:remove_resource(jid:tolower(JID)), + case mod_pubsub:publish_item( + LBJID, LServer, ?NS_AVATAR_METADATA, + JID, <<>>, [xmpp:encode(Meta)]) of + {result, _} -> + IQ; + {error, StanzaErr} -> + {stop, StanzaErr} + end; +publish_avatar(#iq{from = JID} = IQ, Meta, MimeType, Data, ItemID) -> + #avatar_meta{info = Info} = Meta, + {_, LServer, _} = LBJID = jid:remove_resource(jid:tolower(JID)), + Payload = xmpp:encode(#avatar_data{data = Data}), + case mod_pubsub:publish_item( + LBJID, LServer, ?NS_AVATAR_DATA, + JID, ItemID, [Payload]) of + {result, _} -> + {W, H} = case eimp:identify(Data) of + {ok, ImgInfo} -> + {proplists:get_value(width, ImgInfo), + proplists:get_value(height, ImgInfo)}; + _ -> + {undefined, undefined} + end, + I = #avatar_info{id = ItemID, + width = W, + height = H, + type = MimeType, + bytes = size(Data)}, + Meta1 = Meta#avatar_meta{info = [I|Info]}, + case mod_pubsub:publish_item( + LBJID, LServer, ?NS_AVATAR_METADATA, + JID, ItemID, [xmpp:encode(Meta1)]) of + {result, _} -> + IQ; + {error, StanzaErr} -> + ?ERROR_MSG("Failed to publish avatar metadata for ~s: ~p", + [jid:encode(JID), StanzaErr]), + {stop, StanzaErr} + end; + {error, StanzaErr} -> + ?ERROR_MSG("Failed to publish avatar data for ~s: ~p", + [jid:encode(JID), StanzaErr]), + {stop, StanzaErr} + end. + +-spec convert_avatar(binary(), binary(), vcard_temp()) -> + {ok, binary(), binary()} | + {error, eimp:error_reason() | base64_error} | + pass. +convert_avatar(LUser, LServer, VCard) -> + case get_converting_rules(LServer) of + [] -> + pass; + Rules -> + case VCard#vcard_temp.photo of + #vcard_photo{binval = Data} when is_binary(Data) -> + convert_avatar(LUser, LServer, Data, Rules); + _ -> + pass + end + end. + +-spec convert_avatar(binary(), binary(), binary(), convert_rules()) -> + {ok, eimp:img_type(), binary()} | + {error, eimp:error_reason()} | + pass. +convert_avatar(LUser, LServer, Data, Rules) -> + Type = get_type(Data), + NewType = convert_to_type(Type, Rules), + if NewType == undefined orelse Type == NewType -> + pass; + true -> + ?DEBUG("Converting avatar of ~s@~s: ~s -> ~s", + [LUser, LServer, Type, NewType]), + case eimp:convert(Data, NewType) of + {ok, NewData} -> + {ok, encode_mime_type(NewType), NewData}; + {error, Reason} = Err -> + ?ERROR_MSG("Failed to convert avatar of " + "~s@~s (~s -> ~s): ~s", + [LUser, LServer, Type, NewType, + eimp:format_error(Reason)]), + Err + end + end. + +-spec set_vcard_avatar(jid(), vcard_photo() | undefined, map()) -> ok. +set_vcard_avatar(JID, VCardPhoto, Meta) -> + case get_vcard(JID) of + {ok, #vcard_temp{photo = VCardPhoto}} -> + ok; + {ok, VCard} -> + VCard1 = VCard#vcard_temp{photo = VCardPhoto}, + IQ = #iq{from = JID, to = JID, id = randoms:get_string(), + type = set, sub_els = [VCard1], meta = Meta}, + LServer = JID#jid.lserver, + ejabberd_hooks:run_fold(vcard_iq_set, LServer, IQ, []), + ok; + {error, _} -> + ok + end. + +-spec delete_vcard_avatar(jid()) -> ok. +delete_vcard_avatar(JID) -> + set_vcard_avatar(JID, undefined, #{}). + +-spec get_vcard(jid()) -> {ok, vcard_temp()} | {error, invalid_vcard}. +get_vcard(#jid{luser = LUser, lserver = LServer}) -> + VCardEl = case mod_vcard:get_vcard(LUser, LServer) of + [El] -> El; + _ -> #vcard_temp{} + end, + try xmpp:decode(VCardEl, ?NS_VCARD, []) of + #vcard_temp{} = VCard -> + {ok, VCard}; + _ -> + ?ERROR_MSG("invalid vCard of ~s@~s in the database", + [LUser, LServer]), + {error, invalid_vcard} + catch _:{xmpp_codec, Why} -> + ?ERROR_MSG("failed to decode vCard of ~s@~s: ~s", + [LUser, LServer, xmpp:format_error(Why)]), + {error, invalid_vcard} + end. + +-spec stop_with_error(binary(), eimp:error_reason()) -> + {stop, stanza_error()}. +stop_with_error(Lang, Reason) -> + Txt = eimp:format_error(Reason), + {stop, xmpp:err_internal_server_error(Txt, Lang)}. + +-spec get_converting_rules(binary()) -> convert_rules(). +get_converting_rules(LServer) -> + gen_mod:get_module_opt(LServer, ?MODULE, convert, []). + +-spec get_type(binary()) -> eimp:img_type() | unknown. +get_type(Data) -> + eimp:get_type(Data). + +-spec convert_to_type(eimp:img_type() | unknown, convert_rules()) -> + eimp:img_type() | undefined. +convert_to_type(unknown, _Rules) -> + undefined; +convert_to_type(Type, Rules) -> + case proplists:get_value(Type, Rules) of + undefined -> + proplists:get_value(default, Rules); + T -> + T + end. + +-spec decode_mime_type(binary()) -> eimp:img_type() | unknown. +decode_mime_type(MimeType) -> + case str:to_lower(MimeType) of + <<"image/jpeg">> -> jpeg; + <<"image/png">> -> png; + <<"image/webp">> -> webp; + <<"image/gif">> -> gif; + _ -> unknown + end. + +-spec encode_mime_type(eimp:img_type()) -> binary(). +encode_mime_type(Type) -> + <<"image/", (atom_to_binary(Type, latin1))/binary>>. + +mod_opt_type({convert, png}) -> + fun(jpeg) -> jpeg; + (webp) -> webp; + (gif) -> gif + end; +mod_opt_type({convert, webp}) -> + fun(jpeg) -> jpeg; + (png) -> png; + (gif) -> gif + end; +mod_opt_type({convert, jpeg}) -> + fun(png) -> png; + (webp) -> webp; + (gif) -> gif + end; +mod_opt_type({convert, gif}) -> + fun(png) -> png; + (jpeg) -> jpeg; + (webp) -> webp + end; +mod_opt_type({convert, default}) -> + fun(png) -> png; + (webp) -> webp; + (jpeg) -> jpeg; + (gif) -> gif + end; +mod_opt_type(_) -> + [{convert, default}, + {convert, webp}, + {convert, png}, + {convert, gif}, + {convert, jpeg}]. diff --git a/src/mod_block_strangers.erl b/src/mod_block_strangers.erl index 49d79e043..b2c56f36b 100644 --- a/src/mod_block_strangers.erl +++ b/src/mod_block_strangers.erl @@ -32,7 +32,7 @@ -export([start/2, stop/1, reload/3, depends/2, mod_opt_type/1]). --export([filter_packet/1]). +-export([filter_packet/1, filter_offline_msg/1]). -include("xmpp.hrl"). -include("ejabberd.hrl"). @@ -43,60 +43,105 @@ start(Host, _Opts) -> ejabberd_hooks:add(user_receive_packet, Host, ?MODULE, filter_packet, 25), - ok. + ejabberd_hooks:add(offline_message_hook, Host, + ?MODULE, filter_offline_msg, 25). stop(Host) -> ejabberd_hooks:delete(user_receive_packet, Host, ?MODULE, filter_packet, 25), - ok. + ejabberd_hooks:delete(offline_message_hook, Host, + ?MODULE, filter_offline_msg, 25). reload(_Host, _NewOpts, _OldOpts) -> ok. -filter_packet({#message{} = Msg, State} = Acc) -> - From = xmpp:get_from(Msg), +filter_packet({#message{from = From} = Msg, State} = Acc) -> LFrom = jid:tolower(From), LBFrom = jid:remove_resource(LFrom), - #{pres_a := PresA, jid := JID, lserver := LServer} = State, + #{pres_a := PresA} = State, + case (?SETS):is_element(LFrom, PresA) + orelse (?SETS):is_element(LBFrom, PresA) + orelse sets_bare_member(LBFrom, PresA) of + false -> + case check_message(Msg) of + allow -> Acc; + deny -> {stop, {drop, State}} + end; + true -> + Acc + end; +filter_packet(Acc) -> + Acc. + +filter_offline_msg({_Action, #message{} = Msg} = Acc) -> + case check_message(Msg) of + allow -> Acc; + deny -> {stop, {drop, Msg}} + end. + +check_message(#message{from = From, to = To} = Msg) -> + LServer = To#jid.lserver, AllowLocalUsers = gen_mod:get_module_opt(LServer, ?MODULE, allow_local_users, true), case (Msg#message.body == [] andalso Msg#message.subject == []) - orelse (AllowLocalUsers andalso - ejabberd_router:is_my_route(From#jid.lserver)) - orelse (?SETS):is_element(LFrom, PresA) - orelse (?SETS):is_element(LBFrom, PresA) - orelse sets_bare_member(LBFrom, PresA) of + orelse ((AllowLocalUsers orelse From#jid.luser == <<"">>) andalso + ejabberd_router:is_my_host(From#jid.lserver)) of false -> - {Sub, _} = ejabberd_hooks:run_fold( - roster_get_jid_info, LServer, - {none, []}, [JID#jid.luser, LServer, From]), - case Sub of + case check_subscription(From, To) of none -> Drop = gen_mod:get_module_opt(LServer, ?MODULE, drop, true), Log = gen_mod:get_module_opt(LServer, ?MODULE, log, false), if Log -> - ?INFO_MSG("Drop packet: ~s", - [fxml:element_to_binary( - xmpp:encode(Msg, ?NS_CLIENT))]); + ?INFO_MSG("~s message from stranger ~s to ~s", + [if Drop -> "Dropping"; + true -> "Allow" + end, + jid:encode(From), jid:encode(To)]); true -> ok end, if Drop -> - {stop, {drop, State}}; + deny; true -> - Acc + allow end; - _ -> - Acc + some -> + allow end; true -> - Acc - end; -filter_packet(Acc) -> - Acc. + allow + end. + +-spec check_subscription(jid(), jid()) -> none | some. +check_subscription(From, To) -> + {LocalUser, LocalServer, _} = jid:tolower(To), + {RemoteUser, RemoteServer, _} = jid:tolower(From), + case ejabberd_hooks:run_fold( + roster_get_jid_info, LocalServer, + {none, []}, [LocalUser, LocalServer, From]) of + {none, _} when RemoteUser == <<"">> -> + none; + {none, _} -> + case gen_mod:get_module_opt(LocalServer, ?MODULE, + allow_transports, true) of + true -> + %% Check if the contact's server is in the roster + case ejabberd_hooks:run_fold( + roster_get_jid_info, LocalServer, + {none, []}, + [LocalUser, LocalServer, jid:make(RemoteServer)]) of + {none, _} -> none; + _ -> some + end; + false -> + none + end; + _ -> + some + end. sets_bare_member({U, S, <<"">>} = LBJID, Set) -> case ?SETS:next(sets_iterator_from(LBJID, Set)) of @@ -133,4 +178,6 @@ mod_opt_type(log) -> fun (B) when is_boolean(B) -> B end; mod_opt_type(allow_local_users) -> fun (B) when is_boolean(B) -> B end; -mod_opt_type(_) -> [drop, log, allow_local_users]. +mod_opt_type(allow_transports) -> + fun (B) when is_boolean(B) -> B end; +mod_opt_type(_) -> [drop, log, allow_local_users, allow_transports]. diff --git a/src/mod_bosh.erl b/src/mod_bosh.erl index ed12d569c..6ee580477 100644 --- a/src/mod_bosh.erl +++ b/src/mod_bosh.erl @@ -337,58 +337,16 @@ get_container_children(Heading) -> ]. get_style_cdata() -> - <<" - body { - margin: 0; - padding: 0; - font-family: sans-serif; - color: #fff; - } - h1 { - font-size: 3em; - color: #444; - } - p { - line-height: 1.5em; - color: #888; - } - a { - color: #fff; - } - a:hover, - a:active { - text-decoration: underline; - } - .container { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: #424A55; - background-image: -webkit-linear-gradient(270deg, rgba(48,52,62,0) 24%, #30353e 100%); - background-image: linear-gradient(-180deg, rgba(48,52,62,0) 24%, #30353e 100%); - } - .section { - padding: 3em; - } - .white.section { - background: #fff; - border-bottom: 4px solid #41AFCA; - } - .white.section a { - text-decoration: none; - color: #41AFCA; - } - .white.section a:hover, - .white.section a:active { - text-decoration: underline; - } - .block { - margin: 0 auto; - max-width: 900px; - width: 100%; - }">>. + case misc:read_css("bosh.css") of + {ok, Data} -> Data; + {error, _} -> <<>> + end. get_image_src() -> - <<"data:image/png;base64,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">>. + case misc:read_img("bosh-logo.png") of + {ok, Img} -> + B64Img = base64:encode(Img), + <<"data:image/png;base64,", B64Img/binary>>; + {error, _} -> + <<>> + end. diff --git a/src/mod_bosh_redis.erl b/src/mod_bosh_redis.erl index 3847befc0..70af2482b 100644 --- a/src/mod_bosh_redis.erl +++ b/src/mod_bosh_redis.erl @@ -1,11 +1,28 @@ -%%%------------------------------------------------------------------- -%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> -%%% @copyright (C) 2017, Evgeny Khramtsov -%%% @doc -%%% -%%% @end +%%%---------------------------------------------------------------------- +%%% File : mod_bosh_redis.erl +%%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% Purpose : %%% Created : 28 Mar 2017 by Evgeny Khramtsov <ekhramtsov@process-one.net> -%%%------------------------------------------------------------------- +%%% +%%% +%%% ejabberd, Copyright (C) 2017-2017 ProcessOne +%%% +%%% This program is free software; you can redistribute it and/or +%%% modify it under the terms of the GNU General Public License as +%%% published by the Free Software Foundation; either version 2 of the +%%% License, or (at your option) any later version. +%%% +%%% This program is distributed in the hope that it will be useful, +%%% but WITHOUT ANY WARRANTY; without even the implied warranty of +%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +%%% General Public License for more details. +%%% +%%% You should have received a copy of the GNU General Public License along +%%% with this program; if not, write to the Free Software Foundation, Inc., +%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +%%% +%%%---------------------------------------------------------------------- + -module(mod_bosh_redis). -behaviour(mod_bosh). -behaviour(gen_server). diff --git a/src/mod_bosh_sql.erl b/src/mod_bosh_sql.erl index 9c09a727b..621e9d317 100644 --- a/src/mod_bosh_sql.erl +++ b/src/mod_bosh_sql.erl @@ -1,11 +1,28 @@ -%%%------------------------------------------------------------------- -%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> -%%% @copyright (C) 2017, Evgeny Khramtsov -%%% @doc -%%% -%%% @end +%%%---------------------------------------------------------------------- +%%% File : mod_bosh_sql.erl +%%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% Purpose : %%% Created : 28 Mar 2017 by Evgeny Khramtsov <ekhramtsov@process-one.net> -%%%------------------------------------------------------------------- +%%% +%%% +%%% ejabberd, Copyright (C) 2017-2017 ProcessOne +%%% +%%% This program is free software; you can redistribute it and/or +%%% modify it under the terms of the GNU General Public License as +%%% published by the Free Software Foundation; either version 2 of the +%%% License, or (at your option) any later version. +%%% +%%% This program is distributed in the hope that it will be useful, +%%% but WITHOUT ANY WARRANTY; without even the implied warranty of +%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +%%% General Public License for more details. +%%% +%%% You should have received a copy of the GNU General Public License along +%%% with this program; if not, write to the Free Software Foundation, Inc., +%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +%%% +%%%---------------------------------------------------------------------- + -module(mod_bosh_sql). -behaviour(mod_bosh). diff --git a/src/mod_caps.erl b/src/mod_caps.erl index c34c81631..edc93bbf1 100644 --- a/src/mod_caps.erl +++ b/src/mod_caps.erl @@ -118,11 +118,11 @@ user_send_packet({#presence{type = available, from = #jid{luser = U, lserver = LServer} = From, to = #jid{luser = U, lserver = LServer, lresource = <<"">>}} = Pkt, - State}) -> + #{jid := To} = State}) -> case read_caps(Pkt) of nothing -> ok; #caps{version = Version, exts = Exts} = Caps -> - feature_request(LServer, From, Caps, [Version | Exts]) + feature_request(LServer, From, To, Caps, [Version | Exts]) end, {Pkt, State}; user_send_packet(Acc) -> @@ -130,13 +130,13 @@ user_send_packet(Acc) -> -spec user_receive_packet({stanza(), ejabberd_c2s:state()}) -> {stanza(), ejabberd_c2s:state()}. user_receive_packet({#presence{from = From, type = available} = Pkt, - #{lserver := LServer} = State}) -> + #{lserver := LServer, jid := To} = State}) -> IsRemote = not ejabberd_router:is_my_host(From#jid.lserver), if IsRemote -> case read_caps(Pkt) of nothing -> ok; #caps{version = Version, exts = Exts} = Caps -> - feature_request(LServer, From, Caps, [Version | Exts]) + feature_request(LServer, From, To, Caps, [Version | Exts]) end; true -> ok end, @@ -298,7 +298,12 @@ handle_call(_Req, _From, State) -> handle_cast(_Msg, State) -> {noreply, State}. -handle_info(_Info, State) -> {noreply, State}. +handle_info({iq_reply, IQReply, {Host, From, To, Caps, SubNodes}}, State) -> + feature_response(IQReply, Host, From, To, Caps, SubNodes), + {noreply, State}; +handle_info(Info, State) -> + ?WARNING_MSG("unexpected info: ~p", [Info]), + {noreply, State}. terminate(_Reason, State) -> Host = State#state.host, @@ -322,39 +327,37 @@ terminate(_Reason, State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. --spec feature_request(binary(), jid(), caps(), [binary()]) -> any(). -feature_request(Host, From, Caps, +-spec feature_request(binary(), jid(), jid(), caps(), [binary()]) -> any(). +feature_request(Host, From, To, Caps, [SubNode | Tail] = SubNodes) -> Node = Caps#caps.node, NodePair = {Node, SubNode}, case ets_cache:lookup(caps_features_cache, NodePair, caps_read_fun(Host, NodePair)) of {ok, Fs} when is_list(Fs) -> - feature_request(Host, From, Caps, Tail); + feature_request(Host, From, To, Caps, Tail); _ -> - LFrom = jid:tolower(From), - case ets_cache:insert_new(caps_requests_cache, {LFrom, NodePair}, ok) of + LTo = jid:tolower(To), + case ets_cache:insert_new(caps_requests_cache, {LTo, NodePair}, ok) of true -> IQ = #iq{type = get, - from = jid:make(Host), - to = From, + from = From, + to = To, sub_els = [#disco_info{node = <<Node/binary, "#", SubNode/binary>>}]}, - F = fun (IQReply) -> - feature_response(IQReply, Host, From, Caps, - SubNodes) - end, - ejabberd_local:route_iq(IQ, F); + ejabberd_router:route_iq( + IQ, {Host, From, To, Caps, SubNodes}, + gen_mod:get_module_proc(Host, ?MODULE)); false -> ok end, - feature_request(Host, From, Caps, Tail) + feature_request(Host, From, To, Caps, Tail) end; -feature_request(_Host, _From, _Caps, []) -> ok. +feature_request(_Host, _From, _To, _Caps, []) -> ok. --spec feature_response(iq(), binary(), ljid(), caps(), [binary()]) -> any(). +-spec feature_response(iq(), binary(), jid(), jid(), caps(), [binary()]) -> any(). feature_response(#iq{type = result, sub_els = [El]}, - Host, From, Caps, [SubNode | SubNodes]) -> + Host, From, To, Caps, [SubNode | SubNodes]) -> NodePair = {Caps#caps.node, SubNode}, try DiscoInfo = xmpp:decode(El), @@ -374,10 +377,10 @@ feature_response(#iq{type = result, sub_els = [El]}, catch _:{xmpp_codec, _Why} -> ok end, - feature_request(Host, From, Caps, SubNodes); -feature_response(_IQResult, Host, From, Caps, + feature_request(Host, From, To, Caps, SubNodes); +feature_response(_IQResult, Host, From, To, Caps, [_SubNode | SubNodes]) -> - feature_request(Host, From, Caps, SubNodes). + feature_request(Host, From, To, Caps, SubNodes). -spec caps_read_fun(binary(), {binary(), binary()}) -> fun(() -> {ok, [binary()] | non_neg_integer()} | error). diff --git a/src/mod_carboncopy_sql.erl b/src/mod_carboncopy_sql.erl index 3271d8a1c..1b8e1e111 100644 --- a/src/mod_carboncopy_sql.erl +++ b/src/mod_carboncopy_sql.erl @@ -42,6 +42,7 @@ enable(LUser, LServer, LResource, NS) -> NodeS = erlang:atom_to_binary(node(), latin1), case ?SQL_UPSERT(LServer, "carboncopy", ["!username=%(LUser)s", + "!server_host=%(LServer)s", "!resource=%(LResource)s", "namespace=%(NS)s", "node=%(NodeS)s"]) of @@ -56,7 +57,7 @@ disable(LUser, LServer, LResource) -> case ejabberd_sql:sql_query( LServer, ?SQL("delete from carboncopy where username=%(LUser)s " - "and resource=%(LResource)s")) of + "and %(LServer)H and resource=%(LResource)s")) of {updated, _} -> ok; Err -> @@ -68,7 +69,7 @@ list(LUser, LServer) -> case ejabberd_sql:sql_query( LServer, ?SQL("select @(resource)s, @(namespace)s, @(node)s from carboncopy " - "where username=%(LUser)s")) of + "where username=%(LUser)s and %(LServer)H")) of {selected, Rows} -> {ok, [{Resource, NS, binary_to_atom(Node, latin1)} || {Resource, NS, Node} <- Rows]}; diff --git a/src/mod_client_state.erl b/src/mod_client_state.erl index efe6a260f..f7adb1c67 100644 --- a/src/mod_client_state.erl +++ b/src/mod_client_state.erl @@ -187,7 +187,7 @@ unregister_hooks(Host) -> %%-------------------------------------------------------------------- -spec c2s_stream_started(c2s_state(), stream_start()) -> c2s_state(). c2s_stream_started(State, _) -> - State#{csi_state => active, csi_queue => queue_new()}. + init_csi_state(State). -spec c2s_authenticated_packet(c2s_state(), xmpp_element()) -> c2s_state(). c2s_authenticated_packet(C2SState, #csi{type = active}) -> @@ -265,7 +265,10 @@ filter_other({Stanza, #{jid := JID} = C2SState} = Acc) when ?is_stanza(Stanza) - Acc; _ -> ?DEBUG("Won't add stanza for ~s to CSI queue", [jid:encode(JID)]), - From = xmpp:get_from(Stanza), + From = case xmpp:get_from(Stanza) of + undefined -> JID; + F -> F + end, C2SState1 = dequeue_sender(From, C2SState), {Stanza, C2SState1} end; @@ -284,6 +287,10 @@ add_stream_feature(Features, Host) -> %%-------------------------------------------------------------------- %% Internal functions. %%-------------------------------------------------------------------- +-spec init_csi_state(c2s_state()) -> c2s_state(). +init_csi_state(C2SState) -> + C2SState#{csi_state => active, csi_queue => queue_new()}. + -spec enqueue_stanza(csi_type(), stanza(), c2s_state()) -> filter_acc(). enqueue_stanza(Type, Stanza, #{csi_state := inactive, csi_queue := Q} = C2SState) -> @@ -302,12 +309,18 @@ enqueue_stanza(_Type, Stanza, State) -> -spec dequeue_sender(jid(), c2s_state()) -> c2s_state(). dequeue_sender(#jid{luser = U, lserver = S} = Sender, - #{csi_queue := Q, jid := JID} = C2SState) -> - ?DEBUG("Flushing packets of ~s@~s from CSI queue of ~s", - [U, S, jid:encode(JID)]), - {Elems, Q1} = queue_take(Sender, Q), - C2SState1 = flush_stanzas(C2SState, Elems), - C2SState1#{csi_queue => Q1}. + #{jid := JID} = C2SState) -> + case maps:get(csi_queue, C2SState, undefined) of + undefined -> + %% This may happen when the module is (re)loaded in runtime + init_csi_state(C2SState); + Q -> + ?DEBUG("Flushing packets of ~s@~s from CSI queue of ~s", + [U, S, jid:encode(JID)]), + {Elems, Q1} = queue_take(Sender, Q), + C2SState1 = flush_stanzas(C2SState, Elems), + C2SState1#{csi_queue => Q1} + end. -spec flush_queue(c2s_state()) -> c2s_state(). flush_queue(#{csi_queue := Q, jid := JID} = C2SState) -> diff --git a/src/mod_configure.erl b/src/mod_configure.erl index 3bb9f2279..31f7a9c80 100644 --- a/src/mod_configure.erl +++ b/src/mod_configure.erl @@ -192,7 +192,7 @@ get_local_identity(Acc, _From, _To, Node, Lang) -> -define(INFO_RESULT(Allow, Feats, Lang), case Allow of - deny -> {error, xmpp:err_forbidden(<<"Denied by ACL">>, Lang)}; + deny -> {error, xmpp:err_forbidden(<<"Access denied by service policy">>, Lang)}; allow -> {result, Feats} end). @@ -310,7 +310,7 @@ get_sm_items(Acc, From, Items ++ Nodes ++ get_user_resources(User, Server)}; {allow, <<"config">>} -> {result, []}; {_, <<"config">>} -> - {error, xmpp:err_forbidden(<<"Denied by ACL">>, Lang)}; + {error, xmpp:err_forbidden(<<"Access denied by service policy">>, Lang)}; _ -> Acc end end. @@ -432,7 +432,7 @@ get_local_items(Acc, From, #jid{lserver = LServer} = To, _ -> LNode = tokenize(Node), Allow = acl:match_rule(LServer, configure, From), - Err = xmpp:err_forbidden(<<"Denied by ACL">>, Lang), + Err = xmpp:err_forbidden(<<"Access denied by service policy">>, Lang), case LNode of [<<"config">>] -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); @@ -765,7 +765,7 @@ get_stopped_nodes(_Lang) -> -define(COMMANDS_RESULT(LServerOrGlobal, From, To, Request, Lang), case acl:match_rule(LServerOrGlobal, configure, From) of - deny -> {error, xmpp:err_forbidden(<<"Denied by ACL">>, Lang)}; + deny -> {error, xmpp:err_forbidden(<<"Access denied by service policy">>, Lang)}; allow -> adhoc_local_commands(From, To, Request) end). @@ -1737,7 +1737,7 @@ adhoc_sm_commands(_Acc, From, action = Action, xdata = XData} = Request) -> case acl:match_rule(LServer, configure, From) of deny -> - {error, xmpp:err_forbidden(<<"Denied by ACL">>, Lang)}; + {error, xmpp:err_forbidden(<<"Access denied by service policy">>, Lang)}; allow -> ActionIsExecute = Action == execute orelse Action == complete, if Action == cancel -> diff --git a/src/mod_delegation.erl b/src/mod_delegation.erl index 865f8ebf4..27e00768d 100644 --- a/src/mod_delegation.erl +++ b/src/mod_delegation.erl @@ -47,6 +47,7 @@ -type disco_acc() :: {error, stanza_error()} | {result, [binary()]} | empty. -record(state, {server_host = <<"">> :: binary(), delegations = dict:new() :: ?TDICT}). +-type state() :: #state{}. %%%=================================================================== %%% API @@ -161,27 +162,6 @@ handle_cast({component_connected, Host}, State) -> end end, NSAttrsAccessList), {noreply, State}; -handle_cast({disco_info, Type, Host, NS, Info}, State) -> - From = jid:make(State#state.server_host), - To = jid:make(Host), - case dict:find({NS, Type}, State#state.delegations) of - error -> - Msg = #message{from = From, to = To, - sub_els = [#delegation{delegated = [#delegated{ns = NS}]}]}, - Delegations = dict:store({NS, Type}, {Host, Info}, State#state.delegations), - gen_iq_handler:add_iq_handler(Type, State#state.server_host, NS, - ?MODULE, Type, gen_iq_handler:iqdisc(Host)), - ejabberd_router:route(Msg), - ?INFO_MSG("Namespace '~s' is delegated to external component '~s'", - [NS, Host]), - {noreply, State#state{delegations = Delegations}}; - {ok, {AnotherHost, _}} -> - ?WARNING_MSG("Failed to delegate namespace '~s' to " - "external component '~s' because it's already " - "delegated to '~s'", - [NS, Host, AnotherHost]), - {noreply, State} - end; handle_cast({component_disconnected, Host}, State) -> ServerHost = State#state.server_host, Delegations = @@ -199,7 +179,24 @@ handle_cast({component_disconnected, Host}, State) -> handle_cast(_Msg, State) -> {noreply, State}. -handle_info(_Info, State) -> +handle_info({iq_reply, ResIQ, {disco_info, Type, Host, NS}}, State) -> + {noreply, + case ResIQ of + #iq{type = result, sub_els = [SubEl]} -> + try xmpp:decode(SubEl) of + #disco_info{} = Info -> + process_disco_info(State, Type, Host, NS, Info) + catch _:{xmpp_codec, _} -> + State + end; + _ -> + State + end}; +handle_info({iq_reply, ResIQ, #iq{} = IQ}, State) -> + process_iq_result(IQ, ResIQ), + {noreply, State}; +handle_info(Info, State) -> + ?WARNING_MSG("unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, State) -> @@ -246,12 +243,12 @@ process_iq(#iq{to = To, lang = Lang, sub_els = [SubEl]} = IQ, Type) -> forwarded = #forwarded{xml_els = [xmpp:encode(IQ)]}}, NewFrom = jid:make(LServer), NewTo = jid:make(Host), - ejabberd_local:route_iq( + ejabberd_router:route_iq( #iq{type = set, from = NewFrom, to = NewTo, sub_els = [Delegation]}, - fun(Result) -> process_iq_result(IQ, Result) end), + IQ, gen_mod:get_module_proc(LServer, ?MODULE)), ignore; error -> Txt = <<"Failed to map delegated namespace to external component">>, @@ -284,29 +281,41 @@ process_iq_result(#iq{lang = Lang} = IQ, timeout) -> Err = xmpp:err_internal_server_error(Txt, Lang), ejabberd_router:route_error(IQ, Err). +-spec process_disco_info(state(), ejabberd_local | ejabberd_sm, + binary(), binary(), disco_info()) -> state(). +process_disco_info(State, Type, Host, NS, Info) -> + From = jid:make(State#state.server_host), + To = jid:make(Host), + case dict:find({NS, Type}, State#state.delegations) of + error -> + Msg = #message{from = From, to = To, + sub_els = [#delegation{delegated = [#delegated{ns = NS}]}]}, + Delegations = dict:store({NS, Type}, {Host, Info}, State#state.delegations), + gen_iq_handler:add_iq_handler(Type, State#state.server_host, NS, + ?MODULE, Type, gen_iq_handler:iqdisc(Host)), + ejabberd_router:route(Msg), + ?INFO_MSG("Namespace '~s' is delegated to external component '~s'", + [NS, Host]), + State#state{delegations = Delegations}; + {ok, {AnotherHost, _}} -> + ?WARNING_MSG("Failed to delegate namespace '~s' to " + "external component '~s' because it's already " + "delegated to '~s'", + [NS, Host, AnotherHost]), + State + end. + -spec send_disco_queries(binary(), binary(), binary()) -> ok. send_disco_queries(LServer, Host, NS) -> From = jid:make(LServer), To = jid:make(Host), lists:foreach( fun({Type, Node}) -> - ejabberd_local:route_iq( + ejabberd_router:route_iq( #iq{type = get, from = From, to = To, sub_els = [#disco_info{node = Node}]}, - fun(#iq{type = result, sub_els = [SubEl]}) -> - try xmpp:decode(SubEl) of - #disco_info{} = Info-> - Proc = gen_mod:get_module_proc(LServer, ?MODULE), - gen_server:cast( - Proc, {disco_info, Type, Host, NS, Info}); - _ -> - ok - catch _:{xmpp_codec, _} -> - ok - end; - (_) -> - ok - end) + {disco_info, Type, Host, NS}, + gen_mod:get_module_proc(LServer, ?MODULE)) end, [{ejabberd_local, <<(?NS_DELEGATION)/binary, "::", NS/binary>>}, {ejabberd_sm, <<(?NS_DELEGATION)/binary, ":bare:", NS/binary>>}]). diff --git a/src/mod_fail2ban.erl b/src/mod_fail2ban.erl index b49773403..5e931853f 100644 --- a/src/mod_fail2ban.erl +++ b/src/mod_fail2ban.erl @@ -20,8 +20,9 @@ %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - +%%% %%%------------------------------------------------------------------- + -module(mod_fail2ban). -behaviour(gen_mod). @@ -103,7 +104,8 @@ c2s_stream_started(#{ip := {Addr, _}} = State, _) -> %% gen_mod callbacks %%==================================================================== start(Host, Opts) -> - catch ets:new(failed_auth, [named_table, public]), + catch ets:new(failed_auth, [named_table, public, + {heir, erlang:group_leader(), none}]), gen_mod:start_child(?MODULE, Host, Opts). stop(Host) -> diff --git a/src/mod_http_fileserver.erl b/src/mod_http_fileserver.erl index 4e3cfd08b..f34936724 100644 --- a/src/mod_http_fileserver.erl +++ b/src/mod_http_fileserver.erl @@ -66,6 +66,8 @@ {-1, 403, [], <<"Forbidden">>}). -define(HTTP_ERR_REQUEST_AUTH, {-1, 401, ?REQUEST_AUTH_HEADERS, <<"Unauthorized">>}). +-define(HTTP_ERR_HOST_UNKNOWN, + {-1, 410, [], <<"Host unknown">>}). -define(DEFAULT_CONTENT_TYPE, <<"application/octet-stream">>). @@ -178,10 +180,15 @@ check_docroot_defined(DocRoot, Host) -> end. check_docroot_exists(DocRoot) -> - case file:read_file_info(DocRoot) of - {error, Reason} -> - throw({error_access_docroot, DocRoot, Reason}); - {ok, FI} -> FI + case filelib:ensure_dir(filename:join(DocRoot, "foo")) of + ok -> + case file:read_file_info(DocRoot) of + {error, Reason} -> + throw({error_access_docroot, DocRoot, Reason}); + {ok, FI} -> FI + end; + {error, Reason} -> + throw({error_access_docroot, DocRoot, Reason}) end. check_docroot_is_dir(DRInfo, DocRoot) -> @@ -297,18 +304,22 @@ code_change(_OldVsn, State, _Extra) -> %% Returns the page to be sent back to the client and/or HTTP status code. process(LocalPath, #request{host = Host, auth = Auth, headers = RHeaders} = Request) -> ?DEBUG("Requested ~p", [LocalPath]), - try gen_server:call(get_proc_name(Host), {serve, LocalPath, Auth, RHeaders}) of - {FileSize, Code, Headers, Contents} -> - add_to_log(FileSize, Code, Request), - {Code, Headers, Contents} - catch - exit:{noproc, _} -> - ?ERROR_MSG("Received an HTTP request with Host ~p, but couldn't find the related " - "ejabberd virtual host", [Request#request.host]), - ejabberd_web:error(not_found) + try + VHost = ejabberd_router:host_of_route(Host), + {FileSize, Code, Headers, Contents} = + gen_server:call(get_proc_name(VHost), + {serve, LocalPath, Auth, RHeaders}), + add_to_log(FileSize, Code, Request#request{host = VHost}), + {Code, Headers, Contents} + catch _:{Why, _} when Why == noproc; Why == invalid_domain; Why == unregistered_route -> + ?DEBUG("Received an HTTP request with Host: ~s, " + "but couldn't find the related " + "ejabberd virtual host", [Host]), + {FileSize1, Code1, Headers1, Contents1} = ?HTTP_ERR_HOST_UNKNOWN, + add_to_log(FileSize1, Code1, Request#request{host = ?MYNAME}), + {Code1, Headers1, Contents1} end. - serve(LocalPath, Auth, DocRoot, DirectoryIndices, CustomHeaders, DefaultContentType, ContentTypes, UserAccess, IfModifiedSince) -> CanProceed = case {UserAccess, Auth} of @@ -424,9 +435,8 @@ add_to_log(File, FileSize, Code, Request) -> {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:local_time(), IP = ip_to_string(element(1, Request#request.ip)), Path = join(Request#request.path, "/"), - Query = case join(lists:map(fun(E) -> lists:concat([element(1, E), "=", binary_to_list(element(2, E))]) end, - Request#request.q), "&") of - [] -> + Query = case stringify_query(Request#request.q) of + <<"">> -> ""; String -> [$? | String] @@ -445,6 +455,15 @@ add_to_log(File, FileSize, Code, Request) -> [IP, Day, Month, Year, Hour, Minute, Second, Request#request.method, Path, Query, Code, FileSize, Referer, UserAgent]). +stringify_query(Q) -> + stringify_query(Q, []). +stringify_query([], Res) -> + join(lists:reverse(Res), "&"); +stringify_query([{nokey, _B} | Q], Res) -> + stringify_query(Q, Res); +stringify_query([{A, B} | Q], Res) -> + stringify_query(Q, [join([A,B], "=") | Res]). + find_header(Header, Headers, Default) -> case lists:keysearch(Header, 1, Headers) of {value, {_, Value}} -> Value; diff --git a/src/mod_http_upload.erl b/src/mod_http_upload.erl index 8d986d0d3..c3c295f66 100644 --- a/src/mod_http_upload.erl +++ b/src/mod_http_upload.erl @@ -107,10 +107,11 @@ get_url :: binary(), service_url :: binary() | undefined, thumbnail :: boolean(), + custom_headers :: [{binary(), binary()}], slots = #{} :: map()}). -record(media_info, - {type :: binary(), + {type :: atom(), height :: integer(), width :: integer()}). @@ -226,6 +227,7 @@ init([ServerHost, Opts]) -> GetURL = gen_mod:get_opt(get_url, Opts, PutURL), ServiceURL = gen_mod:get_opt(service_url, Opts), Thumbnail = gen_mod:get_opt(thumbnail, Opts, true), + CustomHeaders = gen_mod:get_opt(custom_headers, Opts, []), DocRoot1 = expand_home(str:strip(DocRoot, right, $/)), DocRoot2 = expand_host(DocRoot1, ServerHost), case DirMode of @@ -236,12 +238,14 @@ init([ServerHost, Opts]) -> end, case Thumbnail of true -> - case string:str(os:cmd("identify"), "Magick") of - 0 -> - ?ERROR_MSG("Cannot find 'identify' command, please install " - "ImageMagick or disable thumbnail creation", []); - _ -> - ok + case misc:have_eimp() of + false -> + ?ERROR_MSG("ejabberd is built without graphics support, " + "please rebuild it with --enable-graphics or " + "set 'thumbnail: false' for module '~s' in " + "ejabberd.yml", [?MODULE]); + _ -> + ok end; false -> ok @@ -258,7 +262,8 @@ init([ServerHost, Opts]) -> docroot = DocRoot2, put_url = expand_host(str:strip(PutURL, right, $/), ServerHost), get_url = expand_host(str:strip(GetURL, right, $/), ServerHost), - service_url = ServiceURL}}. + service_url = ServiceURL, + custom_headers = CustomHeaders}}. -spec handle_call(_, {pid(), _}, state()) -> {reply, {ok, pos_integer(), binary(), @@ -266,25 +271,30 @@ init([ServerHost, Opts]) -> pos_integer() | undefined}, state()} | {reply, {error, atom()}, state()} | {noreply, state()}. -handle_call({use_slot, Slot, Size}, _From, #state{file_mode = FileMode, - dir_mode = DirMode, - get_url = GetPrefix, - thumbnail = Thumbnail, - docroot = DocRoot} = State) -> +handle_call({use_slot, Slot, Size}, _From, + #state{file_mode = FileMode, + dir_mode = DirMode, + get_url = GetPrefix, + thumbnail = Thumbnail, + custom_headers = CustomHeaders, + docroot = DocRoot} = State) -> case get_slot(Slot, State) of {ok, {Size, Timer}} -> timer:cancel(Timer), NewState = del_slot(Slot, State), Path = str:join([DocRoot | Slot], <<$/>>), - {reply, {ok, Path, FileMode, DirMode, GetPrefix, Thumbnail}, + {reply, + {ok, Path, FileMode, DirMode, GetPrefix, Thumbnail, CustomHeaders}, NewState}; {ok, {_WrongSize, _Timer}} -> {reply, {error, size_mismatch}, State}; error -> {reply, {error, invalid_slot}, State} end; -handle_call(get_docroot, _From, #state{docroot = DocRoot} = State) -> - {reply, {ok, DocRoot}, State}; +handle_call(get_conf, _From, + #state{docroot = DocRoot, + custom_headers = CustomHeaders} = State) -> + {reply, {ok, DocRoot, CustomHeaders}, State}; handle_call(Request, From, State) -> ?ERROR_MSG("Got unexpected request from ~p: ~p", [From, Request]), {noreply, State}. @@ -353,44 +363,44 @@ process(LocalPath, #request{method = Method, host = Host, ip = IP}) Method == 'HEAD' -> ?DEBUG("Rejecting ~s request from ~s for ~s: Too few path components", [Method, ?ADDR_TO_STR(IP), Host]), - http_response(Host, 404); + http_response(404); process(_LocalPath, #request{method = 'PUT', host = Host, ip = IP, data = Data} = Request) -> {Proc, Slot} = parse_http_request(Request), case catch gen_server:call(Proc, {use_slot, Slot, byte_size(Data)}) of - {ok, Path, FileMode, DirMode, GetPrefix, Thumbnail} -> + {ok, Path, FileMode, DirMode, GetPrefix, Thumbnail, CustomHeaders} -> ?DEBUG("Storing file from ~s for ~s: ~s", [?ADDR_TO_STR(IP), Host, Path]), case store_file(Path, Data, FileMode, DirMode, GetPrefix, Slot, Thumbnail) of ok -> - http_response(Host, 201); + http_response(201, CustomHeaders); {ok, Headers, OutData} -> - http_response(Host, 201, Headers, OutData); + http_response(201, Headers ++ CustomHeaders, OutData); {error, Error} -> ?ERROR_MSG("Cannot store file ~s from ~s for ~s: ~p", [Path, ?ADDR_TO_STR(IP), Host, ?FORMAT(Error)]), - http_response(Host, 500) + http_response(500) end; {error, size_mismatch} -> ?INFO_MSG("Rejecting file from ~s for ~s: Unexpected size (~B)", [?ADDR_TO_STR(IP), Host, byte_size(Data)]), - http_response(Host, 413); + http_response(413); {error, invalid_slot} -> ?INFO_MSG("Rejecting file from ~s for ~s: Invalid slot", [?ADDR_TO_STR(IP), Host]), - http_response(Host, 403); + http_response(403); Error -> ?ERROR_MSG("Cannot handle PUT request from ~s for ~s: ~p", [?ADDR_TO_STR(IP), Host, Error]), - http_response(Host, 500) + http_response(500) end; process(_LocalPath, #request{method = Method, host = Host, ip = IP} = Request) when Method == 'GET'; Method == 'HEAD' -> {Proc, [_UserDir, _RandDir, FileName] = Slot} = parse_http_request(Request), - case catch gen_server:call(Proc, get_docroot) of - {ok, DocRoot} -> + case catch gen_server:call(Proc, get_conf) of + {ok, DocRoot, CustomHeaders} -> Path = str:join([DocRoot | Slot], <<$/>>), case file:read_file(Path) of {ok, Data} -> @@ -405,37 +415,47 @@ process(_LocalPath, #request{method = Method, host = Host, ip = IP} = Request) $", FileName/binary, $">>}] end, Headers2 = [{<<"Content-Type">>, ContentType} | Headers1], - http_response(Host, 200, Headers2, Data); + Headers3 = Headers2 ++ CustomHeaders, + http_response(200, Headers3, Data); {error, eacces} -> ?INFO_MSG("Cannot serve ~s to ~s: Permission denied", [Path, ?ADDR_TO_STR(IP)]), - http_response(Host, 403); + http_response(403); {error, enoent} -> ?INFO_MSG("Cannot serve ~s to ~s: No such file", [Path, ?ADDR_TO_STR(IP)]), - http_response(Host, 404); + http_response(404); {error, eisdir} -> ?INFO_MSG("Cannot serve ~s to ~s: Is a directory", [Path, ?ADDR_TO_STR(IP)]), - http_response(Host, 404); + http_response(404); {error, Error} -> ?INFO_MSG("Cannot serve ~s to ~s: ~s", [Path, ?ADDR_TO_STR(IP), ?FORMAT(Error)]), - http_response(Host, 500) + http_response(500) end; Error -> ?ERROR_MSG("Cannot handle ~s request from ~s for ~s: ~p", [Method, ?ADDR_TO_STR(IP), Host, Error]), - http_response(Host, 500) + http_response(500) end; -process(_LocalPath, #request{method = 'OPTIONS', host = Host, ip = IP}) -> +process(_LocalPath, #request{method = 'OPTIONS', host = Host, + ip = IP} = Request) -> ?DEBUG("Responding to OPTIONS request from ~s for ~s", [?ADDR_TO_STR(IP), Host]), - http_response(Host, 200); + {Proc, _Slot} = parse_http_request(Request), + case catch gen_server:call(Proc, get_conf) of + {ok, _DocRoot, CustomHeaders} -> + http_response(200, CustomHeaders); + Error -> + ?ERROR_MSG("Cannot handle OPTIONS request from ~s for ~s: ~p", + [?ADDR_TO_STR(IP), Host, Error]), + http_response(500) + end; process(_LocalPath, #request{method = Method, host = Host, ip = IP}) -> ?DEBUG("Rejecting ~s request from ~s for ~s", [Method, ?ADDR_TO_STR(IP), Host]), - http_response(Host, 405, [{<<"Allow">>, <<"OPTIONS, HEAD, GET, PUT">>}]). + http_response(405, [{<<"Allow">>, <<"OPTIONS, HEAD, GET, PUT">>}]). %%-------------------------------------------------------------------- %% Exported utility functions. @@ -522,7 +542,7 @@ process_slot_request(#iq{lang = Lang, from = From} = IQ, deny -> ?DEBUG("Denying HTTP upload slot request from ~s", [jid:encode(From)]), - Txt = <<"Denied by ACL">>, + Txt = <<"Access denied by service policy">>, xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)) end. @@ -726,15 +746,15 @@ parse_http_request(#request{host = Host, path = Path}) -> store_file(Path, Data, FileMode, DirMode, GetPrefix, Slot, Thumbnail) -> case do_store_file(Path, Data, FileMode, DirMode) of ok when Thumbnail -> - case identify(Path) of + case identify(Path, Data) of {ok, MediaInfo} -> - case convert(Path, MediaInfo) of - {ok, OutPath} -> + case convert(Path, Data, MediaInfo) of + {ok, OutPath, OutMediaInfo} -> [UserDir, RandDir | _] = Slot, FileName = filename:basename(OutPath), URL = str:join([GetPrefix, UserDir, RandDir, FileName], <<$/>>), - ThumbEl = thumb_el(OutPath, URL), + ThumbEl = thumb_el(OutMediaInfo, URL), {ok, [{<<"Content-Type">>, <<"text/xml; charset=utf-8">>}], @@ -790,30 +810,29 @@ guess_content_type(FileName) -> ?DEFAULT_CONTENT_TYPE, ?CONTENT_TYPES). --spec http_response(binary(), 100..599) +-spec http_response(100..599) -> {pos_integer(), [{binary(), binary()}], binary()}. -http_response(Host, Code) -> - http_response(Host, Code, []). +http_response(Code) -> + http_response(Code, []). --spec http_response(binary(), 100..599, [{binary(), binary()}]) +-spec http_response(100..599, [{binary(), binary()}]) -> {pos_integer(), [{binary(), binary()}], binary()}. -http_response(Host, Code, ExtraHeaders) -> +http_response(Code, ExtraHeaders) -> Message = <<(code_to_message(Code))/binary, $\n>>, - http_response(Host, Code, ExtraHeaders, Message). + http_response(Code, ExtraHeaders, Message). --spec http_response(binary(), 100..599, [{binary(), binary()}], binary()) +-spec http_response(100..599, [{binary(), binary()}], binary()) -> {pos_integer(), [{binary(), binary()}], binary()}. -http_response(Host, Code, ExtraHeaders, Body) -> - CustomHeaders = gen_mod:get_module_opt(Host, ?MODULE, custom_headers, []), +http_response(Code, ExtraHeaders, Body) -> Headers = case proplists:is_defined(<<"Content-Type">>, ExtraHeaders) of true -> ExtraHeaders; false -> [{<<"Content-Type">>, <<"text/plain">>} | ExtraHeaders] - end ++ CustomHeaders, + end, {Code, Headers, Body}. -spec code_to_message(100..599) -> binary(). @@ -830,59 +849,68 @@ code_to_message(_Code) -> <<"">>. %% Image manipulation stuff. %%-------------------------------------------------------------------- --spec identify(binary()) -> {ok, media_info()} | pass. - -identify(Path) -> - Cmd = io_lib:format("identify -format 'ok %m %h %w' ~s", [Path]), - Res = string:strip(os:cmd(Cmd), right, $\n), - case string:tokens(Res, " ") of - ["ok", T, H, W] -> - {ok, #media_info{type = list_to_binary(string:to_lower(T)), - height = list_to_integer(H), - width = list_to_integer(W)}}; - _ -> - ?DEBUG("Cannot identify type of ~s: ~s", [Path, Res]), +-spec identify(binary(), binary()) -> {ok, media_info()} | pass. + +identify(Path, Data) -> + case misc:have_eimp() of + true -> + case eimp:identify(Data) of + {ok, Info} -> + {ok, #media_info{ + type = proplists:get_value(type, Info), + width = proplists:get_value(width, Info), + height = proplists:get_value(height, Info)}}; + {error, Why} -> + ?DEBUG("Cannot identify type of ~s: ~s", + [Path, eimp:format_error(Why)]), + pass + end; + false -> pass end. --spec convert(binary(), media_info()) -> {ok, binary()} | pass. +-spec convert(binary(), binary(), media_info()) -> {ok, binary(), media_info()} | pass. -convert(Path, #media_info{type = T, width = W, height = H}) -> +convert(Path, Data, #media_info{type = T, width = W, height = H} = Info) -> if W * H >= 25000000 -> ?DEBUG("The image ~s is more than 25 Mpix", [Path]), pass; W =< 300, H =< 300 -> - {ok, Path}; - T == <<"gif">>; T == <<"jpeg">>; T == <<"png">>; T == <<"webp">> -> + {ok, Path, Info}; + true -> Dir = filename:dirname(Path), - FileName = <<(randoms:get_string())/binary, $., T/binary>>, + Ext = atom_to_binary(T, latin1), + FileName = <<(randoms:get_string())/binary, $., Ext/binary>>, OutPath = filename:join(Dir, FileName), - Cmd = io_lib:format("convert -resize 300 ~s ~s", [Path, OutPath]), - case os:cmd(Cmd) of - "" -> - {ok, OutPath}; - Err -> + {W1, H1} = if W > H -> {300, round(H*300/W)}; + H > W -> {round(W*300/H), 300}; + true -> {300, 300} + end, + OutInfo = #media_info{type = T, width = W1, height = H1}, + case eimp:convert(Data, T, [{scale, {W1, H1}}]) of + {ok, OutData} -> + case file:write_file(OutPath, OutData) of + ok -> + {ok, OutPath, OutInfo}; + {error, Why} -> + ?ERROR_MSG("Failed to write to ~s: ~s", + [OutPath, file:format_error(Why)]), + pass + end; + {error, Why} -> ?ERROR_MSG("Failed to convert ~s to ~s: ~s", - [Path, OutPath, string:strip(Err, right, $\n)]), + [Path, OutPath, eimp:format_error(Why)]), pass - end; - true -> - ?DEBUG("Won't call 'convert' for unknown type ~s", [T]), - pass + end end. --spec thumb_el(binary(), binary()) -> xmlel(). - -thumb_el(Path, URI) -> - ContentType = guess_content_type(Path), - xmpp:encode( - case identify(Path) of - {ok, #media_info{height = H, width = W}} -> - #thumbnail{'media-type' = ContentType, uri = URI, - height = H, width = W}; - pass -> - #thumbnail{uri = URI, 'media-type' = ContentType} - end). +-spec thumb_el(media_info(), binary()) -> xmlel(). + +thumb_el(#media_info{type = T, height = H, width = W}, URI) -> + MimeType = <<"image/", (atom_to_binary(T, latin1))/binary>>, + Thumb = #thumbnail{'media-type' = MimeType, uri = URI, + height = H, width = W}, + xmpp:encode(Thumb). %%-------------------------------------------------------------------- %% Remove user. diff --git a/src/mod_irc.erl b/src/mod_irc.erl index 04687ea67..92093507e 100644 --- a/src/mod_irc.erl +++ b/src/mod_irc.erl @@ -262,7 +262,7 @@ do_route(Host, ServerHost, Access, Packet) -> end; deny -> Lang = xmpp:get_lang(Packet), - Err = xmpp:err_forbidden(<<"Denied by ACL">>, Lang), + Err = xmpp:err_forbidden(<<"Access denied by service policy">>, Lang), ejabberd_router:route_error(Packet, Err) end. diff --git a/src/mod_irc_connection.erl b/src/mod_irc_connection.erl index b7b2f8e1d..593365910 100644 --- a/src/mod_irc_connection.erl +++ b/src/mod_irc_connection.erl @@ -418,7 +418,7 @@ handle_info({route_chan, Channel, Resource, end catch _:{xmpp_codec, Why} -> Err = xmpp:err_bad_request( - xmpp:format_error(Why), xmpp:get_lang(Packet)), + xmpp:io_format_error(Why), xmpp:get_lang(Packet)), ejabberd_router:route_error(Packet, Err) end, {next_state, StateName, StateData}; diff --git a/src/mod_irc_sql.erl b/src/mod_irc_sql.erl index f9a7d716f..1f8d7d16a 100644 --- a/src/mod_irc_sql.erl +++ b/src/mod_irc_sql.erl @@ -46,7 +46,7 @@ get_data(LServer, Host, From) -> case catch ejabberd_sql:sql_query( LServer, ?SQL("select @(data)s from irc_custom" - " where jid=%(SJID)s and host=%(Host)s")) of + " where jid=%(SJID)s and host=%(Host)s and %(LServer)H")) of {selected, [{SData}]} -> mod_irc:data_to_binary(From, ejabberd_sql:decode_term(SData)); {'EXIT', _} -> error; @@ -61,6 +61,7 @@ set_data(LServer, Host, From, Data) -> "irc_custom", ["!jid=%(SJID)s", "!host=%(Host)s", + "server_host=%(LServer)s", "data=%(SData)s"]), ok end, @@ -73,11 +74,16 @@ export(_Server) -> case str:suffix(Host, IRCHost) of true -> SJID = jid:encode(jid:make(U, S)), + LServer = ejabberd_router:host_of_route(IRCHost), SData = misc:term_to_expr(Data), [?SQL("delete from irc_custom" - " where jid=%(SJID)s and host=%(IRCHost)s;"), - ?SQL("insert into irc_custom(jid, host, data)" - " values (%(SJID)s, %(IRCHost)s, %(SData)s);")]; + " where jid=%(SJID)s and host=%(IRCHost)s and %(LServer)H;"), + ?SQL_INSERT( + "irc_custom", + ["jid=%(SJID)s", + "host=%(Host)s", + "server_host=%(LServer)s", + "data=%(SData)s"])]; false -> [] end diff --git a/src/mod_last_sql.erl b/src/mod_last_sql.erl index b777ba30d..f0889e4ec 100644 --- a/src/mod_last_sql.erl +++ b/src/mod_last_sql.erl @@ -46,7 +46,7 @@ get_last(LUser, LServer) -> case ejabberd_sql:sql_query( LServer, ?SQL("select @(seconds)d, @(state)s from last" - " where username=%(LUser)s")) of + " where username=%(LUser)s and %(LServer)H")) of {selected, []} -> error; {selected, [{TimeStamp, Status}]} -> @@ -60,6 +60,7 @@ get_last(LUser, LServer) -> store_last_info(LUser, LServer, TimeStamp, Status) -> case ?SQL_UPSERT(LServer, "last", ["!username=%(LUser)s", + "!server_host=%(LServer)s", "seconds=%(TimeStamp)d", "state=%(Status)s"]) of ok -> @@ -73,16 +74,19 @@ store_last_info(LUser, LServer, TimeStamp, Status) -> remove_user(LUser, LServer) -> ejabberd_sql:sql_query( LServer, - ?SQL("delete from last where username=%(LUser)s")). + ?SQL("delete from last where username=%(LUser)s and %(LServer)H")). export(_Server) -> [{last_activity, fun(Host, #last_activity{us = {LUser, LServer}, timestamp = TimeStamp, status = Status}) when LServer == Host -> - [?SQL("delete from last where username=%(LUser)s;"), - ?SQL("insert into last(username, seconds, state)" - " values (%(LUser)s, %(TimeStamp)d, %(Status)s);")]; + [?SQL("delete from last where username=%(LUser)s and %(LServer)H;"), + ?SQL_INSERT("last", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "seconds=%(TimeStamp)d", + "state=%(Status)s"])]; (_Host, _R) -> [] end}]. diff --git a/src/mod_legacy_auth.erl b/src/mod_legacy_auth.erl index 5a4ff9108..722a05738 100644 --- a/src/mod_legacy_auth.erl +++ b/src/mod_legacy_auth.erl @@ -133,7 +133,7 @@ authenticate(#{stream_id := StreamID, server := Server, Err = xmpp:make_error(IQ, xmpp:err_jid_malformed()), process_auth_failure(State, U, Err, 'jid-malformed'); false -> - Txt = <<"Denied by ACL">>, + Txt = <<"Access denied by service policy">>, Err = xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)), process_auth_failure(State, U, Err, 'forbidden') end. diff --git a/src/mod_mam.erl b/src/mod_mam.erl index 674cefc05..48552988d 100644 --- a/src/mod_mam.erl +++ b/src/mod_mam.erl @@ -22,22 +22,24 @@ %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%------------------------------------------------------------------- + -module(mod_mam). --protocol({xep, 313, '0.5.1'}). +-protocol({xep, 313, '0.6.1'}). -protocol({xep, 334, '0.2'}). +-protocol({xep, 359, '0.5.0'}). -behaviour(gen_mod). %% API -export([start/2, stop/1, reload/3, depends/2]). --export([user_send_packet/1, user_send_packet_strip_tag/1, user_receive_packet/1, - process_iq_v0_2/1, process_iq_v0_3/1, disco_sm_features/5, - remove_user/2, remove_room/3, mod_opt_type/1, muc_process_iq/2, - muc_filter_message/3, message_is_archived/3, delete_old_messages/2, - get_commands_spec/0, msg_to_el/4, get_room_config/4, set_room_option/3, - offline_message/1, export/1]). +-export([sm_receive_packet/1, user_receive_packet/1, user_send_packet/1, + user_send_packet_strip_tag/1, process_iq_v0_2/1, process_iq_v0_3/1, + disco_sm_features/5, remove_user/2, remove_room/3, mod_opt_type/1, + muc_process_iq/2, muc_filter_message/3, message_is_archived/3, + delete_old_messages/2, get_commands_spec/0, msg_to_el/4, + get_room_config/4, set_room_option/3, offline_message/1, export/1]). -include("xmpp.hrl"). -include("logger.hrl"). @@ -58,7 +60,7 @@ all | chat | groupchat) -> any(). -callback extended_fields() -> [mam_query:property() | #xdata_field{}]. -callback store(xmlel(), binary(), {binary(), binary()}, chat | groupchat, - jid(), binary(), recv | send) -> {ok, binary()} | any(). + jid(), binary(), recv | send, integer()) -> ok | 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(), mam_query:result(), @@ -77,14 +79,16 @@ start(Host, Opts) -> Mod:init(Host, Opts), init_cache(Host, Opts), register_iq_handlers(Host, IQDisc), + ejabberd_hooks:add(sm_receive_packet, Host, ?MODULE, + sm_receive_packet, 50), ejabberd_hooks:add(user_receive_packet, Host, ?MODULE, user_receive_packet, 88), ejabberd_hooks:add(user_send_packet, Host, ?MODULE, user_send_packet, 88), ejabberd_hooks:add(user_send_packet, Host, ?MODULE, - user_send_packet_strip_tag, 500), + user_send_packet_strip_tag, 500), ejabberd_hooks:add(offline_message_hook, Host, ?MODULE, - offline_message, 40), + offline_message, 50), ejabberd_hooks:add(muc_filter_message, Host, ?MODULE, muc_filter_message, 50), ejabberd_hooks:add(muc_process_iq, Host, ?MODULE, @@ -140,14 +144,16 @@ cache_opts(Host, Opts) -> stop(Host) -> unregister_iq_handlers(Host), - ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, - user_send_packet, 88), + ejabberd_hooks:delete(sm_receive_packet, Host, ?MODULE, + sm_receive_packet, 50), ejabberd_hooks:delete(user_receive_packet, Host, ?MODULE, user_receive_packet, 88), ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, + user_send_packet, 88), + ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, user_send_packet_strip_tag, 500), ejabberd_hooks:delete(offline_message_hook, Host, ?MODULE, - offline_message, 40), + offline_message, 50), ejabberd_hooks:delete(muc_filter_message, Host, ?MODULE, muc_filter_message, 50), ejabberd_hooks:delete(muc_process_iq, Host, ?MODULE, @@ -169,8 +175,12 @@ stop(Host) -> false -> ok end, - ejabberd_commands:unregister_commands(get_commands_spec()), - ok. + case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of + false -> + ejabberd_commands:unregister_commands(get_commands_spec()); + true -> + ok + end. reload(Host, NewOpts, OldOpts) -> NewMod = gen_mod:db_mod(Host, NewOpts, ?MODULE), @@ -214,6 +224,10 @@ register_iq_handlers(Host, IQDisc) -> gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_MAM_1, ?MODULE, process_iq_v0_3, IQDisc), gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MAM_1, + ?MODULE, process_iq_v0_3, IQDisc), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_MAM_2, + ?MODULE, process_iq_v0_3, IQDisc), + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MAM_2, ?MODULE, process_iq_v0_3, IQDisc). -spec unregister_iq_handlers(binary()) -> ok. @@ -223,7 +237,9 @@ unregister_iq_handlers(Host) -> gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_MAM_0), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MAM_0), gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_MAM_1), - gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MAM_1). + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MAM_1), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_MAM_2), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MAM_2). -spec remove_user(binary(), binary()) -> ok. remove_user(User, Server) -> @@ -255,87 +271,109 @@ set_room_option(_Acc, {mam, Val}, _Lang) -> set_room_option(Acc, _Property, _Lang) -> Acc. +-spec sm_receive_packet(stanza()) -> stanza(). +sm_receive_packet(#message{to = #jid{lserver = LServer}} = Pkt) -> + init_stanza_id(Pkt, LServer); +sm_receive_packet(Acc) -> + Acc. + -spec user_receive_packet({stanza(), c2s_state()}) -> {stanza(), c2s_state()}. -user_receive_packet({Pkt, #{jid := JID} = C2SState}) -> - Peer = xmpp:get_from(Pkt), +user_receive_packet({#message{from = Peer} = Pkt, #{jid := JID} = C2SState}) -> LUser = JID#jid.luser, LServer = JID#jid.lserver, - Pkt2 = case should_archive(Pkt, LServer) of - true -> - Pkt1 = strip_my_archived_tag(Pkt, LServer), - case store_msg(Pkt1, LUser, LServer, Peer, recv) of - {ok, ID} -> - set_stanza_id(Pkt1, JID, ID); - _ -> - Pkt1 - end; - _ -> - Pkt + Pkt1 = case should_archive(Pkt, LServer) of + true -> + case store_msg(Pkt, LUser, LServer, Peer, recv) of + ok -> + mark_stored_msg(Pkt, JID); + _ -> + Pkt + end; + _ -> + Pkt end, - {Pkt2, C2SState}. + {Pkt1, C2SState}; +user_receive_packet(Acc) -> + Acc. --spec user_send_packet({stanza(), c2s_state()}) -> {stanza(), c2s_state()}. -user_send_packet({Pkt, #{jid := JID} = C2SState}) -> - Peer = xmpp:get_to(Pkt), +-spec user_send_packet({stanza(), c2s_state()}) + -> {stanza(), c2s_state()}. +user_send_packet({#message{to = Peer} = Pkt, #{jid := JID} = C2SState}) -> LUser = JID#jid.luser, LServer = JID#jid.lserver, - Pkt2 = case should_archive(Pkt, LServer) of - true -> - Pkt1 = strip_my_archived_tag(Pkt, LServer), + Pkt1 = init_stanza_id(Pkt, LServer), + Pkt2 = case should_archive(Pkt1, LServer) of + true -> case store_msg(xmpp:set_from_to(Pkt1, JID, Peer), - LUser, LServer, Peer, send) of - {ok, ID} -> - set_stanza_id(Pkt1, JID, ID); - _ -> + LUser, LServer, Peer, send) of + ok -> + mark_stored_msg(Pkt1, JID); + _ -> Pkt1 - end; - false -> - Pkt + end; + false -> + Pkt1 end, - {Pkt2, C2SState}. + {Pkt2, C2SState}; +user_send_packet(Acc) -> + Acc. + +-spec user_send_packet_strip_tag({stanza(), c2s_state()}) + -> {stanza(), c2s_state()}. +user_send_packet_strip_tag({#message{} = Pkt, #{jid := JID} = C2SState}) -> + LServer = JID#jid.lserver, + {strip_my_stanza_id(Pkt, LServer), C2SState}; +user_send_packet_strip_tag(Acc) -> + Acc. -spec offline_message({any(), message()}) -> {any(), message()}. offline_message({_Action, #message{from = Peer, to = To} = Pkt} = Acc) -> LUser = To#jid.luser, LServer = To#jid.lserver, case should_archive(Pkt, LServer) of - true -> - Pkt1 = strip_my_archived_tag(Pkt, LServer), - case store_msg(Pkt1, LUser, LServer, Peer, recv) of - {ok, ID} -> - {archived, set_stanza_id(Pkt1, To, ID)}; - _ -> - Acc - end; - false -> - Acc + true -> + case store_msg(Pkt, LUser, LServer, Peer, recv) of + ok -> + {archived, mark_stored_msg(Pkt, To)}; + _ -> + Acc + end; + false -> + Acc end. --spec user_send_packet_strip_tag({stanza(), c2s_state()}) -> - {stanza(), c2s_state()}. -user_send_packet_strip_tag({Pkt, #{jid := JID} = C2SState}) -> - LServer = JID#jid.lserver, - {strip_my_archived_tag(Pkt, LServer), C2SState}. - -spec muc_filter_message(message(), mod_muc_room:state(), binary()) -> message(). -muc_filter_message(Pkt, #state{config = Config, jid = RoomJID} = MUCState, +muc_filter_message(#message{from = From} = Pkt, + #state{config = Config, jid = RoomJID} = MUCState, FromNick) -> - From = xmpp:get_from(Pkt), + LServer = RoomJID#jid.lserver, + Pkt1 = init_stanza_id(Pkt, LServer), if Config#config.mam -> - LServer = RoomJID#jid.lserver, - NewPkt = strip_my_archived_tag(Pkt, LServer), - StorePkt = strip_x_jid_tags(NewPkt), + StorePkt = strip_x_jid_tags(Pkt1), case store_muc(MUCState, StorePkt, RoomJID, From, FromNick) of - {ok, ID} -> - set_stanza_id(NewPkt, RoomJID, ID); + ok -> + mark_stored_msg(Pkt1, RoomJID); _ -> - NewPkt + Pkt1 end; true -> - Pkt - end. + Pkt1 + end; +muc_filter_message(Acc, _MUCState, _FromNick) -> + Acc. +-spec get_stanza_id(stanza()) -> integer(). +get_stanza_id(#message{meta = #{stanza_id := ID}}) -> + ID. + +-spec init_stanza_id(stanza(), binary()) -> stanza(). +init_stanza_id(Pkt, LServer) -> + ID = p1_time_compat:system_time(micro_seconds), + Pkt1 = strip_my_stanza_id(Pkt, LServer), + xmpp:put_meta(Pkt1, stanza_id, ID). + +-spec set_stanza_id(stanza(), jid(), integer()) -> stanza(). set_stanza_id(Pkt, JID, ID) -> BareJID = jid:remove_resource(JID), Archived = #mam_archived{by = BareJID, id = ID}, @@ -343,6 +381,11 @@ set_stanza_id(Pkt, JID, ID) -> NewEls = [Archived, StanzaID|xmpp:get_els(Pkt)], xmpp:set_els(Pkt, NewEls). +-spec mark_stored_msg(message(), jid()) -> message(). +mark_stored_msg(#message{meta = #{stanza_id := ID}} = Pkt, JID) -> + Pkt1 = set_stanza_id(Pkt, JID, integer_to_binary(ID)), + xmpp:put_meta(Pkt1, mam_archived, true). + % Query archive v0.2 process_iq_v0_2(#iq{from = #jid{lserver = LServer}, to = #jid{lserver = LServer}, @@ -368,7 +411,7 @@ muc_process_iq(#iq{type = T, lang = Lang, from = From, sub_els = [#mam_query{xmlns = NS}]} = IQ, MUCState) - when (T == set andalso (NS == ?NS_MAM_0 orelse NS == ?NS_MAM_1)) orelse + when (T == set andalso (NS /= ?NS_MAM_TMP)) orelse (T == get andalso NS == ?NS_MAM_TMP) -> case may_enter_room(From, MUCState) of true -> @@ -381,7 +424,7 @@ muc_process_iq(#iq{type = T, lang = Lang, end; muc_process_iq(#iq{type = get, sub_els = [#mam_query{xmlns = NS}]} = IQ, - MUCState) when NS == ?NS_MAM_0; NS == ?NS_MAM_1 -> + MUCState) when NS /= ?NS_MAM_TMP -> LServer = MUCState#state.server_host, process_iq(LServer, IQ); muc_process_iq(IQ, _MUCState) -> @@ -411,22 +454,18 @@ disco_sm_features(empty, From, To, Node, Lang) -> disco_sm_features({result, OtherFeatures}, #jid{luser = U, lserver = S}, #jid{luser = U, lserver = S}, <<"">>, _Lang) -> - {result, [?NS_MAM_TMP, ?NS_MAM_0, ?NS_MAM_1 | OtherFeatures]}; + {result, [?NS_MAM_TMP, ?NS_MAM_0, ?NS_MAM_1, ?NS_MAM_2, ?NS_SID_0 | + OtherFeatures]}; disco_sm_features(Acc, _From, _To, _Node, _Lang) -> Acc. -spec message_is_archived(boolean(), c2s_state(), message()) -> boolean(). message_is_archived(true, _C2SState, _Pkt) -> true; -message_is_archived(false, #{jid := JID}, Pkt) -> - #jid{luser = LUser, lserver = LServer} = JID, - Peer = xmpp:get_from(Pkt), +message_is_archived(false, #{lserver := LServer}, Pkt) -> case gen_mod:get_module_opt(LServer, ?MODULE, assume_mam_usage, false) of true -> - should_archive(strip_my_archived_tag(Pkt, LServer), LServer) - andalso should_archive_peer(LUser, LServer, - get_prefs(LUser, LServer), - Peer); + is_archived(Pkt, LServer); false -> false end. @@ -482,7 +521,7 @@ process_iq(LServer, #iq{sub_els = [#mam_query{xmlns = NS}]} = IQ) -> process_iq(#iq{type = set, lang = Lang, sub_els = [#mam_prefs{default = undefined, xmlns = NS}]} = IQ) -> Why = {missing_attr, <<"default">>, <<"prefs">>, NS}, - ErrTxt = xmpp:format_error(Why), + ErrTxt = xmpp:io_format_error(Why), xmpp:make_error(IQ, xmpp:err_bad_request(ErrTxt, Lang)); process_iq(#iq{from = #jid{luser = LUser, lserver = LServer}, to = #jid{lserver = LServer}, @@ -535,15 +574,14 @@ process_iq(LServer, #iq{from = #jid{luser = LUser}, lang = Lang, end end. +-spec should_archive(message(), binary()) -> boolean(). should_archive(#message{type = error}, _LServer) -> false; -should_archive(#message{meta = #{sm_copy := true}}, _LServer) -> - false; should_archive(#message{meta = #{from_offline := true}}, _LServer) -> false; should_archive(#message{body = Body, subject = Subject, type = Type} = Pkt, LServer) -> - case is_resent(Pkt, LServer) of + case is_archived(Pkt, LServer) of true -> false; false -> @@ -562,8 +600,8 @@ should_archive(#message{body = Body, subject = Subject, should_archive(_, _LServer) -> false. --spec strip_my_archived_tag(stanza(), binary()) -> stanza(). -strip_my_archived_tag(Pkt, LServer) -> +-spec strip_my_stanza_id(stanza(), binary()) -> stanza(). +strip_my_stanza_id(Pkt, LServer) -> Els = xmpp:get_els(Pkt), NewEls = lists:filter( fun(El) -> @@ -645,6 +683,7 @@ should_archive_peer(LUser, LServer, end end. +-spec should_archive_muc(message()) -> boolean(). should_archive_muc(#message{type = groupchat, body = Body, subject = Subj} = Pkt) -> case check_store_hint(Pkt) of @@ -668,6 +707,7 @@ should_archive_muc(#message{type = groupchat, should_archive_muc(_) -> false. +-spec check_store_hint(message()) -> store | no_store | none. check_store_hint(Pkt) -> case has_store_hint(Pkt) of true -> @@ -681,7 +721,6 @@ check_store_hint(Pkt) -> end end. - -spec has_store_hint(message()) -> boolean(). has_store_hint(Message) -> xmpp:has_subtag(Message, #hint{type = 'store'}). @@ -693,8 +732,8 @@ has_no_store_hint(Message) -> xmpp:has_subtag(Message, #hint{type = 'no-permanent-store'}) orelse xmpp:has_subtag(Message, #hint{type = 'no-permanent-storage'}). --spec is_resent(message(), binary()) -> boolean(). -is_resent(Pkt, LServer) -> +-spec is_archived(message(), binary()) -> boolean(). +is_archived(Pkt, LServer) -> case xmpp:get_subtag(Pkt, #stanza_id{by = #jid{}}) of #stanza_id{by = #jid{lserver = LServer}} -> true; @@ -702,33 +741,38 @@ is_resent(Pkt, LServer) -> false end. +-spec may_enter_room(jid(), mod_muc_room:state()) -> boolean(). 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). --spec store_msg(stanza(), - binary(), binary(), jid(), send | recv) -> - {ok, binary()} | pass. +-spec store_msg(message(), binary(), binary(), jid(), send | recv) + -> ok | pass | any(). store_msg(Pkt, LUser, LServer, Peer, Dir) -> Prefs = get_prefs(LUser, LServer), - case should_archive_peer(LUser, LServer, Prefs, Peer) of - true -> - US = {LUser, LServer}, + case {should_archive_peer(LUser, LServer, Prefs, Peer), Pkt} of + {true, #message{meta = #{sm_copy := true}}} -> + ok; % Already stored. + {true, _} -> case ejabberd_hooks:run_fold(store_mam_message, LServer, Pkt, [LUser, LServer, Peer, chat, Dir]) of drop -> pass; - NewPkt -> + Pkt1 -> + US = {LUser, LServer}, + ID = get_stanza_id(Pkt1), + El = xmpp:encode(Pkt1), Mod = gen_mod:db_mod(LServer, ?MODULE), - El = xmpp:encode(NewPkt), - Mod:store(El, LServer, US, chat, Peer, <<"">>, Dir) + Mod:store(El, LServer, US, chat, Peer, <<"">>, Dir, ID) end; - false -> + {false, _} -> pass end. +-spec store_muc(mod_muc_room:state(), message(), jid(), jid(), binary()) + -> ok | pass | any(). store_muc(MUCState, Pkt, RoomJID, Peer, Nick) -> case should_archive_muc(Pkt) of true -> @@ -738,10 +782,12 @@ store_muc(MUCState, Pkt, RoomJID, Peer, Nick) -> [U, S, Peer, groupchat, recv]) of drop -> pass; - NewPkt -> + Pkt1 -> + US = {U, S}, + ID = get_stanza_id(Pkt1), + El = xmpp:encode(Pkt1), Mod = gen_mod:db_mod(LServer, ?MODULE), - El = xmpp:encode(NewPkt), - Mod:store(El, LServer, {U, S}, groupchat, Peer, Nick, recv) + Mod:store(El, LServer, US, groupchat, Peer, Nick, recv, ID) end; false -> pass @@ -863,8 +909,13 @@ select(_LServer, JidRequestor, JidArchive, Query, RSM, {Msgs, true, L} end; select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType) -> - Mod = gen_mod:db_mod(LServer, ?MODULE), - Mod:select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType). + case might_expose_jid(Query, MsgType) of + true -> + {[], true, 0}; + false -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType) + end. msg_to_el(#archive_msg{timestamp = TS, packet = El, nick = Nick, peer = Peer, id = ID}, @@ -931,13 +982,13 @@ send(Msgs, Count, IsComplete, #mam_fin{xmlns = NS, id = QID, rsm = RSMOut, complete = IsComplete} end, - if NS == ?NS_MAM_TMP; NS == ?NS_MAM_1 -> + if NS /= ?NS_MAM_0 -> lists:foreach( fun(El) -> ejabberd_router:route(El) end, Els), xmpp:make_iq_result(IQ, Result); - NS == ?NS_MAM_0 -> + true -> ejabberd_router:route(xmpp:make_iq_result(IQ)), lists:foreach( fun(El) -> @@ -988,6 +1039,13 @@ match_rsm(Now, #rsm_set{before = ID}) when is_binary(ID), ID /= <<"">> -> match_rsm(_Now, _) -> true. +might_expose_jid(Query, + {groupchat, Role, #state{config = #config{anonymous = true}}}) + when Role /= moderator -> + proplists:is_defined(with, Query); +might_expose_jid(_Query, _MsgType) -> + false. + get_jids(undefined) -> []; get_jids(Js) -> diff --git a/src/mod_mam_mnesia.erl b/src/mod_mam_mnesia.erl index f498bc3c7..71f1f701b 100644 --- a/src/mod_mam_mnesia.erl +++ b/src/mod_mam_mnesia.erl @@ -28,7 +28,7 @@ %% 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/6]). + extended_fields/0, store/8, write_prefs/4, get_prefs/2, select/6]). -include_lib("stdlib/include/ms_transform.hrl"). -include("xmpp.hrl"). @@ -91,10 +91,10 @@ delete_old_user_messages(User, TimeStamp, Type) -> ok end end, + NextRecord = mnesia:dirty_next(archive_msg, User), case mnesia:transaction(F) of {atomic, ok} -> - delete_old_user_messages(mnesia:dirty_next(archive_msg, User), - TimeStamp, Type); + delete_old_user_messages(NextRecord, TimeStamp, Type); {aborted, Err} -> ?ERROR_MSG("Cannot delete old MAM messages: ~s", [Err]), Err @@ -103,7 +103,7 @@ delete_old_user_messages(User, TimeStamp, Type) -> extended_fields() -> []. -store(Pkt, _, {LUser, LServer}, Type, Peer, Nick, _Dir) -> +store(Pkt, _, {LUser, LServer}, Type, Peer, Nick, _Dir, TS) -> case {mnesia:table_info(archive_msg, disc_only_copies), mnesia:table_info(archive_msg, memory)} of {[_|_], TableSize} when TableSize > ?TABLE_SIZE_LIMIT -> @@ -112,13 +112,11 @@ store(Pkt, _, {LUser, LServer}, Type, Peer, Nick, _Dir) -> {error, overflow}; _ -> LPeer = {PUser, PServer, _} = jid:tolower(Peer), - TS = p1_time_compat:timestamp(), - ID = integer_to_binary(now_to_usec(TS)), F = fun() -> mnesia:write( #archive_msg{us = {LUser, LServer}, - id = ID, - timestamp = TS, + id = integer_to_binary(TS), + timestamp = misc:usec_to_now(TS), peer = LPeer, bare_peer = {PUser, PServer, <<>>}, type = Type, @@ -127,7 +125,7 @@ store(Pkt, _, {LUser, LServer}, Type, Peer, Nick, _Dir) -> end, case mnesia:transaction(F) of {atomic, ok} -> - {ok, ID}; + ok; {aborted, Err} -> ?ERROR_MSG("Cannot add message to MAM archive of ~s@~s: ~s", [LUser, LServer, Err]), @@ -178,9 +176,6 @@ select(_LServer, JidRequestor, %%%=================================================================== %%% Internal functions %%%=================================================================== -now_to_usec({MSec, Sec, USec}) -> - (MSec*1000000 + Sec)*1000000 + USec. - make_matchspec(LUser, LServer, Start, undefined, With) -> %% List is always greater than a tuple make_matchspec(LUser, LServer, Start, [], With); diff --git a/src/mod_mam_sql.erl b/src/mod_mam_sql.erl index 7e02b5791..40aa98367 100644 --- a/src/mod_mam_sql.erl +++ b/src/mod_mam_sql.erl @@ -30,7 +30,7 @@ %% 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/6, export/1]). + extended_fields/0, store/8, write_prefs/4, get_prefs/2, select/6, export/1]). -include_lib("stdlib/include/ms_transform.hrl"). -include("xmpp.hrl"). @@ -38,6 +38,12 @@ -include("logger.hrl"). -include("ejabberd_sql_pt.hrl"). +-ifdef(NEW_SQL_SCHEMA). +-define(USE_NEW_SCHEMA, true). +-else. +-define(USE_NEW_SCHEMA, false). +-endif. + %%%=================================================================== %%% API %%%=================================================================== @@ -47,31 +53,38 @@ init(_Host, _Opts) -> remove_user(LUser, LServer) -> ejabberd_sql:sql_query( LServer, - ?SQL("delete from archive where username=%(LUser)s")), + ?SQL("delete from archive where username=%(LUser)s and %(LServer)H")), ejabberd_sql:sql_query( LServer, - ?SQL("delete from archive_prefs where username=%(LUser)s")). + ?SQL("delete from archive_prefs where username=%(LUser)s and %(LServer)H")). remove_room(LServer, LName, LHost) -> LUser = jid:encode({LName, LHost, <<>>}), remove_user(LUser, LServer). delete_old_messages(ServerHost, TimeStamp, Type) -> - TypeClause = if Type == all -> <<"">>; - true -> [<<" and kind='">>, misc: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, <<";">>]), + TS = now_to_usec(TimeStamp), + case Type of + all -> + ejabberd_sql:sql_query( + ServerHost, + ?SQL("delete from archive" + " where timestamp < %(TS)d and %(ServerHost)H")); + _ -> + SType = misc:atom_to_binary(Type), + ejabberd_sql:sql_query( + ServerHost, + ?SQL("delete from archive" + " where timestamp < %(TS)d" + " and kind=%(SType)s" + " and %(ServerHost)H")) + end, ok. extended_fields() -> [{withtext, <<"">>}]. -store(Pkt, LServer, {LUser, LHost}, Type, Peer, Nick, _Dir) -> - TSinteger = p1_time_compat:system_time(micro_seconds), - ID = integer_to_binary(TSinteger), +store(Pkt, LServer, {LUser, LHost}, Type, Peer, Nick, _Dir, TS) -> SUser = case Type of chat -> LUser; groupchat -> jid:encode({LUser, LHost, <<>>}) @@ -86,18 +99,19 @@ store(Pkt, LServer, {LUser, LHost}, Type, Peer, Nick, _Dir) -> SType = misc:atom_to_binary(Type), case ejabberd_sql:sql_query( LServer, - ?SQL("insert into archive (username, timestamp," - " peer, bare_peer, xml, txt, kind, nick) values (" - "%(SUser)s, " - "%(TSinteger)d, " - "%(LPeer)s, " - "%(BarePeer)s, " - "%(XML)s, " - "%(Body)s, " - "%(SType)s, " - "%(Nick)s)")) of + ?SQL_INSERT( + "archive", + ["username=%(SUser)s", + "server_host=%(LServer)s", + "timestamp=%(TS)d", + "peer=%(LPeer)s", + "bare_peer=%(BarePeer)s", + "xml=%(XML)s", + "txt=%(Body)s", + "kind=%(SType)s", + "nick=%(Nick)s"])) of {updated, _} -> - {ok, ID}; + ok; Err -> Err end. @@ -113,6 +127,7 @@ write_prefs(LUser, _LServer, #archive_prefs{default = Default, ServerHost, "archive_prefs", ["!username=%(LUser)s", + "!server_host=%(ServerHost)s", "def=%(SDefault)s", "always=%(SAlways)s", "never=%(SNever)s"]) of @@ -126,7 +141,7 @@ get_prefs(LUser, LServer) -> case ejabberd_sql:sql_query( LServer, ?SQL("select @(def)s, @(always)s, @(never)s from archive_prefs" - " where username=%(LUser)s")) of + " where username=%(LUser)s and %(LServer)H")) of {selected, [{SDefault, SAlways, SNever}]} -> Default = erlang:binary_to_existing_atom(SDefault, utf8), Always = ejabberd_sql:decode_term(SAlways), @@ -192,8 +207,13 @@ export(_Server) -> SDefault = erlang:atom_to_binary(Default, utf8), SAlways = misc:term_to_expr(Always), SNever = misc:term_to_expr(Never), - [?SQL("insert into archive_prefs (username, def, always, never) values" - "(%(LUser)s, %(SDefault)s, %(SAlways)s, %(SNever)s);")]; + [?SQL_INSERT( + "archive_prefs", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "def=%(SDefault)s", + "always=%(SAlways)s", + "never=%(SNever)s"])]; (_Host, _R) -> [] end}, @@ -212,11 +232,17 @@ export(_Server) -> XML = fxml:element_to_binary(Pkt), Body = fxml:get_subtag_cdata(Pkt, <<"body">>), SType = misc:atom_to_binary(Type), - [?SQL("insert into archive (username, timestamp, " - "peer, bare_peer, xml, txt, kind, nick) " - "values (%(SUser)s, %(TStmp)d, %(LPeer)s, " - "%(BarePeer)s, %(XML)s, %(Body)s, %(SType)s, " - "%(Nick)s);")]; + [?SQL_INSERT( + "archive", + ["username=%(SUser)s", + "server_host=%(LServer)s", + "timestamp=%(TStmp)d", + "peer=%(LPeer)s", + "bare_peer=%(BarePeer)s", + "xml=%(XML)s", + "txt=%(Body)s", + "kind=%(SType)s", + "nick=%(Nick)s"])]; (_Host, _R) -> [] end}]. @@ -303,11 +329,24 @@ make_sql_query(User, LServer, MAMQuery, RSM) -> [] end, SUser = Escape(User), + SServer = Escape(LServer), - Query = [<<"SELECT ">>, TopClause, <<" timestamp, xml, peer, kind, nick" - " FROM archive WHERE username='">>, - SUser, <<"'">>, WithClause, WithTextClause, StartClause, EndClause, - PageClause], + Query = + case ?USE_NEW_SCHEMA of + true -> + [<<"SELECT ">>, TopClause, + <<" timestamp, xml, peer, kind, nick" + " FROM archive WHERE username='">>, + SUser, <<"' and server_host='">>, + SServer, <<"'">>, WithClause, WithTextClause, + StartClause, EndClause, PageClause]; + false -> + [<<"SELECT ">>, TopClause, + <<" timestamp, xml, peer, kind, nick" + " FROM archive WHERE username='">>, + SUser, <<"'">>, WithClause, WithTextClause, + StartClause, EndClause, PageClause] + end, QueryPage = case Direction of @@ -322,9 +361,19 @@ make_sql_query(User, LServer, MAMQuery, RSM) -> [Query, <<" ORDER BY timestamp ASC ">>, LimitClause, <<";">>] end, - {QueryPage, - [<<"SELECT COUNT(*) FROM archive WHERE username='">>, - SUser, <<"'">>, WithClause, WithTextClause, StartClause, EndClause, <<";">>]}. + case ?USE_NEW_SCHEMA of + true -> + {QueryPage, + [<<"SELECT COUNT(*) FROM archive WHERE username='">>, + SUser, <<"' and server_host='">>, + SServer, <<"'">>, WithClause, WithTextClause, + StartClause, EndClause, <<";">>]}; + false -> + {QueryPage, + [<<"SELECT COUNT(*) FROM archive WHERE username='">>, + SUser, <<"'">>, WithClause, WithTextClause, + StartClause, EndClause, <<";">>]} + end. -spec get_max_direction_id(rsm_set() | undefined) -> {integer() | undefined, diff --git a/src/mod_muc.erl b/src/mod_muc.erl index 2aebe2226..f7d5303fb 100644 --- a/src/mod_muc.erl +++ b/src/mod_muc.erl @@ -39,6 +39,7 @@ reload/3, room_destroyed/4, store_room/4, + store_room/5, restore_room/3, forget_room/3, create_room/5, @@ -61,6 +62,7 @@ count_online_rooms/1, register_online_user/4, unregister_online_user/4, + iq_set_register_info/5, count_online_rooms_by_user/3, get_online_rooms_by_user/3, can_use_nick/4]). @@ -87,7 +89,7 @@ -type muc_room_opts() :: [{atom(), any()}]. -callback init(binary(), gen_mod:opts()) -> any(). -callback import(binary(), binary(), [binary()]) -> ok. --callback store_room(binary(), binary(), binary(), list()) -> {atomic, any()}. +-callback store_room(binary(), binary(), binary(), list(), list()|undefined) -> {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(). @@ -104,6 +106,8 @@ -callback unregister_online_user(binary(), ljid(), binary(), binary()) -> any(). -callback count_online_rooms_by_user(binary(), binary(), binary()) -> non_neg_integer(). -callback get_online_rooms_by_user(binary(), binary(), binary()) -> [{binary(), binary()}]. +-callback get_subscribed_rooms(binary(), binary(), jid()) -> + {ok, [{ljid(), binary(), [binary()]}]} | {error, any()}. %%==================================================================== %% API @@ -156,9 +160,12 @@ create_room(Host, Name, From, Nick, Opts) -> gen_server:call(Proc, {create, Name, Host, From, Nick, Opts}). store_room(ServerHost, Host, Name, Opts) -> + store_room(ServerHost, Host, Name, Opts, undefined). + +store_room(ServerHost, Host, Name, Opts, ChangesHints) -> LServer = jid:nameprep(ServerHost), Mod = gen_mod:db_mod(LServer, ?MODULE), - Mod:store_room(LServer, Host, Name, Opts). + Mod:store_room(LServer, Host, Name, Opts, ChangesHints). restore_room(ServerHost, Host, Name) -> LServer = jid:nameprep(ServerHost), @@ -509,7 +516,7 @@ process_disco_info(#iq{type = get, to = To, lang = Lang, X = ejabberd_hooks:run_fold(disco_info, ServerHost, [], [ServerHost, ?MODULE, <<"">>, Lang]), MAMFeatures = case gen_mod:is_loaded(ServerHost, mod_mam) of - true -> [?NS_MAM_TMP, ?NS_MAM_0, ?NS_MAM_1]; + true -> [?NS_MAM_TMP, ?NS_MAM_0, ?NS_MAM_1, ?NS_MAM_2]; false -> [] end, RSMFeatures = case RMod:rsm_supported() of @@ -680,7 +687,7 @@ iq_disco_items(_ServerHost, _Host, _From, Lang, _MaxRoomsDiscoItems, _Node, _RSM -spec get_room_disco_item({binary(), binary(), pid()}, term()) -> {ok, disco_item()} | - {error, timeout | notfound}. + {error, timeout | notfound}. get_room_disco_item({Name, Host, Pid}, Query) -> RoomJID = jid:make(Name, Host), try p1_fsm:sync_send_all_state_event(Pid, Query, 100) of @@ -688,24 +695,31 @@ get_room_disco_item({Name, Host, Pid}, Query) -> {ok, #disco_item{jid = RoomJID, name = Desc}}; false -> {error, notfound} - catch _:{timeout, _} -> + catch _:{timeout, {p1_fsm, _, _}} -> {error, timeout}; - _:{noproc, _} -> + _:{_, {p1_fsm, _, _}} -> {error, notfound} end. get_subscribed_rooms(ServerHost, Host, From) -> - Rooms = get_online_rooms(ServerHost, Host), + LServer = jid:nameprep(ServerHost), + Mod = gen_mod:db_mod(LServer, ?MODULE), BareFrom = jid:remove_resource(From), - lists:flatmap( - fun({Name, _, Pid}) -> - case p1_fsm:sync_send_all_state_event(Pid, {is_subscribed, BareFrom}) of - true -> [jid:make(Name, Host)]; - false -> [] - end; - (_) -> - [] - end, Rooms). + case Mod:get_subscribed_rooms(LServer, Host, BareFrom) of + not_implemented -> + Rooms = get_online_rooms(ServerHost, Host), + lists:flatmap( + fun({Name, _, Pid}) -> + case p1_fsm:sync_send_all_state_event(Pid, {is_subscribed, BareFrom}) of + true -> [jid:make(Name, Host)]; + false -> [] + end; + (_) -> + [] + end, Rooms); + V -> + V + end. get_nick(ServerHost, Host, From) -> LServer = jid:nameprep(ServerHost), diff --git a/src/mod_muc_admin.erl b/src/mod_muc_admin.erl index 5197c1b71..ac11283ad 100644 --- a/src/mod_muc_admin.erl +++ b/src/mod_muc_admin.erl @@ -29,7 +29,7 @@ -behaviour(gen_mod). -export([start/2, stop/1, reload/3, depends/2, muc_online_rooms/1, - muc_register_nick/3, muc_unregister_nick/1, + muc_register_nick/3, muc_unregister_nick/2, create_room_with_opts/4, create_room/3, destroy_room/2, create_rooms_file/1, destroy_rooms_file/1, rooms_unused_list/2, rooms_unused_destroy/2, @@ -62,7 +62,12 @@ start(Host, _Opts) -> ejabberd_hooks:add(webadmin_page_host, Host, ?MODULE, web_page_host, 50). stop(Host) -> - ejabberd_commands:unregister_commands(get_commands_spec()), + case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of + false -> + ejabberd_commands:unregister_commands(get_commands_spec()); + true -> + ok + end, ejabberd_hooks:delete(webadmin_menu_main, ?MODULE, web_menu_main, 50), ejabberd_hooks:delete(webadmin_menu_host, Host, ?MODULE, web_menu_host, 50), ejabberd_hooks:delete(webadmin_page_main, ?MODULE, web_page_main, 50), @@ -91,16 +96,18 @@ get_commands_spec() -> args = [{host, binary}], result = {rooms, {list, {room, string}}}}, #ejabberd_commands{name = muc_register_nick, tags = [muc], - desc = "Register a nick in the MUC service", + desc = "Register a nick to a User JID in the MUC service of a server", module = ?MODULE, function = muc_register_nick, - args_desc = ["Nick", "User JID", "MUC service"], - args_example = [<<"Tim">>, <<"tim@example.org">>, <<"muc.example.org">>], - args = [{nick, binary}, {jid, binary}, {domain, binary}], + args_desc = ["Nick", "User JID", "Server Host"], + args_example = [<<"Tim">>, <<"tim@example.org">>, <<"example.org">>], + args = [{nick, binary}, {jid, binary}, {serverhost, binary}], result = {res, rescode}}, #ejabberd_commands{name = muc_unregister_nick, tags = [muc], - desc = "Unregister the nick in the MUC service", + desc = "Unregister the nick registered by that account in the MUC service", module = ?MODULE, function = muc_unregister_nick, - args = [{nick, binary}], + args_desc = ["User JID", "MUC service"], + args_example = [<<"tim@example.org">>, <<"example.org">>], + args = [{jid, binary}, {serverhost, binary}], result = {res, rescode}}, #ejabberd_commands{name = create_room, tags = [muc_room], @@ -305,31 +312,14 @@ muc_online_rooms(ServerHost) -> || {Name, _, _} <- mod_muc:get_online_rooms(Host)] end, Hosts). -muc_register_nick(Nick, JIDBinary, Domain) -> - try jid:decode(JIDBinary) of - JID -> - F = fun (MHost, MNick) -> - mnesia:write(#muc_registered{us_host=MHost, nick=MNick}) - end, - case mnesia:transaction(F, [{{JID#jid.luser, JID#jid.lserver}, - Domain}, Nick]) of - {atomic, ok} -> ok; - {aborted, _Error} -> error - end - catch _:{bad_jid, _} -> throw({error, "Malformed JID"}) - end. +muc_register_nick(Nick, FromBinary, ServerHost) -> + Host = find_host(ServerHost), + From = jid:decode(FromBinary), + Lang = <<"en">>, + mod_muc:iq_set_register_info(ServerHost, Host, From, Nick, Lang). -muc_unregister_nick(Nick) -> - F2 = fun(N) -> - [{_,Key,_}|_] = mnesia:index_read(muc_registered, N, 3), - mnesia:delete({muc_registered, Key}) - end, - case mnesia:transaction(F2, [Nick], 1) of - {atomic, ok} -> - ok; - {aborted, _Error} -> - error - end. +muc_unregister_nick(FromBinary, ServerHost) -> + muc_register_nick(<<"">>, FromBinary, ServerHost). get_user_rooms(LUser, LServer) -> lists:flatmap( @@ -817,7 +807,13 @@ get_room_occupants(Pid) -> dict:to_list(S#state.users)). get_room_occupants_number(Room, Host) -> - length(get_room_occupants(Room, Host)). + case get_room_pid(Room, Host) of + room_not_found -> + throw({error, room_not_found}); + Pid -> + S = get_room_state(Pid), + dict:size(S#state.users) + end. %%---------------------------- %% Send Direct Invitation diff --git a/src/mod_muc_log.erl b/src/mod_muc_log.erl index 61101d1c2..f2685aaab 100644 --- a/src/mod_muc_log.erl +++ b/src/mod_muc_log.erl @@ -502,186 +502,21 @@ make_dir_rec(Dir) -> %% c("../../ejabberd/src/jlib.erl"). %% base64:encode(F1b). -image_base64(<<"powered-by-erlang.png">>) -> - <<"iVBORw0KGgoAAAANSUhEUgAAAGUAAAAfCAYAAAD+xQNoA" - "AADN0lEQVRo3u1aP0waURz+rjGRRQ+nUyRCYmJyDPTapD" - "ARaSIbTUjt1gVSh8ZW69aBAR0cWLSxCXWp59LR1jbdqKn" - "GxoQuRZZrSYyHEVM6iZMbHewROA7u3fHvkr5vOn737vcu" - "33ffu9/vcQz+gef5Cij6CkmSGABgFEH29r5SVvqIsTEOH" - "o8HkiQxDBXEOjg9PcHc3BxuUSqsI8jR0REAUFGsCCoKFY" - "WCBAN6AxyO0Z7cyMXFb6oGqSgAsIrJut9hMQlvdNbUhKW" - "shLd3HtTF4jihShgVpRaBxKKmIGX5HL920/hz/BM2+zAm" - "pn2YioQaxnECj0BiEYcrG0Tzzc8/rfudSm02jaVSm9Vr1" - "MdG8rSKKXlJ7lHrfjouCut2IrC82BDPbe/gc+xlXez7Kx" - "Ez63H4lmIN473Rh8Si1BKhRY6aEJI8pLmbjSPN0xOnBBI" - "Lmg5RC6Lg28preKOzsNmHG8R1Bf0o7GdMucUslDy1pJLG" - "2sndVVG0lq3c9vum4zmBR1kuwiYMN5ybmCYXxQg57ThFO" - "TYznzpPO+IQi+IK+jXjg/YhuIJ+cIIHg+wQJoJ+2N3jYN" - "3Olvk4ge/IU98spne+FfGtlslm16nna8fduntfDscoVjG" - "JqUgIjz686ViFUdjP4N39x9Xq638viZVtlq2tLXKncLf5" - "ticuZSWU5XOUshJKxxKtfdtdvs4OyNb/68urKvlluYizg" - "wwu5SLK8jllu1t9ihYOlzdwdpBBKSvh+vKKzHkCj1JW3y" - "1m+hSj13WjqOiJKK0qpXKhSFxJAYBvKYaZ9TjWRu4SiWi" - "2LyDtb6wghGmn5HfTml16ILGA/G5al2DW7URYTFYrOU7g" - "icQ020sYqYDM9CbdgqFd4vzHL03JfvLjk6ZgADAVCSEsJ" - "vHsdL+utNYrm2ufZDVZSkzPKaQkW8kthpyS297BvRdRzR" - "6DdTurJbPy9Ov1K6xr3HBPQuIMowR3asegUyDuU9SuUG+" - "dmIGyZ0b7FBN9St3WunyC5yMsrVv7uXzRP58s/qKn6C4q" - "lQoVxVIvd4YBwzBUFKs6ZaD27U9hEdcAN98Sx2IxykafI" - "YrizbfESoB+dd9/KF/d/wX3cJvREzl1vAAAAABJRU5Erk" - "Jggg==">>; -image_base64(<<"valid-xhtml10.png">>) -> - <<"iVBORw0KGgoAAAANSUhEUgAAAFgAAAAfCAMAAAEjEcpEA" - "AACiFBMVEUAAADe5+fOezmtra3ejEKlhELvvWO9WlrehE" - "LOe3vepaWclHvetVLGc3PerVKcCAj3vVqUjHOUe1JjlL0" - "xOUpjjL2UAAC91ueMrc7vrVKlvdbW3u+EpcbO3ufO1ucY" - "WpSMKQi9SiF7e3taWkoQEAiMczkQSoxaUkpzc3O1lEoIC" - "ACEazEhGAgIAACEYzFra2utjELWcznGnEr/7+9jY2POaz" - "HOYzGta2NShLVrlL05OUqctdacCADGa2ucAADGpVqUtc6" - "1ORg5OTmlUikYGAiUezl7YzEYEAiUczkxMTG9nEqtIRDe" - "3t4AMXu9lEoQCACMazEAKXspKSmljFrW1ta1jELOzs7n7" - "/fGxsa9pVqEOSkpY5xznL29tZxahLXOpVr/99ZrY1L/79" - "ZjUiljSikAOYTvxmMAMYScezmchFqUczGtlFp7c2utjFq" - "UlJStxt73///39/9Ce61CSkq9xsZznMbW5+9Cc62MjIxC" - "Qkrv9/fv7/fOzsbnlErWjIz/3mtCORhza1IpIRBzWjH/1" - "mtCMRhzY1L/zmvnvVpSQiHOpVJrUinntVr3zmOEc1L3xm" - "NaWlq1nFo5QkrGWim1lFoISpRSUlK1zt4hWpwASoz////" - "///8xa6WUaykAQoxKe61KSkp7nMbWtWPe5+9jWlL39/f3" - "9/fWrWNCQkLera3nvWPv7+85MRjntWPetVp7c1IxKRCUl" - "HtKORh7a1IxIRCUjHtaSiHWrVIpIQhzWinvvVpaQiH/1m" - "PWpVKMe1L/zmP/xmNrUiGErc4YGBj/73PG1ucQWpT/53O" - "9nFoQUpS1SiEQEBC9zt69vb05c6UISoxSUko5a6UICAhS" - "SkohUpS1tbXetWMAQoSUgD+kAAAA2HRSTlP/////////i" - "P9sSf//dP////////////////////////////////////" - "////////////8M////////////ef/////////////////" - "/////////////////////////////////////////////" - "//////////////////////9d/////////////////////" - "///////////////AP//////////////CP//RP////////" - "/////////////////////////////////////////////" - "///////9xPp1gAAAFvUlEQVR42pVWi18URRwfy7vsYUba" - "iqBRBFmICUQGVKcZckQeaRJQUCLeycMSfKGH0uo5NELpI" - "vGQGzokvTTA85VHKTpbRoeJnPno/p1+M7t3txj20e/Nzu" - "7Ofve7v/k9Zg4Vc+wRQMW0eyLx1ZSANeBDxVmxZZSwEUY" - "kGAewm1eIBOMRvhv1UA+q8KXIVuxGdCelFYwxAnxOrxgb" - "Y8Ti1t4VA0QHYz4x3FnVC8OVLXv9fkKGSWDoW/4lG6Vbd" - "tBblesOs+MjmEmzJKNIJWFEfEQTCWNPFKvcKEymjLO1b8" - "bwYQd1hCiiDCl5KsrDCIlhj4fSuvcpfSpgJmyv6dzeZv+" - "nMPx3dhbt94II07/JZliEtm1N2RIYPkTYshwYm245a/zk" - "WjJwcyFh6ZIcYxxmqiaDSYxhOhFUsqngi3Fzcj3ljdYDN" - "E9uzA1YD/5MhnzW1KRqF7mYG8jFYXLcfLpjOe2LA0fuGq" - "QrQHl10sdK0sFcFSOSlzF0BgXQH9h3QZDBI0ccNEhftjX" - "uippBDD2/eMRiETmwwNEYHyqhdDyo22w+3QHuNbdve5a7" - "eOkHmDVJ0ixNmfbz1h0qo/Q6GuSB2wQJQbpOjOQAl7woW" - "SRJ0m2ewhvAOUiYYtZtaZL0CZZmtmVOQttLfr/dbveLZo" - "drfrL7W75wG/JjqkQxoNTtNsTKELQpQL6/D5loaSmyTT8" - "TUhsmi8iFA0hZiyltf7OiNKdarRm5w2So2lTNdPLuIzR+" - "AiLj8VTRJaj0LmX4VhJ27f/VJV/yycilWPOrk8NkXi7Qq" - "mj5bHqVZlJKZIRk1wFzKrt0WUbnXMPJ1fk4TJ5oWBA61p" - "1V76DeIs0MX+s3GxRlA1vtw83KhgNphc1nyErLO5zcvbO" - "srq+scbZnpzc6QVFPenLwGxmC+BOfYI+DN55QYddh4Q/N" - "E/yGYYj4TOGNngQavAZnzzTovEA+kcMJ+247uYexNA+4F" - "svjmuv662jsWxPZx2xg890bYMYnTgya7bjmCiEY0qgJ0v" - "MF3c+NoFdPyzxz6V3Uxs3AOWCDchRvOsQtBrbFsrT2fhH" - "Ec7ByGzu/dA4IO0A3HdfeP9yMqAwP6NPEb6cbwn0PWVU1" - "7/FDBQh/CPIrbfcg027IZrsAT/Bf3FNWyn9RSR4cvvwn3" - "e4HFmYPDl/thYcRVi8qPEoXVUWBl6FTBFTtnqmKKg5wnl" - "F4wZ1yeLv7TiwXKektE+iDBNicWEyLpnFhfDkpJc3q2kh" - "SPyQBbE0dMJnOoDzTwGsI7cdyMkL5gWqUjCF6Txst/twx" - "Cv1WzzHoy21ZDQ1xnuDzdPDWR4knr14v0tYn3IxaMFFdi" - "MOlEOJHw1jOQ4sWt5rQopRkXZhMEi7pmeDCVWBlfUKwhM" - "Z7rsF6elKsvbwiKxgxIdewa3ErsaYomCVZFYJb0GUu3Jq" - "GUNoplBxYiYby8vLBFWef+Cri4/I1sbQ/1OtYTrNtdXS+" - "rSe7kQ52eSObL99/iErCWUjCy5W4JLygmCouGfG9x9fmx" - "17XhBuDCaOerbt538erta7TFktLvdHghZcCbcPQO33zIJ" - "G9kxF5hoVXnzTzRz0r5js8oTj6uyPkGRf346HOLcasgFe" - "xueNUWFPtuFKzjoSFYYedhwVlhsRVYWWJpltv1XPQT1Rl" - "0bjZIBlb1XujVDzY/Kj4k6Ku3+Z0jo1owjVzDpFTXe1ju" - "vBSWNFmNWGZy8LvzUl5PN4JCwyNDzbQ0aAj4Zrjz0FatG" - "JJYhvq4j7mGSpvytGFlZtHf2C4o/28Zu8z7wo7eYPfXys" - "nF0i9NnPh1t1zR7VBb9GqaOXhtTmHQdgMFXE+Z608cnpO" - "DdZdjL+TuDY44Q38kJXHhccWLoOd9uv1AwwvO+48uu+fa" - "CSJPJ1bmy6ThyvpivBmYWgjxPDPAp7JTemY/yGKFEiRt/" - "jG/2P79s8KCwoLCgoLC/khUBA5F0SfQZ+RYfpNE/4Xosm" - "q7jsZAJsAAAAASUVORK5CYII=">>; -image_base64(<<"vcss.png">>) -> - <<"iVBORw0KGgoAAAANSUhEUgAAAFgAAAAfCAMAAABUFvrSA" - "AABKVBMVEUAAAAjIx8MR51ZVUqAdlmdnZ3ejEWLDAuNjY" - "1kiMG0n2d9fX19Ghfrp1FtbW3y39+3Ph6lIRNdXV2qJBF" - "cVUhcVUhPT0/dsmpUfLr57+/u7u4/PDWZAACZAADOp1Gd" - "GxG+SyTgvnNdSySzk16+mkuxw+BOS0BOS0DOzs7MzMy4T" - "09RRDwsJBG+vr73wV6fkG6eCQRFcLSurq6/X1+ht9nXfz" - "5sepHuwV59ZTHetFjQ2+wMCQQ2ZK5tWCsmWajsz8+Sq9N" - "MPh4hVaY8MRj///////////////////////9MTEyOp9Lu" - "8vhXU1A8PDyjOSTBz+YLRJ2rLy8sLCwXTaKujEUcHByDn" - "82dfz7/zGafDw+fDw+zRSlzlMcMDAyNcji1tbXf5vIcFg" - "vATJOjAAAAY3RSTlP/8/////////////////8A//////P" - "/////ov//8//////////////z///T//////////+i////" - "//////////8w/////6IA/xAgMP//////////8////////" - "/8w0/////////+zehebAAACkUlEQVR42u2VfVPTQBDG19" - "VqC6LY+lKrRIxFQaFSBPuSvhBPF8SIUZK2J5Yav/+HcO8" - "uZdLqTCsU/nKnyWwvk1/unnt2D9ZmH+8/cMAaTRFy+ng6" - "9/yiwC/+gy8R3McGv5zHvGJEGAdR4eBgi1IbZwevIEZE2" - "4pFtBtzG1Q4AoD5zvw5pEDcJvIQV/TE3/l+H9GnNJwcdA" - "BS5wAbFQLMqI98/UReoAaOTlaJsp0zaHx7LwZvY0BUR2x" - "pWTzqam0gzY8KGzG4MhBCNGucha4QbpETy+Yk/BP85nt7" - "34AjpQLTsE4ZFpf/dnkUCglXVNYB+OfUZJHvAqAoa45Oe" - "uPgm4+Xjtv7xm4N7PMV4C61+Mrz3H2WImm3ATiWrAiwZR" - "WcUA5Ej4dgIEMxDv6yxHHcNuAutnjv2HZ1NeuycoVPh0m" - "wC834zZC9Ao5dkZZKwLVGwT+WdLw0YOZ1saEkUDoT+QGW" - "KZ0E2xpcrPakVW2KXwyUtYEtlEAj3GXD/fYwrryAdeiyG" - "qidQSw1eqtJcA8cZq4zXqhPuCBYE1fKJjh/5X6MwRm9c2" - "xf7WVdLf5oSdt64esVIwVAKC1HJ2oli8vj3L0YzC4zjkM" - "agt+arDAs6bApbL1RVlWIqrJbreqKZmh4y6VR7rAJeUYD" - "VRj9VqRXkErpJ9lbEwtE83KlIfeG4p52t7zWIMO1XcaGz" - "54uUyet+hBM7BXXDS8Xc5+8Gmmbu1xwSoGIokA3oTptQe" - "cQ4Iimm/Ew7jwbPfMi3TM91T9XVIGo+W9xC8oWpugVCXL" - "uwXijjxJ3r/6PjX7nlFua8QmyM+TO/Gja2TTc2Z95C5ua" - "ewGH6cJi6bJO6Z+TY276eH3tbgy+/3ly3Js+rj66osG/A" - "V5htgaQ9SeRAAAAAElFTkSuQmCC">>; -image_base64(<<"powered-by-ejabberd.png">>) -> - <<"iVBORw0KGgoAAAANSUhEUgAAAGUAAAAfCAMAAADJG/NaA" - "AAAw1BMVEUAAAAjBgYtBAM5AwFCAAAYGAJNAABcAABIDQ" - "5qAAAoJRV7AACFAAAoKSdJHByLAAAwLwk1NQA1MzFJKyo" - "4NxtDQQBEQT5KSCxSTgBSUBlgQ0JYSEpZWQJPUU5hYABb" - "W0ZiYClcW1poaCVwbQRpaDhzYWNsakhuZ2VrbFZ8dwCEg" - "AB3dnd4d2+OjACDhYKcmACJi4iQkpWspgCYmJm5swCmqa" - "zEwACwsbS4ub3X0QLExsPLyszW1Nnc3ODm5ugMBwAWAwP" - "Hm1IFAAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHUgAAAAJ" - "cEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfVCRQOBA7VB" - "kCMAAACcElEQVRIx72WjXKiMBSFQalIFbNiy1pdrJZaRV" - "YR5deGwPs/VRNBSBB2OjvQO0oYjPfj5J6bCcdx8i2Uldx" - "KcDhk1HbIPwFBF/kHKJfjPSVAyIRHF9rRZ4sUX3EDdWOv" - "1+u2tESaavpnYTbv9zvd0WwDy3/QcGQXlH5uTxB1l07MJ" - "lRpsUei0JF6Qi+OHyGK7ijXxPklHe/umIllim3iUBMJDI" - "EULxxPP0TVWhhKJoN9fUpdmQLteV8aDgEAg9gIcTjL4F4" - "L+r4WVKEF+rbJdwYYAoQHY+oQjnGootyKwxapoi73WkyF" - "FySQBv988naEEp4+YMMec5VUCQDJTscEy7Kc0HsLmqNE7" - "rovDjMpIHHGYeidXn4TQcaxMYqP3RV3C8oCl2WvrlSPaN" - "pGZadRnmPGCk8ylM2okAJ4i9TEe1KersXxSl6jUt5uayi" - "IodirtcKLOaWblj50wiyMv1F9lm9TUDArGAD0FmEpvCUs" - "VoZy6dW81Fg0aDaHogQa36ekAPG5DDGsbdZrGsrzZUnzv" - "Bo1I2tLmuL69kSitAweyHKN9b3leDfQMnu3nIIKWfmXnq" - "GVKedJT6QpICbJvf2f8aOsvn68v+k7/cwUQdPoxaMoRTn" - "KFHNlKsKQphCTOa84u64vpi8bH31CqsbF6lSONRTkTyQG" - "Arq49/fEvjBwz4eDS2/JpaXRNOoXRD/VmOrDVTJJRIZCT" - "Lav3VrqbPvP3vdduGEhQJzilncbpSA4F3vsihErO+dayv" - "/sY5/yRE0GDEXCu2VoNiMlo5i+P2KlgMEvTNk2eYa5XEy" - "h12Ex17Z8vzQUR3KEPbYd6XG87eC4Ly75RneS5ZYHAAAA" - "AElFTkSuQmCC">>. - create_image_files(Images_dir) -> Filenames = [<<"powered-by-ejabberd.png">>, <<"powered-by-erlang.png">>, <<"valid-xhtml10.png">>, <<"vcss.png">>], - lists:foreach(fun (Filename) -> - Filename_full = fjoin([Images_dir, Filename]), - {ok, F} = file:open(Filename_full, [write]), - Image = base64:decode(image_base64(Filename)), - io:format(F, <<"~s">>, [Image]), - file:close(F) - end, - Filenames), - ok. + lists:foreach( + fun(Filename) -> + Src = filename:join([misc:img_dir(), Filename]), + Dst = fjoin([Images_dir, Filename]), + case file:copy(Src, Dst) of + {ok, _} -> ok; + {error, Why} -> + ?ERROR_MSG("Failed to copy ~s to ~s", + [Src, Dst, file:format_error(Why)]) + end + end, Filenames). fw(F, S) -> fw(F, S, [], html). @@ -768,77 +603,10 @@ put_header(F, Room, Date, CSSFile, Lang, Hour_offset, put_header_css(F, false) -> fw(F, <<"<style type=\"text/css\">">>), fw(F, <<"<!--">>), - fw(F, - <<".ts {color: #AAAAAA; text-decoration: " - "none;}">>), - fw(F, - <<".mrcm {color: #009900; font-style: italic; " - "font-weight: bold;}">>), - fw(F, - <<".msc {color: #009900; font-style: italic; " - "font-weight: bold;}">>), - fw(F, - <<".msm {color: #000099; font-style: italic; " - "font-weight: bold;}">>), - fw(F, <<".mj {color: #009900; font-style: italic;}">>), - fw(F, <<".ml {color: #009900; font-style: italic;}">>), - fw(F, <<".mk {color: #009900; font-style: italic;}">>), - fw(F, <<".mb {color: #009900; font-style: italic;}">>), - fw(F, <<".mnc {color: #009900; font-style: italic;}">>), - fw(F, <<".mn {color: #0000AA;}">>), - fw(F, <<".mne {color: #AA0099;}">>), - fw(F, - <<"a.nav {color: #AAAAAA; font-family: " - "monospace; letter-spacing: 3px; text-decorati" - "on: none;}">>), - fw(F, - <<"div.roomtitle {border-bottom: #224466 " - "solid 3pt; margin-left: 20pt;}">>), - fw(F, - <<"div.roomtitle {color: #336699; font-size: " - "24px; font-weight: bold; font-family: " - "sans-serif; letter-spacing: 3px; text-decorat" - "ion: none;}">>), - fw(F, - <<"a.roomjid {color: #336699; font-size: " - "24px; font-weight: bold; font-family: " - "sans-serif; letter-spacing: 3px; margin-left: " - "20pt; text-decoration: none;}">>), - fw(F, - <<"div.logdate {color: #663399; font-size: " - "20px; font-weight: bold; font-family: " - "sans-serif; letter-spacing: 2px; border-botto" - "m: #224466 solid 1pt; margin-left:80pt; " - "margin-top:20px;}">>), - fw(F, - <<"div.roomsubject {color: #336699; font-size: " - "18px; font-family: sans-serif; margin-left: " - "80pt; margin-bottom: 10px;}">>), - fw(F, - <<"div.rc {color: #336699; font-size: 12px; " - "font-family: sans-serif; margin-left: " - "50%; text-align: right; background: " - "#f3f6f9; border-bottom: 1px solid #336699; " - "border-right: 4px solid #336699;}">>), - fw(F, - <<"div.rct {font-weight: bold; background: " - "#e3e6e9; padding-right: 10px;}">>), - fw(F, <<"div.rcos {padding-right: 10px;}">>), - fw(F, <<"div.rcoe {color: green;}">>), - fw(F, <<"div.rcod {color: red;}">>), - fw(F, <<"div.rcoe:after {content: \": v\";}">>), - fw(F, <<"div.rcod:after {content: \": x\";}">>), - fw(F, <<"div.rcot:after {}">>), - fw(F, - <<".legend {width: 100%; margin-top: 30px; " - "border-top: #224466 solid 1pt; padding: " - "10px 0px 10px 0px; text-align: left; " - "font-family: monospace; letter-spacing: " - "2px;}">>), - fw(F, - <<".w3c {position: absolute; right: 10px; " - "width: 60%; text-align: right; font-family: " - "monospace; letter-spacing: 1px;}">>), + case misc:read_css("muc.css") of + {ok, Data} -> fw(F, Data); + {error, _} -> ok + end, fw(F, <<"//-->">>), fw(F, <<"</style>">>); put_header_css(F, CSSFile) -> @@ -849,16 +617,10 @@ put_header_css(F, CSSFile) -> put_header_script(F) -> fw(F, <<"<script type=\"text/javascript\">">>), - fw(F, <<"function sh(e) // Show/Hide an element">>), - fw(F, - <<"{if(document.getElementById(e).style.display=" - "='none')">>), - fw(F, - <<"{document.getElementById(e).style.display='bl" - "ock';}">>), - fw(F, - <<"else {document.getElementById(e).style.displa" - "y='none';}}">>), + case misc:read_js("muc.js") of + {ok, Data} -> fw(F, Data); + {error, _} -> ok + end, fw(F, <<"</script>">>). put_room_config(_F, _RoomConfig, _Lang, plaintext) -> @@ -952,7 +714,7 @@ get_room_info(RoomJID, Opts) -> false -> <<"">> end, Subject = case lists:keysearch(subject, 1, Opts) of - {value, {_, S}} -> S; + {value, {_, S}} -> xmpp:get_text(S); false -> <<"">> end, SubjectAuthor = case lists:keysearch(subject_author, 1, @@ -974,10 +736,9 @@ roomconfig_to_string(Options, Lang, FileFormat) -> Os2 = lists:sort(Os1), Options2 = Title ++ Os2, lists:foldl(fun ({Opt, Val}, R) -> - case get_roomconfig_text(Opt) of + case get_roomconfig_text(Opt, Lang) of undefined -> R; - OptT -> - OptText = (?T(OptT)), + OptText -> R2 = case Val of false -> <<"<div class=\"rcod\">", @@ -1025,49 +786,49 @@ roomconfig_to_string(Options, Lang, FileFormat) -> end, <<"">>, Options2). -get_roomconfig_text(title) -> <<"Room title">>; -get_roomconfig_text(persistent) -> - <<"Make room persistent">>; -get_roomconfig_text(public) -> - <<"Make room public searchable">>; -get_roomconfig_text(public_list) -> - <<"Make participants list public">>; -get_roomconfig_text(password_protected) -> - <<"Make room password protected">>; -get_roomconfig_text(password) -> <<"Password">>; -get_roomconfig_text(anonymous) -> - <<"This room is not anonymous">>; -get_roomconfig_text(members_only) -> - <<"Make room members-only">>; -get_roomconfig_text(moderated) -> - <<"Make room moderated">>; -get_roomconfig_text(members_by_default) -> - <<"Default users as participants">>; -get_roomconfig_text(allow_change_subj) -> - <<"Allow users to change the subject">>; -get_roomconfig_text(allow_private_messages) -> - <<"Allow users to send private messages">>; -get_roomconfig_text(allow_private_messages_from_visitors) -> - <<"Allow visitors to send private messages to">>; -get_roomconfig_text(allow_query_users) -> - <<"Allow users to query other users">>; -get_roomconfig_text(allow_user_invites) -> - <<"Allow users to send invites">>; -get_roomconfig_text(logging) -> <<"Enable logging">>; -get_roomconfig_text(allow_visitor_nickchange) -> - <<"Allow visitors to change nickname">>; -get_roomconfig_text(allow_visitor_status) -> - <<"Allow visitors to send status text in " - "presence updates">>; -get_roomconfig_text(captcha_protected) -> - <<"Make room captcha protected">>; -get_roomconfig_text(description) -> - <<"Room description">>; -%% get_roomconfig_text(subject) -> "Subject"; -%% get_roomconfig_text(subject_author) -> "Subject author"; -get_roomconfig_text(max_users) -> - <<"Maximum Number of Occupants">>; -get_roomconfig_text(_) -> undefined. +get_roomconfig_text(title, Lang) -> ?T(<<"Room title">>); +get_roomconfig_text(persistent, Lang) -> + ?T(<<"Make room persistent">>); +get_roomconfig_text(public, Lang) -> + ?T(<<"Make room public searchable">>); +get_roomconfig_text(public_list, Lang) -> + ?T(<<"Make participants list public">>); +get_roomconfig_text(password_protected, Lang) -> + ?T(<<"Make room password protected">>); +get_roomconfig_text(password, Lang) -> ?T(<<"Password">>); +get_roomconfig_text(anonymous, Lang) -> + ?T(<<"This room is not anonymous">>); +get_roomconfig_text(members_only, Lang) -> + ?T(<<"Make room members-only">>); +get_roomconfig_text(moderated, Lang) -> + ?T(<<"Make room moderated">>); +get_roomconfig_text(members_by_default, Lang) -> + ?T(<<"Default users as participants">>); +get_roomconfig_text(allow_change_subj, Lang) -> + ?T(<<"Allow users to change the subject">>); +get_roomconfig_text(allow_private_messages, Lang) -> + ?T(<<"Allow users to send private messages">>); +get_roomconfig_text(allow_private_messages_from_visitors, Lang) -> + ?T(<<"Allow visitors to send private messages to">>); +get_roomconfig_text(allow_query_users, Lang) -> + ?T(<<"Allow users to query other users">>); +get_roomconfig_text(allow_user_invites, Lang) -> + ?T(<<"Allow users to send invites">>); +get_roomconfig_text(logging, Lang) -> ?T(<<"Enable logging">>); +get_roomconfig_text(allow_visitor_nickchange, Lang) -> + ?T(<<"Allow visitors to change nickname">>); +get_roomconfig_text(allow_visitor_status, Lang) -> + ?T(<<"Allow visitors to send status text in " + "presence updates">>); +get_roomconfig_text(captcha_protected, Lang) -> + ?T(<<"Make room CAPTCHA protected">>); +get_roomconfig_text(description, Lang) -> + ?T(<<"Room description">>); +%% get_roomconfig_text(subject, Lang) -> "Subject"; +%% get_roomconfig_text(subject_author, Lang) -> "Subject author"; +get_roomconfig_text(max_users, Lang) -> + ?T(<<"Maximum Number of Occupants">>); +get_roomconfig_text(_, _) -> undefined. %% Users = [{JID, Nick, Role}] roomoccupants_to_string(Users, _FileFormat) -> diff --git a/src/mod_muc_mnesia.erl b/src/mod_muc_mnesia.erl index 015c5ec43..aa59038c9 100644 --- a/src/mod_muc_mnesia.erl +++ b/src/mod_muc_mnesia.erl @@ -28,12 +28,13 @@ -behaviour(mod_muc_room). %% API --export([init/2, import/3, store_room/4, restore_room/3, forget_room/3, +-export([init/2, import/3, store_room/5, restore_room/3, forget_room/3, can_use_nick/4, get_rooms/2, get_nick/3, set_nick/4]). -export([register_online_room/4, unregister_online_room/4, find_online_room/3, get_online_rooms/3, count_online_rooms/2, rsm_supported/0, register_online_user/4, unregister_online_user/4, - count_online_rooms_by_user/3, get_online_rooms_by_user/3]). + count_online_rooms_by_user/3, get_online_rooms_by_user/3, + get_subscribed_rooms/3]). -export([set_affiliation/6, set_affiliations/4, get_affiliation/5, get_affiliations/3, search_affiliation/4]). %% gen_server callbacks @@ -63,7 +64,7 @@ start_link(Host, Opts) -> Name = gen_mod:get_module_proc(Host, ?MODULE), gen_server:start_link({local, Name}, ?MODULE, [Host, Opts], []). -store_room(_LServer, Host, Name, Opts) -> +store_room(_LServer, Host, Name, Opts, _) -> F = fun () -> mnesia:write(#muc_room{name_host = {Name, Host}, opts = Opts}) @@ -397,3 +398,6 @@ transform(#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)}. + +get_subscribed_rooms(_, _, _) -> + not_implemented. diff --git a/src/mod_muc_riak.erl b/src/mod_muc_riak.erl index 42e644fdd..57d9666bf 100644 --- a/src/mod_muc_riak.erl +++ b/src/mod_muc_riak.erl @@ -28,12 +28,13 @@ -behaviour(mod_muc_room). %% API --export([init/2, import/3, store_room/4, restore_room/3, forget_room/3, +-export([init/2, import/3, store_room/5, restore_room/3, forget_room/3, can_use_nick/4, get_rooms/2, get_nick/3, set_nick/4]). -export([register_online_room/4, unregister_online_room/4, find_online_room/3, get_online_rooms/3, count_online_rooms/2, rsm_supported/0, register_online_user/4, unregister_online_user/4, - count_online_rooms_by_user/3, get_online_rooms_by_user/3]). + count_online_rooms_by_user/3, get_online_rooms_by_user/3, + get_subscribed_rooms/3]). -export([set_affiliation/6, set_affiliations/4, get_affiliation/5, get_affiliations/3, search_affiliation/4]). @@ -46,7 +47,7 @@ init(_Host, _Opts) -> ok. -store_room(_LServer, Host, Name, Opts) -> +store_room(_LServer, Host, Name, Opts, _) -> {atomic, ejabberd_riak:put(#muc_room{name_host = {Name, Host}, opts = Opts}, muc_room_schema())}. @@ -183,6 +184,9 @@ import(_LServer, <<"muc_registered">>, ejabberd_riak:put(R, muc_registered_schema(), [{'2i', [{<<"nick_host">>, {Nick, RoomHost}}]}]). +get_subscribed_rooms(_, _, _) -> + not_implemented. + %%%=================================================================== %%% Internal functions %%%=================================================================== diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl index fde43694c..bafa938dc 100644 --- a/src/mod_muc_room.erl +++ b/src/mod_muc_room.erl @@ -251,7 +251,7 @@ normal_state({route, <<"">>, try xmpp:decode_els(Packet) of Pkt -> process_normal_message(From, Pkt, StateData) catch _:{xmpp_codec, Why} -> - Txt = xmpp:format_error(Why), + Txt = xmpp:io_format_error(Why), Err = xmpp:err_bad_request(Txt, Lang), ejabberd_router:route_error(Packet, Err), StateData @@ -329,7 +329,7 @@ normal_state({route, <<"">>, end end catch _:{xmpp_codec, Why} -> - ErrTxt = xmpp:format_error(Why), + ErrTxt = xmpp:io_format_error(Why), Err = xmpp:err_bad_request(ErrTxt, Lang), ejabberd_router:route_error(IQ0, Err) end; @@ -433,27 +433,31 @@ normal_state({route, ToNick, {next_state, normal_state, StateData} end; normal_state({route, ToNick, - #iq{from = From, id = StanzaId, lang = Lang} = Packet}, + #iq{from = From, type = Type, lang = Lang} = Packet}, StateData) -> case {(StateData#state.config)#config.allow_query_users, - is_user_online_iq(StanzaId, From, StateData)} of - {true, {true, NewId, FromFull}} -> + (?DICT):find(jid:tolower(From), StateData#state.users)} of + {true, {ok, #user{nick = FromNick}}} -> case find_jid_by_nick(ToNick, StateData) of false -> ErrText = <<"Recipient is not in the conference room">>, Err = xmpp:err_item_not_found(ErrText, Lang), ejabberd_router:route_error(Packet, Err); - ToJID -> - {ok, #user{nick = FromNick}} = - (?DICT):find(jid:tolower(FromFull), StateData#state.users), - {ToJID2, Packet2} = handle_iq_vcard(ToJID, NewId, Packet), - ejabberd_router:route( - xmpp:set_from_to( - Packet2, - jid:replace_resource(StateData#state.jid, FromNick), - ToJID2)) + To -> + FromJID = jid:replace_resource(StateData#state.jid, FromNick), + if Type == get; Type == set -> + ToJID = case is_vcard_request(Packet) of + true -> jid:remove_resource(To); + false -> To + end, + ejabberd_router:route_iq( + xmpp:set_from_to(Packet, FromJID, ToJID), Packet, self()); + true -> + ejabberd_router:route( + xmpp:set_from_to(Packet, FromJID, To)) + end end; - {_, {false, _, _}} -> + {true, error} -> ErrText = <<"Only occupants are allowed to send queries " "to the conference">>, Err = xmpp:err_not_acceptable(ErrText, Lang), @@ -660,6 +664,18 @@ handle_info({captcha_failed, From}, normal_state, {next_state, normal_state, NewState}; handle_info(shutdown, _StateName, StateData) -> {stop, shutdown, StateData}; +handle_info({iq_reply, #iq{type = Type, sub_els = Els}, + #iq{from = From, to = To} = IQ}, StateName, StateData) -> + ejabberd_router:route( + xmpp:set_from_to( + IQ#iq{type = Type, sub_els = Els}, + To, From)), + {next_state, StateName, StateData}; +handle_info({iq_reply, timeout, IQ}, StateName, StateData) -> + Txt = <<"Request has timed out">>, + Err = xmpp:err_recipient_unavailable(Txt, IQ#iq.lang), + ejabberd_router:route_error(IQ, Err), + {next_state, StateName, StateData}; handle_info(_Info, StateName, StateData) -> {next_state, StateName, StateData}. @@ -717,7 +733,7 @@ process_groupchat_message(#message{from = From, lang = Lang} = Packet, StateData ((StateData#state.config)#config.moderated == false) -> Subject = check_subject(Packet), {NewStateData1, IsAllowed} = case Subject of - false -> {StateData, true}; + [] -> {StateData, true}; _ -> case can_change_subject(Role, @@ -749,7 +765,7 @@ process_groupchat_message(#message{from = From, lang = Lang} = Packet, StateData {next_state, normal_state, StateData}; NewPacket1 -> NewPacket = xmpp:remove_subtag(NewPacket1, #nick{}), - Node = if Subject == false -> ?NS_MUCSUB_NODES_MESSAGES; + Node = if Subject == [] -> ?NS_MUCSUB_NODES_MESSAGES; true -> ?NS_MUCSUB_NODES_SUBJECT end, send_wrapped_multiple( @@ -920,6 +936,12 @@ process_voice_approval(From, Pkt, VoiceApproval, StateData) -> StateData end. +-spec is_vcard_request(iq()) -> boolean(). +is_vcard_request(#iq{type = T, sub_els = [El]}) -> + (T == get orelse T == set) andalso xmpp:get_ns(El) == ?NS_VCARD; +is_vcard_request(_) -> + false. + %% @doc Check if this non participant can send message to room. %% %% XEP-0045 v1.23: @@ -1014,7 +1036,13 @@ do_process_presence(Nick, #presence{from = From, type = available, lang = Lang} From, Packet, StateData), NewState = add_user_presence(From, Stanza, StateData), - send_new_presence(From, NewState, StateData), + case xmpp:has_subtag(Packet, #muc{}) of + true -> + send_initial_presences_and_messages( + From, Nick, Packet, NewState, StateData); + false -> + send_new_presence(From, NewState, StateData) + end, NewState end end; @@ -1028,8 +1056,16 @@ do_process_presence(Nick, #presence{from = From, type = unavailable} = Packet, end, NewState = add_user_presence_un(From, NewPacket, StateData), case (?DICT):find(Nick, StateData#state.nicks) of - {ok, [_, _ | _]} -> ok; - _ -> send_new_presence(From, NewState, StateData) + {ok, [_, _ | _]} -> + Aff = get_affiliation(From, StateData), + Item = #muc_item{affiliation = Aff, role = none, jid = From}, + Pres = xmpp:set_subtag( + Packet, #muc_user{items = [Item], + status_codes = [110]}), + send_wrapped(jid:replace_resource(StateData#state.jid, Nick), + From, Pres, ?NS_MUCSUB_NODES_PRESENCE, StateData); + _ -> + send_new_presence(From, NewState, StateData) end, Reason = xmpp:get_text(NewPacket#presence.status), remove_online_user(From, NewState, Reason); @@ -1115,59 +1151,6 @@ is_occupant_or_admin(JID, StateData) -> _ -> false end. -%%% -%%% Handle IQ queries of vCard -%%% --spec is_user_online_iq(binary(), jid(), state()) -> - {boolean(), binary(), jid()}. -is_user_online_iq(StanzaId, JID, StateData) - when JID#jid.lresource /= <<"">> -> - {is_user_online(JID, StateData), StanzaId, JID}; -is_user_online_iq(StanzaId, JID, StateData) - when JID#jid.lresource == <<"">> -> - try stanzaid_unpack(StanzaId) of - {OriginalId, Resource} -> - JIDWithResource = jid:replace_resource(JID, Resource), - {is_user_online(JIDWithResource, StateData), OriginalId, - JIDWithResource} - catch - _:_ -> {is_user_online(JID, StateData), StanzaId, JID} - end. - --spec handle_iq_vcard(jid(), binary(), iq()) -> {jid(), iq()}. -handle_iq_vcard(ToJID, NewId, #iq{type = Type, sub_els = SubEls} = IQ) -> - ToBareJID = jid:remove_resource(ToJID), - case SubEls of - [SubEl] when Type == get, ToBareJID /= ToJID -> - case xmpp:get_ns(SubEl) of - ?NS_VCARD -> - {ToBareJID, change_stanzaid(ToJID, IQ)}; - _ -> - {ToJID, xmpp:set_id(IQ, NewId)} - end; - _ -> - {ToJID, xmpp:set_id(IQ, NewId)} - end. - --spec stanzaid_pack(binary(), binary()) -> binary(). -stanzaid_pack(OriginalId, Resource) -> - <<"berd", - (base64:encode(<<"ejab\000", - OriginalId/binary, "\000", - Resource/binary>>))/binary>>. - --spec stanzaid_unpack(binary()) -> {binary(), binary()}. -stanzaid_unpack(<<"berd", StanzaIdBase64/binary>>) -> - StanzaId = base64:decode(StanzaIdBase64), - [<<"ejab">>, OriginalId, Resource] = - str:tokens(StanzaId, <<"\000">>), - {OriginalId, Resource}. - --spec change_stanzaid(jid(), iq()) -> iq(). -change_stanzaid(ToJID, #iq{id = PreviousId} = Packet) -> - NewId = stanzaid_pack(PreviousId, ToJID#jid.lresource), - xmpp:set_id(Packet, NewId). - %% Decide the fate of the message and its sender %% Returns: continue_delivery | forget_message | {expulse_sender, Reason} -spec decide_fate_message(message(), jid(), state()) -> @@ -1227,7 +1210,7 @@ get_error_condition(undefined) -> -spec get_error_text(stanza_error()) -> binary(). get_error_text(#stanza_error{text = Txt}) -> - xmpp:get_text([Txt]). + xmpp:get_text(Txt). -spec make_reason(stanza(), jid(), state(), binary()) -> binary(). make_reason(Packet, From, StateData, Reason1) -> @@ -1243,7 +1226,20 @@ expulse_participant(Packet, From, StateData, Reason1) -> #presence{type = unavailable, status = xmpp:mk_text(Reason2)}, StateData), - send_new_presence(From, NewState, StateData), + LJID = jid:tolower(From), + {ok, #user{nick = Nick}} = (?DICT):find(LJID, StateData#state.users), + case (?DICT):find(Nick, StateData#state.nicks) of + {ok, [_, _ | _]} -> + Aff = get_affiliation(From, StateData), + Item = #muc_item{affiliation = Aff, role = none, jid = From}, + Pres = xmpp:set_subtag( + Packet, #muc_user{items = [Item], + status_codes = [110]}), + send_wrapped(jid:replace_resource(StateData#state.jid, Nick), + From, Pres, ?NS_MUCSUB_NODES_PRESENCE, StateData); + _ -> + send_new_presence(From, NewState, StateData) + end, remove_online_user(From, NewState). -spec set_affiliation(jid(), affiliation(), state()) -> state(). @@ -1597,7 +1593,13 @@ set_subscriber(JID, Nick, Nodes, StateData) -> Nicks = ?DICT:store(Nick, [LBareJID], StateData#state.subscriber_nicks), NewStateData = StateData#state{subscribers = Subscribers, subscriber_nicks = Nicks}, - store_room(NewStateData), + store_room(NewStateData, [{add_subscription, BareJID, Nick, Nodes}]), + case not ?DICT:is_key(LBareJID, StateData#state.subscribers) of + true -> + send_subscriptions_change_notifications(BareJID, Nick, subscribe, NewStateData); + _ -> + ok + end, NewStateData. -spec add_online_user(jid(), binary(), role(), state()) -> state(). @@ -1856,11 +1858,8 @@ add_new_user(From, Nick, Packet, StateData) -> From, Packet, add_online_user(From, Nick, Role, StateData)), - send_existing_presences(From, NewState), - send_initial_presence(From, NewState, StateData), - History = get_history(Nick, Packet, NewState), - send_history(From, History, NewState), - send_subject(From, StateData), + send_initial_presences_and_messages( + From, Nick, Packet, NewState, StateData), NewState; true -> set_subscriber(From, Nick, Nodes, StateData) @@ -2055,6 +2054,15 @@ presence_broadcast_allowed(JID, StateData) -> Role = get_role(JID, StateData), lists:member(Role, (StateData#state.config)#config.presence_broadcast). +-spec send_initial_presences_and_messages( + jid(), binary(), presence(), state(), state()) -> ok. +send_initial_presences_and_messages(From, Nick, Presence, NewState, OldState) -> + send_existing_presences(From, NewState), + send_initial_presence(From, NewState, OldState), + History = get_history(Nick, Presence, NewState), + send_history(From, History, NewState), + send_subject(From, OldState). + -spec send_initial_presence(jid(), state(), state()) -> ok. send_initial_presence(NJID, StateData, OldStateData) -> send_new_presence1(NJID, <<"">>, true, StateData, OldStateData). @@ -2420,7 +2428,7 @@ lqueue_cut(Q, N) -> add_message_to_history(FromNick, FromJID, Packet, StateData) -> add_to_log(text, {FromNick, Packet}, StateData), case check_subject(Packet) of - false -> + [] -> TimeStamp = p1_time_compat:timestamp(), AddrPacket = case (StateData#state.config)#config.anonymous of true -> Packet; @@ -2459,19 +2467,19 @@ send_history(JID, History, StateData) -> -spec send_subject(jid(), state()) -> ok. send_subject(JID, #state{subject_author = Nick} = StateData) -> Subject = case StateData#state.subject of - <<"">> -> [#text{}]; - Subj -> xmpp:mk_text(Subj) + [] -> [#text{}]; + [_|_] = S -> S end, Packet = #message{from = jid:replace_resource(StateData#state.jid, Nick), to = JID, type = groupchat, subject = Subject}, ejabberd_router:route(Packet). --spec check_subject(message()) -> false | binary(). +-spec check_subject(message()) -> [text()]. check_subject(#message{subject = [_|_] = Subj, body = [], thread = undefined}) -> - xmpp:get_text(Subj); + Subj; check_subject(_) -> - false. + []. -spec can_change_subject(role(), boolean(), state()) -> boolean(). can_change_subject(Role, IsSubscriber, StateData) -> @@ -2691,14 +2699,14 @@ find_changed_items(UJID, UAffiliation, URole, [#muc_item{jid = J, nick = Nick, reason = Reason, role = Role, affiliation = Affiliation}|Items], Lang, StateData, Res) -> - [JID | _] = JIDs = + [JID | _] = JIDs = if J /= undefined -> [J]; Nick /= <<"">> -> case find_jids_by_nick(Nick, StateData) of [] -> - ErrText = str:format(<<"Nickname ~s does not exist in the room">>, - [Nick]), + ErrText = {<<"Nickname ~s does not exist in the room">>, + [Nick]}, throw({error, xmpp:err_not_acceptable(ErrText, Lang)}); JIDList -> JIDList @@ -3296,8 +3304,7 @@ change_config(Config, StateData) -> Config#config.persistent} of {_, true} -> - mod_muc:store_room(NSD#state.server_host, - NSD#state.host, NSD#state.room, make_opts(NSD)); + store_room(NSD); {true, false} -> mod_muc:forget_room(NSD#state.server_host, NSD#state.host, NSD#state.room); @@ -3495,7 +3502,12 @@ set_opts([{Opt, Val} | Opts], StateData) -> subscriber_nicks = Nicks}; affiliations -> StateData#state{affiliations = (?DICT):from_list(Val)}; - subject -> StateData#state{subject = Val}; + subject -> + Subj = if Val == <<"">> -> []; + is_binary(Val) -> [#text{data = Val}]; + is_list(Val) -> Val + end, + StateData#state{subject = Subj}; subject_author -> StateData#state{subject_author = Val}; _ -> StateData end, @@ -3605,7 +3617,7 @@ process_iq_disco_info(_From, #iq{type = get, lang = Lang}, StateData) -> ++ case {gen_mod:is_loaded(StateData#state.server_host, mod_mam), Config#config.mam} of {true, true} -> - [?NS_MAM_TMP, ?NS_MAM_0, ?NS_MAM_1]; + [?NS_MAM_TMP, ?NS_MAM_0, ?NS_MAM_1, ?NS_MAM_2, ?NS_SID_0]; _ -> [] end, @@ -3761,7 +3773,8 @@ process_iq_mucsub(From, #iq{type = set, sub_els = [#muc_unsubscribe{}]}, Subscribers = ?DICT:erase(LBareJID, StateData#state.subscribers), NewStateData = StateData#state{subscribers = Subscribers, subscriber_nicks = Nicks}, - store_room(NewStateData), + store_room(NewStateData, [{del_subscription, LBareJID}]), + send_subscriptions_change_notifications(LBareJID, Nick, unsubscribe, StateData), NewStateData2 = case close_room_if_temporary_and_empty(NewStateData) of {stop, normal, _} -> stop; {next_state, normal_state, SD} -> SD @@ -3806,7 +3819,8 @@ get_subscription_nodes(#iq{sub_els = [#muc_subscribe{events = Nodes}]}) -> ?NS_MUCSUB_NODES_AFFILIATIONS, ?NS_MUCSUB_NODES_SUBJECT, ?NS_MUCSUB_NODES_CONFIG, - ?NS_MUCSUB_NODES_PARTICIPANTS]) + ?NS_MUCSUB_NODES_PARTICIPANTS, + ?NS_MUCSUB_NODES_SUBSCRIBERS]) end, Nodes); get_subscription_nodes(_) -> []. @@ -4027,14 +4041,51 @@ element_size(El) -> -spec store_room(state()) -> ok. store_room(StateData) -> + store_room(StateData, []). +store_room(StateData, ChangesHints) -> if (StateData#state.config)#config.persistent -> mod_muc:store_room(StateData#state.server_host, StateData#state.host, StateData#state.room, - make_opts(StateData)); + make_opts(StateData), + ChangesHints); true -> ok end. +-spec send_subscriptions_change_notifications(jid(), binary(), subscribe|unsubscribe, state()) -> ok. +send_subscriptions_change_notifications(From, Nick, Type, State) -> + ?DICT:fold(fun(_, #subscriber{nodes = Nodes, jid = JID}, _) -> + case lists:member(?NS_MUCSUB_NODES_SUBSCRIBERS, Nodes) of + true -> + ShowJid = case (State#state.config)#config.anonymous == false orelse + get_role(JID, State) == moderator orelse + get_default_role(get_affiliation(JID, State), State) == moderator of + true -> true; + _ -> false + end, + Payload = case {Type, ShowJid} of + {subscribe, true} -> + #muc_subscribe{jid = From, nick = Nick}; + {subscribe, _} -> + #muc_subscribe{nick = Nick}; + {unsubscribe, true} -> + #muc_unsubscribe{jid = From, nick = Nick}; + {unsubscribe, _} -> + #muc_unsubscribe{nick = Nick} + end, + Packet = #message{ + sub_els = [#ps_event{ + items = #ps_items{ + node = ?NS_MUCSUB_NODES_SUBSCRIBERS, + items = [#ps_item{ + id = randoms:get_string(), + xml_els = [xmpp:encode(Payload)]}]}}]}, + ejabberd_router:route(xmpp:set_from_to(Packet, From, JID)); + false -> + ok + end + end, ok, State#state.subscribers). + -spec send_wrapped(jid(), jid(), stanza(), binary(), state()) -> ok. send_wrapped(From, To, Packet, Node, State) -> LTo = jid:tolower(To), diff --git a/src/mod_muc_sql.erl b/src/mod_muc_sql.erl index 94d5706b8..8aa6071c8 100644 --- a/src/mod_muc_sql.erl +++ b/src/mod_muc_sql.erl @@ -30,13 +30,14 @@ -behaviour(mod_muc_room). %% API --export([init/2, store_room/4, restore_room/3, forget_room/3, +-export([init/2, store_room/5, restore_room/3, forget_room/3, can_use_nick/4, get_rooms/2, get_nick/3, set_nick/4, import/3, export/1]). -export([register_online_room/4, unregister_online_room/4, find_online_room/3, get_online_rooms/3, count_online_rooms/2, rsm_supported/0, register_online_user/4, unregister_online_user/4, - count_online_rooms_by_user/3, get_online_rooms_by_user/3]). + count_online_rooms_by_user/3, get_online_rooms_by_user/3, + get_subscribed_rooms/3]). -export([set_affiliation/6, set_affiliations/4, get_affiliation/5, get_affiliations/3, search_affiliation/4]). @@ -56,24 +57,79 @@ init(Host, Opts) -> ok end. -store_room(LServer, Host, Name, Opts) -> - SOpts = misc:term_to_expr(Opts), +store_room(LServer, Host, Name, Opts, ChangesHints) -> + {Subs, Opts2} = case lists:keytake(subscribers, 1, Opts) of + {value, {subscribers, S}, OptN} -> {S, OptN}; + _ -> {[], Opts} + end, + SOpts = misc:term_to_expr(Opts2), F = fun () -> ?SQL_UPSERT_T( "muc_room", ["!name=%(Name)s", "!host=%(Host)s", - "opts=%(SOpts)s"]) + "server_host=%(LServer)s", + "opts=%(SOpts)s"]), + case ChangesHints of + Changes when is_list(Changes) -> + [change_room(Host, Name, Change) || Change <- Changes]; + _ -> + ejabberd_sql:sql_query_t( + ?SQL("delete from muc_room_subscribers where " + "room=%(Name)s and host=%(Host)s")), + [change_room(Host, Name, {add_subscription, JID, Nick, Nodes}) + || {JID, Nick, Nodes} <- Subs] + end end, ejabberd_sql:sql_transaction(LServer, F). +change_room(Host, Room, {add_subscription, JID, Nick, Nodes}) -> + SJID = jid:encode(JID), + SNodes = misc:term_to_expr(Nodes), + ?SQL_UPSERT_T( + "muc_room_subscribers", + ["!jid=%(SJID)s", + "!host=%(Host)s", + "!room=%(Room)s", + "nick=%(Nick)s", + "nodes=%(SNodes)s"]); +change_room(Host, Room, {del_subscription, JID}) -> + SJID = jid:encode(JID), + ejabberd_sql:sql_query_t(?SQL("delete from muc_room_subscribers where " + "room=%(Room)s and host=%(Host)s and jid=%(SJID)s")); +change_room(Host, Room, Change) -> + ?ERROR_MSG("Unsupported change on room ~s@~s: ~p", [Room, Host, Change]). + restore_room(LServer, Host, Name) -> case catch ejabberd_sql:sql_query( LServer, ?SQL("select @(opts)s from muc_room where name=%(Name)s" " and host=%(Host)s")) of {selected, [{Opts}]} -> - mod_muc:opts_to_binary(ejabberd_sql:decode_term(Opts)); + OptsD = ejabberd_sql:decode_term(Opts), + case catch ejabberd_sql:sql_query( + LServer, + ?SQL("select @(jid)s, @(nick)s, @(nodes)s from muc_room_subscribers where room=%(Name)s" + " and host=%(Host)s")) of + {selected, []} -> + OptsR = mod_muc:opts_to_binary(OptsD), + case lists:keymember(subscribers, 1, OptsD) of + true -> + store_room(LServer, Host, Name, OptsR, undefined); + _ -> + ok + end, + OptsR; + {selected, Subs} -> + SubData = lists:map( + fun({Jid, Nick, Nodes}) -> + {jid:decode(Jid), Nick, ejabberd_sql:decode_term(Nodes)} + end, Subs), + Opts2 = lists:keystore(subscribers, 1, OptsD, {subscribers, SubData}), + mod_muc:opts_to_binary(Opts2); + _ -> + error + end; _ -> error end. @@ -82,6 +138,9 @@ forget_room(LServer, Host, Name) -> F = fun () -> ejabberd_sql:sql_query_t( ?SQL("delete from muc_room where name=%(Name)s" + " and host=%(Host)s")), + ejabberd_sql:sql_query_t( + ?SQL("delete from muc_room_subscribers where room=%(Name)s" " and host=%(Host)s")) end, ejabberd_sql:sql_transaction(LServer, F). @@ -103,13 +162,36 @@ get_rooms(LServer, Host) -> ?SQL("select @(name)s, @(opts)s from muc_room" " where host=%(Host)s")) of {selected, RoomOpts} -> + case catch ejabberd_sql:sql_query( + LServer, + ?SQL("select @(room)s, @(jid)s, @(nick)s, @(nodes)s from muc_room_subscribers" + " where host=%(Host)s")) of + {selected, Subs} -> + SubsD = lists:foldl( + fun({Room, Jid, Nick, Nodes}, Dict) -> + dict:append(Room, {jid:decode(Jid), + Nick, ejabberd_sql:decode_term(Nodes)}, Dict) + end, dict:new(), Subs), lists:map( fun({Room, Opts}) -> + OptsD = ejabberd_sql:decode_term(Opts), + OptsD2 = case {dict:find(Room, SubsD), lists:keymember(subscribers, 1, OptsD)} of + {_, true} -> + store_room(LServer, Host, Room, mod_muc:opts_to_binary(OptsD), undefined), + OptsD; + {{ok, SubsI}, false} -> + lists:keystore(subscribers, 1, OptsD, {subscribers, SubsI}); + _ -> + OptsD + end, #muc_room{name_host = {Room, Host}, - opts = mod_muc:opts_to_binary( - ejabberd_sql:decode_term(Opts))} + opts = mod_muc:opts_to_binary(OptsD2)} end, RoomOpts); Err -> + ?ERROR_MSG("failed to get rooms subscribers: ~p", [Err]), + [] + end; + Err -> ?ERROR_MSG("failed to get rooms: ~p", [Err]), [] end. @@ -146,6 +228,7 @@ set_nick(LServer, Host, From, Nick) -> "muc_registered", ["!jid=%(JID)s", "!host=%(Host)s", + "server_host=%(LServer)s", "nick=%(Nick)s"]), ok; true -> @@ -177,6 +260,7 @@ register_online_room(ServerHost, Room, Host, Pid) -> "muc_online_room", ["!name=%(Room)s", "!host=%(Host)s", + "server_host=%(ServerHost)s", "node=%(NodeS)s", "pid=%(PidS)s"]) of ok -> @@ -251,6 +335,7 @@ register_online_user(ServerHost, {U, S, R}, Room, Host) -> "!resource=%(R)s", "!name=%(Room)s", "!host=%(Host)s", + "server_host=%(ServerHost)s", "node=%(NodeS)s"]) of ok -> ok; @@ -299,9 +384,12 @@ export(_Server) -> SOpts = misc:term_to_expr(Opts), [?SQL("delete from muc_room where name=%(Name)s" " and host=%(RoomHost)s;"), - ?SQL("insert into muc_room(name, host, opts) " - "values (" - "%(Name)s, %(RoomHost)s, %(SOpts)s);")]; + ?SQL_INSERT( + "muc_room", + ["name=%(Name)s", + "host=%(Host)s", + "server_host=%(Host)s", + "opts=%(SOpts)s"])]; false -> [] end @@ -314,9 +402,12 @@ export(_Server) -> SJID = jid:encode(jid:make(U, S)), [?SQL("delete from muc_registered where" " jid=%(SJID)s and host=%(RoomHost)s;"), - ?SQL("insert into muc_registered(jid, host, " - "nick) values (" - "%(SJID)s, %(RoomHost)s, %(Nick)s);")]; + ?SQL_INSERT( + "muc_registered", + ["jid=%(SJID)s", + "host=%(Host)s", + "server_host=%(Host)s", + "nick=%(Nick)s"])]; false -> [] end @@ -325,6 +416,19 @@ export(_Server) -> import(_, _, _) -> ok. +get_subscribed_rooms(LServer, Host, Jid) -> + JidS = jid:encode(Jid), + case catch ejabberd_sql:sql_query( + LServer, + ?SQL("select @(room)s from muc_room_subscribers where jid=%(JidS)s" + " and host=%(Host)s")) of + {selected, Subs} -> + [jid:make(Room, Host, <<>>) || {Room} <- Subs]; + Error -> + ?ERROR_MSG("Error when fetching subscribed rooms ~p", [Error]), + [] + end. + %%%=================================================================== %%% Internal functions %%%=================================================================== diff --git a/src/mod_multicast.erl b/src/mod_multicast.erl index e10315b7a..7b772521d 100644 --- a/src/mod_multicast.erl +++ b/src/mod_multicast.erl @@ -240,7 +240,7 @@ handle_iq(Packet, State) -> end catch _:{xmpp_codec, Why} -> Lang = xmpp:get_lang(Packet), - Err = xmpp:err_bad_request(xmpp:format_error(Why), Lang), + Err = xmpp:err_bad_request(xmpp:io_format_error(Why), Lang), ejabberd_router:route_error(Packet, Err) end. diff --git a/src/mod_offline.erl b/src/mod_offline.erl index 0be61f71f..5b95fe4b4 100644 --- a/src/mod_offline.erl +++ b/src/mod_offline.erl @@ -706,22 +706,25 @@ user_queue_parse_query(LUser, LServer, Query) -> Mod = gen_mod:db_mod(LServer, ?MODULE), case lists:keysearch(<<"delete">>, 1, Query) of {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; + user_queue_parse_query(LUser, LServer, Query, Mod); _ -> nothing end. +user_queue_parse_query(LUser, LServer, Query, Mod) -> + case lists:keytake(<<"selected">>, 1, Query) of + {value, {_, Seq}, Query2} -> + case catch binary_to_integer(Seq) of + I when is_integer(I), I>=0 -> + Mod:remove_message(LUser, LServer, I); + _ -> + nothing + end, + user_queue_parse_query(LUser, LServer, Query2, Mod); + false -> + nothing + end. + us_to_list({User, Server}) -> jid:encode({User, Server, <<"">>}). diff --git a/src/mod_offline_sql.erl b/src/mod_offline_sql.erl index f43f4c929..53a0d3451 100644 --- a/src/mod_offline_sql.erl +++ b/src/mod_offline_sql.erl @@ -56,8 +56,11 @@ store_message(#offline_msg{us = {LUser, LServer}} = M) -> xmpp:encode(NewPacket)), case ejabberd_sql:sql_query( LServer, - ?SQL("insert into spool(username, xml) values " - "(%(LUser)s, %(XML)s)")) of + ?SQL_INSERT( + "spool", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "xml=%(XML)s"])) of {updated, _} -> ok; _ -> @@ -87,10 +90,8 @@ remove_expired_messages(_LServer) -> remove_old_messages(Days, LServer) -> case ejabberd_sql:sql_query( LServer, - [<<"DELETE FROM spool" - " WHERE created_at < " - "NOW() - INTERVAL '">>, - integer_to_list(Days), <<"' DAY;">>]) of + ?SQL("DELETE FROM spool" + " WHERE created_at < NOW() - INTERVAL %(Days)d DAY")) of {updated, N} -> ?INFO_MSG("~p message(s) deleted from offline spool", [N]); _Error -> @@ -101,13 +102,13 @@ remove_old_messages(Days, LServer) -> remove_user(LUser, LServer) -> ejabberd_sql:sql_query( LServer, - ?SQL("delete from spool where username=%(LUser)s")). + ?SQL("delete from spool where username=%(LUser)s and %(LServer)H")). read_message_headers(LUser, LServer) -> case ejabberd_sql:sql_query( LServer, ?SQL("select @(xml)s, @(seq)d from spool" - " where username=%(LUser)s order by seq")) of + " where username=%(LUser)s and %(LServer)H order by seq")) of {selected, Rows} -> lists:flatmap( fun({XML, Seq}) -> @@ -129,6 +130,7 @@ read_message(LUser, LServer, Seq) -> case ejabberd_sql:sql_query( LServer, ?SQL("select @(xml)s from spool where username=%(LUser)s" + " and %(LServer)H" " and seq=%(Seq)d")) of {selected, [{RawXML}|_]} -> case xml_to_offline_msg(RawXML) of @@ -144,7 +146,7 @@ read_message(LUser, LServer, Seq) -> remove_message(LUser, LServer, Seq) -> ejabberd_sql:sql_query( LServer, - ?SQL("delete from spool where username=%(LUser)s" + ?SQL("delete from spool where username=%(LUser)s and %(LServer)H" " and seq=%(Seq)d")), ok. @@ -152,7 +154,7 @@ read_all_messages(LUser, LServer) -> case ejabberd_sql:sql_query( LServer, ?SQL("select @(xml)s from spool where " - "username=%(LUser)s order by seq")) of + "username=%(LUser)s and %(LServer)H order by seq")) of {selected, Rs} -> lists:flatmap( fun({XML}) -> @@ -173,7 +175,7 @@ count_messages(LUser, LServer) -> case catch ejabberd_sql:sql_query( LServer, ?SQL("select @(count(*))d from spool " - "where username=%(LUser)s")) of + "where username=%(LUser)s and %(LServer)H")) of {selected, [{Res}]} -> Res; _ -> 0 @@ -183,7 +185,8 @@ export(_Server) -> [{offline_msg, fun(Host, #offline_msg{us = {LUser, LServer}}) when LServer == Host -> - [?SQL("delete from spool where username=%(LUser)s;")]; + [?SQL("delete from spool where username=%(LUser)s" + " and %(LServer)H;")]; (_Host, _R) -> [] end}, @@ -199,8 +202,11 @@ export(_Server) -> Packet1, jid:make(LServer), TimeStamp, <<"Offline Storage">>), XML = fxml:element_to_binary(xmpp:encode(Packet2)), - [?SQL("insert into spool(username, xml) values (" - "%(LUser)s, %(XML)s);")] + [?SQL_INSERT( + "spool", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "xml=%(XML)s"])] catch _:{xmpp_codec, Why} -> ?ERROR_MSG("failed to decode packet ~p of user ~s@~s: ~s", [El, LUser, LServer, xmpp:format_error(Why)]), @@ -249,9 +255,10 @@ get_and_del_spool_msg_t(LServer, LUser) -> Result = ejabberd_sql:sql_query_t( ?SQL("select @(username)s, @(xml)s from spool where " - "username=%(LUser)s order by seq;")), + "username=%(LUser)s and %(LServer)H order by seq;")), ejabberd_sql:sql_query_t( - ?SQL("delete from spool where username=%(LUser)s;")), + ?SQL("delete from spool where" + " username=%(LUser)s and %(LServer)H;")), Result end, ejabberd_sql:sql_transaction(LServer, F). diff --git a/src/mod_ping.erl b/src/mod_ping.erl index 1c9639bf7..023571812 100644 --- a/src/mod_ping.erl +++ b/src/mod_ping.erl @@ -132,7 +132,7 @@ handle_cast({start_ping, JID}, State) -> handle_cast({stop_ping, JID}, State) -> Timers = del_timer(JID, State#state.timers), {noreply, State#state{timers = Timers}}; -handle_cast({iq_pong, JID, timeout}, State) -> +handle_cast({iq_reply, timeout, JID}, State) -> Timers = del_timer(JID, State#state.timers), ejabberd_hooks:run(user_ping_timeout, State#state.host, [JID]), @@ -149,20 +149,19 @@ handle_cast({iq_pong, JID, timeout}, State) -> _ -> ok end, {noreply, State#state{timers = Timers}}; -handle_cast({iq_pong, _JID, _}, State) -> +handle_cast({iq_reply, #iq{}, _JID}, State) -> {noreply, State}; handle_cast(Msg, State) -> ?WARNING_MSG("unexpected cast: ~p", [Msg]), {noreply, State}. handle_info({timeout, _TRef, {ping, JID}}, State) -> - From = jid:make(State#state.host), + Host = State#state.host, + From = jid:remove_resource(JID), IQ = #iq{from = From, to = JID, type = get, sub_els = [#ping{}]}, - Pid = self(), - F = fun (Response) -> - gen_server:cast(Pid, {iq_pong, JID, Response}) - end, - ejabberd_local:route_iq(IQ, F, State#state.ping_ack_timeout), + ejabberd_router:route_iq(IQ, JID, + gen_mod:get_module_proc(Host, ?MODULE), + State#state.ping_ack_timeout), Timers = add_timer(JID, State#state.ping_interval, State#state.timers), {noreply, State#state{timers = Timers}}; diff --git a/src/mod_privacy.erl b/src/mod_privacy.erl index 85384610d..64ae9620e 100644 --- a/src/mod_privacy.erl +++ b/src/mod_privacy.erl @@ -353,7 +353,7 @@ process_lists_set(#iq{from = #jid{luser = LUser, lserver = LServer} = From, lang = Lang} = IQ, Name, Items) -> case catch lists:map(fun decode_item/1, Items) of {error, Why} -> - Txt = xmpp:format_error(Why), + Txt = xmpp:io_format_error(Why), xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); List -> case set_list(LUser, LServer, Name, List) of @@ -638,25 +638,29 @@ is_ptype_match(Item, PType) -> ljid(), none | both | from | to, [binary()]) -> boolean(). is_type_match(none, _Value, _JID, _Subscription, _Groups) -> true; -is_type_match(Type, Value, JID, Subscription, Groups) -> - case Type of - jid -> - case Value of - {<<"">>, Server, <<"">>} -> - case JID of - {_, Server, _} -> true; - _ -> false - end; - {User, Server, <<"">>} -> - case JID of - {User, Server, _} -> true; - _ -> false - end; - _ -> Value == JID - end; - subscription -> Value == Subscription; - group -> lists:member(Value, Groups) - end. +is_type_match(jid, Value, JID, _Subscription, _Groups) -> + case Value of + {<<"">>, Server, <<"">>} -> + case JID of + {_, Server, _} -> true; + _ -> false + end; + {User, Server, <<"">>} -> + case JID of + {User, Server, _} -> true; + _ -> false + end; + {<<"">>, Server, Resource} -> + case JID of + {_, Server, Resource} -> true; + _ -> false + end; + _ -> Value == JID + end; +is_type_match(subscription, Value, _JID, Subscription, _Groups) -> + Value == Subscription; +is_type_match(group, Group, _JID, _Subscription, Groups) -> + lists:member(Group, Groups). -spec remove_user(binary(), binary()) -> ok. remove_user(User, Server) -> @@ -842,4 +846,12 @@ depends(_Host, _Opts) -> mod_opt_type(db_type) -> fun(T) -> ejabberd_config:v_db(?MODULE, T) end; mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; -mod_opt_type(_) -> [db_type, iqdisc]. +mod_opt_type(O) when O == cache_life_time; O == cache_size -> + fun (I) when is_integer(I), I > 0 -> I; + (infinity) -> infinity + end; +mod_opt_type(O) when O == use_cache; O == cache_missed -> + fun (B) when is_boolean(B) -> B end; +mod_opt_type(_) -> + [db_type, iqdisc, cache_life_time, cache_size, + use_cache, cache_missed]. diff --git a/src/mod_privacy_sql.erl b/src/mod_privacy_sql.erl index b19c95fe5..7939cbb26 100644 --- a/src/mod_privacy_sql.erl +++ b/src/mod_privacy_sql.erl @@ -56,13 +56,13 @@ unset_default(LUser, LServer) -> set_default(LUser, LServer, Name) -> F = fun () -> - case get_privacy_list_names_t(LUser) of + case get_privacy_list_names_t(LUser, LServer) of {selected, []} -> {error, notfound}; {selected, Names} -> case lists:member({Name}, Names) of true -> - set_default_privacy_list(LUser, Name); + set_default_privacy_list(LUser, LServer, Name); false -> {error, notfound} end @@ -72,14 +72,14 @@ set_default(LUser, LServer, Name) -> remove_list(LUser, LServer, Name) -> F = fun () -> - case get_default_privacy_list_t(LUser) of + case get_default_privacy_list_t(LUser, LServer) of {selected, []} -> - remove_privacy_list_t(LUser, Name); + remove_privacy_list_t(LUser, LServer, Name); {selected, [{Default}]} -> if Name == Default -> {error, conflict}; true -> - remove_privacy_list_t(LUser, Name) + remove_privacy_list_t(LUser, LServer, Name) end end end, @@ -91,13 +91,14 @@ set_lists(#privacy{us = {LUser, LServer}, F = fun() -> lists:foreach( fun({Name, List}) -> - add_privacy_list(LUser, Name), + add_privacy_list(LUser, LServer, Name), {selected, [<<"id">>], [[I]]} = - get_privacy_list_id_t(LUser, Name), + get_privacy_list_id_t(LUser, LServer, Name), RItems = lists:map(fun item_to_raw/1, List), set_privacy_list(I, RItems), if is_binary(Default) -> - set_default_privacy_list(LUser, Default); + set_default_privacy_list( + LUser, LServer, Default); true -> ok end @@ -108,11 +109,11 @@ set_lists(#privacy{us = {LUser, LServer}, set_list(LUser, LServer, Name, List) -> RItems = lists:map(fun item_to_raw/1, List), F = fun () -> - ID = case get_privacy_list_id_t(LUser, Name) of + ID = case get_privacy_list_id_t(LUser, LServer, Name) of {selected, []} -> - add_privacy_list(LUser, Name), + add_privacy_list(LUser, LServer, Name), {selected, [{I}]} = - get_privacy_list_id_t(LUser, Name), + get_privacy_list_id_t(LUser, LServer, Name), I; {selected, [{I}]} -> I end, @@ -199,9 +200,12 @@ export(Server) -> when LServer == Host -> if Default /= none -> [?SQL("delete from privacy_default_list where" - " username=%(LUser)s;"), - ?SQL("insert into privacy_default_list(username, name) " - "values (%(LUser)s, %(Default)s);")]; + " username=%(LUser)s and %(LServer)H;"), + ?SQL_INSERT( + "privacy_default_list", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "name=%(Default)s"])]; true -> [] end ++ @@ -210,11 +214,14 @@ export(Server) -> RItems = lists:map(fun item_to_raw/1, List), ID = get_id(), [?SQL("delete from privacy_list where" - " username=%(LUser)s and" + " username=%(LUser)s and %(LServer)H and" " name=%(Name)s;"), - ?SQL("insert into privacy_list(username, " - "name, id) values (" - "%(LUser)s, %(Name)s, %(ID)d);"), + ?SQL_INSERT( + "privacy_list", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "name=%(Name)s", + "id=%(ID)d"]), ?SQL("delete from privacy_list_data where" " id=%(ID)d;")] ++ [?SQL("insert into privacy_list_data(id, t, " @@ -312,28 +319,28 @@ get_default_privacy_list(LUser, LServer) -> ejabberd_sql:sql_query( LServer, ?SQL("select @(name)s from privacy_default_list " - "where username=%(LUser)s")). + "where username=%(LUser)s and %(LServer)H")). -get_default_privacy_list_t(LUser) -> +get_default_privacy_list_t(LUser, LServer) -> ejabberd_sql:sql_query_t( ?SQL("select @(name)s from privacy_default_list " - "where username=%(LUser)s")). + "where username=%(LUser)s and %(LServer)H")). get_privacy_list_names(LUser, LServer) -> ejabberd_sql:sql_query( LServer, ?SQL("select @(name)s from privacy_list" - " where username=%(LUser)s")). + " where username=%(LUser)s and %(LServer)H")). -get_privacy_list_names_t(LUser) -> +get_privacy_list_names_t(LUser, LServer) -> ejabberd_sql:sql_query_t( ?SQL("select @(name)s from privacy_list" - " where username=%(LUser)s")). + " where username=%(LUser)s and %(LServer)H")). -get_privacy_list_id_t(LUser, Name) -> +get_privacy_list_id_t(LUser, LServer, Name) -> ejabberd_sql:sql_query_t( ?SQL("select @(id)d from privacy_list" - " where username=%(LUser)s and name=%(Name)s")). + " where username=%(LUser)s and %(LServer)H and name=%(Name)s")). get_privacy_list_data(LUser, LServer, Name) -> ejabberd_sql:sql_query( @@ -343,37 +350,41 @@ get_privacy_list_data(LUser, LServer, Name) -> "@(match_presence_out)b from privacy_list_data " "where id =" " (select id from privacy_list" - " where username=%(LUser)s and name=%(Name)s) " + " where username=%(LUser)s and %(LServer)H and name=%(Name)s) " "order by ord")). -set_default_privacy_list(LUser, Name) -> +set_default_privacy_list(LUser, LServer, Name) -> ?SQL_UPSERT_T( "privacy_default_list", ["!username=%(LUser)s", + "!server_host=%(LServer)s", "name=%(Name)s"]). unset_default_privacy_list(LUser, LServer) -> case ejabberd_sql:sql_query( LServer, ?SQL("delete from privacy_default_list" - " where username=%(LUser)s")) of + " where username=%(LUser)s and %(LServer)H")) of {updated, _} -> ok; Err -> Err end. -remove_privacy_list_t(LUser, Name) -> +remove_privacy_list_t(LUser, LServer, Name) -> case ejabberd_sql:sql_query_t( ?SQL("delete from privacy_list where" - " username=%(LUser)s and name=%(Name)s")) of + " username=%(LUser)s and %(LServer)H and name=%(Name)s")) of {updated, 0} -> {error, notfound}; {updated, _} -> ok; Err -> Err end. -add_privacy_list(LUser, Name) -> +add_privacy_list(LUser, LServer, Name) -> ejabberd_sql:sql_query_t( - ?SQL("insert into privacy_list(username, name) " - "values (%(LUser)s, %(Name)s)")). + ?SQL_INSERT( + "privacy_list", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "name=%(Name)s"])). set_privacy_list(ID, RItems) -> ejabberd_sql:sql_query_t( @@ -395,12 +406,12 @@ set_privacy_list(ID, RItems) -> del_privacy_lists(LUser, LServer) -> case ejabberd_sql:sql_query( LServer, - ?SQL("delete from privacy_list where username=%(LUser)s")) of + ?SQL("delete from privacy_list where username=%(LUser)s and %(LServer)H")) of {updated, _} -> case ejabberd_sql:sql_query( LServer, ?SQL("delete from privacy_default_list " - "where username=%(LUser)s")) of + "where username=%(LUser)s and %(LServer)H")) of {updated, _} -> ok; Err -> Err end; diff --git a/src/mod_private.erl b/src/mod_private.erl index 1cc5e3c11..cb1674688 100644 --- a/src/mod_private.erl +++ b/src/mod_private.erl @@ -133,7 +133,7 @@ set_data(LUser, LServer, Data) -> Mod = gen_mod:db_mod(LServer, ?MODULE), case Mod:set_data(LUser, LServer, Data) of ok -> - delete_cache(Mod, LServer, LServer, Data); + delete_cache(Mod, LUser, LServer, Data); {error, _} = Err -> Err end. diff --git a/src/mod_private_sql.erl b/src/mod_private_sql.erl index 907eeaf3a..5ed584c30 100644 --- a/src/mod_private_sql.erl +++ b/src/mod_private_sql.erl @@ -49,6 +49,7 @@ set_data(LUser, LServer, Data) -> ?SQL_UPSERT_T( "private_storage", ["!username=%(LUser)s", + "!server_host=%(LServer)s", "!namespace=%(XMLNS)s", "data=%(SData)s"]) end, Data) @@ -64,7 +65,8 @@ get_data(LUser, LServer, XMLNS) -> case ejabberd_sql:sql_query( LServer, ?SQL("select @(data)s from private_storage" - " where username=%(LUser)s and namespace=%(XMLNS)s")) of + " where username=%(LUser)s and %(LServer)H" + " and namespace=%(XMLNS)s")) of {selected, [{SData}]} -> parse_element(LUser, LServer, SData); {selected, []} -> @@ -77,7 +79,7 @@ get_all_data(LUser, LServer) -> case ejabberd_sql:sql_query( LServer, ?SQL("select @(namespace)s, @(data)s from private_storage" - " where username=%(LUser)s")) of + " where username=%(LUser)s and %(LServer)H")) of {selected, []} -> error; {selected, Res} -> @@ -95,7 +97,8 @@ get_all_data(LUser, LServer) -> del_data(LUser, LServer) -> case ejabberd_sql:sql_query( LServer, - ?SQL("delete from private_storage where username=%(LUser)s")) of + ?SQL("delete from private_storage" + " where username=%(LUser)s and %(LServer)H")) of {updated, _} -> ok; _ -> @@ -109,10 +112,13 @@ export(_Server) -> when LServer == Host -> SData = fxml:element_to_binary(Data), [?SQL("delete from private_storage where" - " username=%(LUser)s and namespace=%(XMLNS)s;"), - ?SQL("insert into private_storage(username, " - "namespace, data) values (" - "%(LUser)s, %(XMLNS)s, %(SData)s);")]; + " username=%(LUser)s and %(LServer)H and namespace=%(XMLNS)s;"), + ?SQL_INSERT( + "private_storage", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "namespace=%(XMLNS)s", + "data=%(SData)s"])]; (_Host, _R) -> [] end}]. diff --git a/src/mod_privilege.erl b/src/mod_privilege.erl index 6ba061a53..dab7a619b 100644 --- a/src/mod_privilege.erl +++ b/src/mod_privilege.erl @@ -292,7 +292,7 @@ forward_message(#message{to = To} = Msg) -> Err = xmpp:err_bad_request(Txt, Lang), ejabberd_router:route_error(Msg, Err) catch _:{xmpp_codec, Why} -> - Txt = xmpp:format_error(Why), + Txt = xmpp:io_format_error(Why), Err = xmpp:err_bad_request(Txt, Lang), ejabberd_router:route_error(Msg, Err) end; diff --git a/src/mod_proxy65_service.erl b/src/mod_proxy65_service.erl index aaece980a..fb34ba554 100644 --- a/src/mod_proxy65_service.erl +++ b/src/mod_proxy65_service.erl @@ -183,18 +183,18 @@ process_bytestreams(#iq{type = get, from = JID, to = To, lang = Lang} = IQ) -> StreamHost = get_streamhost(Host, ServerHost), xmpp:make_iq_result(IQ, #bytestreams{hosts = [StreamHost]}); deny -> - xmpp:make_error(IQ, xmpp:err_forbidden(<<"Denied by ACL">>, Lang)) + xmpp:make_error(IQ, xmpp:err_forbidden(<<"Access denied by service policy">>, Lang)) end; process_bytestreams(#iq{type = set, lang = Lang, sub_els = [#bytestreams{sid = SID}]} = IQ) when SID == <<"">> orelse size(SID) > 128 -> Why = {bad_attr_value, <<"sid">>, <<"query">>, ?NS_BYTESTREAMS}, - Txt = xmpp:format_error(Why), + Txt = xmpp:io_format_error(Why), xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); process_bytestreams(#iq{type = set, lang = Lang, sub_els = [#bytestreams{activate = undefined}]} = IQ) -> Why = {missing_cdata, <<"">>, <<"activate">>, ?NS_BYTESTREAMS}, - Txt = xmpp:format_error(Why), + Txt = xmpp:io_format_error(Why), xmpp:make_error(IQ, xmpp:err_jid_malformed(Txt, Lang)); process_bytestreams(#iq{type = set, lang = Lang, from = InitiatorJID, to = To, sub_els = [#bytestreams{activate = TargetJID, @@ -232,7 +232,7 @@ process_bytestreams(#iq{type = set, lang = Lang, from = InitiatorJID, to = To, xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) end; deny -> - Txt = <<"Denied by ACL">>, + Txt = <<"Access denied by service policy">>, xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)) end. diff --git a/src/mod_pubsub.erl b/src/mod_pubsub.erl index a67ae5bfc..e065bdaae 100644 --- a/src/mod_pubsub.erl +++ b/src/mod_pubsub.erl @@ -89,7 +89,7 @@ %% API and gen_server callbacks -export([start/2, stop/1, init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3, depends/2, export/1, mod_opt_type/1]). + terminate/2, code_change/3, depends/2, mod_opt_type/1]). %%==================================================================== %% API @@ -259,10 +259,9 @@ init([ServerHost, Opts]) -> end, {Plugins, NodeTree, PepMapping} = init_plugins(Host, ServerHost, Opts), DefaultModule = plugin(Host, hd(Plugins)), - BaseOptions = DefaultModule:options(), - DefaultNodeCfg = filter_node_options( + DefaultNodeCfg = merge_config( gen_mod:get_opt(default_node_config, Opts, []), - BaseOptions), + DefaultModule:options()), lists:foreach( fun(H) -> T = gen_mod:get_module_proc(H, config), @@ -448,10 +447,7 @@ disco_identity(Host, Node, From) -> {result, _} -> {result, [#identity{category = <<"pubsub">>, type = <<"pep">>}, #identity{category = <<"pubsub">>, type = <<"leaf">>, - name = case get_option(Options, title) of - false -> <<>>; - Title -> Title - end}]}; + name = get_option(Options, title, <<>>)}]}; _ -> {result, []} end @@ -515,10 +511,7 @@ disco_items(Host, <<>>, From) -> {result, _} -> [#disco_item{node = Node, jid = jid:make(Host), - name = case get_option(Options, title) of - false -> <<>>; - Title -> Title - end} | Acc]; + name = get_option(Options, title, <<>>)} | Acc]; _ -> Acc end @@ -830,7 +823,8 @@ process_disco_info(#iq{from = From, to = To, lang = Lang, type = get, [ServerHost, ?MODULE, <<>>, <<>>]), case iq_disco_info(Host, Node, From, Lang) of {result, IQRes} -> - xmpp:make_iq_result(IQ, IQRes#disco_info{node = Node, xdata = Info}); + XData = IQRes#disco_info.xdata ++ Info, + xmpp:make_iq_result(IQ, IQRes#disco_info{node = Node, xdata = XData}); {error, Error} -> xmpp:make_error(IQ, Error) end. @@ -939,14 +933,25 @@ node_disco_info(Host, Node, From) -> {result, disco_info()} | {error, stanza_error()}. node_disco_info(Host, Node, _From, _Identity, _Features) -> Action = - fun(#pubsub_node{type = Type, options = Options}) -> + fun(#pubsub_node{id = Nidx, type = Type, options = Options}) -> NodeType = case get_option(Options, node_type) of collection -> <<"collection">>; _ -> <<"leaf">> end, + Affs = case node_call(Host, Type, get_node_affiliations, [Nidx]) of + {result, Result} -> Result; + _ -> [] + end, + Meta = [{title, get_option(Options, title, <<>>)}, + {description, get_option(Options, description, <<>>)}, + {owner, [jid:make(LJID) || {LJID, Aff} <- Affs, Aff =:= owner]}, + {publisher, [jid:make(LJID) || {LJID, Aff} <- Affs, Aff =:= publisher]}, + {num_subscribers, length([LJID || {LJID, Aff} <- Affs, Aff =:= subscriber])}], + XData = #xdata{type = result, + fields = pubsub_meta_data:encode(Meta)}, Is = [#identity{category = <<"pubsub">>, type = NodeType}], Fs = [?NS_PUBSUB | [feature(F) || F <- plugin_features(Host, Type)]], - {result, #disco_info{identities = Is, features = Fs}} + {result, #disco_info{identities = Is, features = Fs, xdata = [XData]}} end, case transaction(Host, Node, Action, sync_dirty) of {result, {_, Result}} -> {result, Result}; @@ -1563,6 +1568,7 @@ delete_node(Host, Node, Owner) -> RNidx = RNode#pubsub_node.id, RType = RNode#pubsub_node.type, ROptions = RNode#pubsub_node.options, + unset_cached_item(RH, RNidx), broadcast_removed_node(RH, RN, RNidx, RType, ROptions, SubsByDepth), ejabberd_hooks:run(pubsub_delete_node, ServerHost, @@ -1577,6 +1583,7 @@ delete_node(Host, Node, Owner) -> lists:foreach(fun ({RNode, _RSubs}) -> {RH, RN} = RNode#pubsub_node.nodeid, RNidx = RNode#pubsub_node.id, + unset_cached_item(RH, RNidx), ejabberd_hooks:run(pubsub_delete_node, ServerHost, [ServerHost, RH, RN, RNidx]) @@ -1588,6 +1595,7 @@ delete_node(Host, Node, Owner) -> end; {result, {TNode, {_, Result}}} -> Nidx = TNode#pubsub_node.id, + unset_cached_item(Host, Nidx), ejabberd_hooks:run(pubsub_delete_node, ServerHost, [ServerHost, Host, Node, Nidx]), case Result of @@ -1796,8 +1804,6 @@ publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload, PubOpts, Access broadcast -> Payload; PluginPayload -> PluginPayload end, - ejabberd_hooks:run(pubsub_publish_item, ServerHost, - [ServerHost, Node, Publisher, service_jid(Host), ItemId, BrPayload]), set_cached_item(Host, Nidx, ItemId, Publisher, BrPayload), case get_option(Options, deliver_notifications) of true -> @@ -1806,6 +1812,8 @@ publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload, PubOpts, Access false -> ok end, + ejabberd_hooks:run(pubsub_publish_item, ServerHost, + [ServerHost, Node, Publisher, service_jid(Host), ItemId, BrPayload]), case Result of default -> {result, Reply}; _ -> {result, Result} @@ -1960,15 +1968,11 @@ purge_node(Host, Node, Owner) -> -spec get_items(host(), binary(), jid(), binary(), binary(), [binary()], undefined | rsm_set()) -> {result, pubsub()} | {error, stanza_error()}. -get_items(Host, Node, From, SubId, SMaxItems, ItemIds, RSM) -> - MaxItems = if SMaxItems == undefined -> - case get_max_items_node(Host) of - undefined -> ?MAXITEMS; - Max -> Max - end; - true -> - SMaxItems - end, +get_items(Host, Node, From, SubId, MaxItems, ItemIds, undefined) + when MaxItems =/= undefined -> + get_items(Host, Node, From, SubId, MaxItems, ItemIds, + #rsm_set{max = MaxItems, before = <<>>}); +get_items(Host, Node, From, SubId, _MaxItems, ItemIds, RSM) -> Action = fun(#pubsub_node{options = Options, type = Type, id = Nidx, owners = O}) -> @@ -1987,8 +1991,14 @@ get_items(Host, Node, From, SubId, SMaxItems, ItemIds, RSM) -> Owners = node_owners_call(Host, Type, Nidx, O), {PS, RG} = get_presence_and_roster_permissions( Host, From, Owners, AccessModel, AllowedGroups), - node_call(Host, Type, get_items, - [Nidx, From, AccessModel, PS, RG, SubId, RSM]) + case ItemIds of + [ItemId] -> + node_call(Host, Type, get_item, + [Nidx, ItemId, From, AccessModel, PS, RG, undefined]); + _ -> + node_call(Host, Type, get_items, + [Nidx, From, AccessModel, PS, RG, SubId, RSM]) + end end end, case transaction(Host, Node, Action, sync_dirty) of @@ -2004,8 +2014,12 @@ get_items(Host, Node, From, SubId, SMaxItems, ItemIds, RSM) -> end, {result, #pubsub{items = #ps_items{node = Node, - items = itemsEls(lists:sublist(SendItems, MaxItems))}, + items = itemsEls(SendItems)}, rsm = RsmOut}}; + {result, {_, Item}} -> + {result, + #pubsub{items = #ps_items{node = Node, + items = itemsEls([Item])}}}; Error -> Error end. @@ -3091,10 +3105,10 @@ get_option(Options, Var, Def) -> -spec node_options(host(), binary()) -> [{atom(), any()}]. node_options(Host, Type) -> - case config(Host, default_node_config) of - undefined -> node_plugin_options(Host, Type); - [] -> node_plugin_options(Host, Type); - Config -> Config + DefaultOpts = node_plugin_options(Host, Type), + case config(Host, plugins) of + [Type|_] -> config(Host, default_node_config, DefaultOpts); + _ -> DefaultOpts end. -spec node_plugin_options(host(), binary()) -> [{atom(), any()}]. @@ -3108,13 +3122,6 @@ node_plugin_options(Host, Type) -> Result end. --spec filter_node_options([{atom(), any()}], [{atom(), any()}]) -> [{atom(), any()}]. -filter_node_options(Options, BaseOptions) -> - lists:foldl(fun({Key, Val}, Acc) -> - DefaultValue = proplists:get_value(Key, Options, Val), - [{Key, DefaultValue}|Acc] - end, [], BaseOptions). - -spec node_owners_action(host(), binary(), nodeIdx(), [ljid()]) -> [ljid()]. node_owners_action(Host, Type, Nidx, []) -> case node_action(Host, Type, get_node_affiliations, [Nidx]) of @@ -3199,8 +3206,8 @@ set_configure(Host, Node, From, Config, Lang) -> case tree_call(Host, set_node, [N#pubsub_node{options = NewOpts}]) of - {result, Nidx} -> {result, ok}; - ok -> {result, ok}; + {result, Nidx} -> {result, NewOpts}; + ok -> {result, NewOpts}; Err -> Err end; _ -> @@ -3209,10 +3216,9 @@ set_configure(Host, Node, From, Config, Lang) -> end end, case transaction(Host, Node, Action, transaction) of - {result, {TNode, ok}} -> + {result, {TNode, Options}} -> Nidx = TNode#pubsub_node.id, Type = TNode#pubsub_node.type, - Options = TNode#pubsub_node.options, broadcast_config_notification(Host, Node, Nidx, Type, Options, Lang), {result, undefined}; Other -> @@ -3220,11 +3226,11 @@ set_configure(Host, Node, From, Config, Lang) -> end. -spec merge_config([proplists:property()], [proplists:property()]) -> [proplists:property()]. -merge_config(Config1, Config2) -> +merge_config(CustomConfig, DefaultConfig) -> lists:foldl( fun({Opt, Val}, Acc) -> lists:keystore(Opt, 1, Acc, {Opt, Val}) - end, Config2, Config1). + end, DefaultConfig, CustomConfig). -spec decode_node_config(undefined | xdata(), binary(), binary()) -> pubsub_node_config:result() | @@ -3374,11 +3380,11 @@ tree(Host) -> tree(_Host, <<"virtual">>) -> nodetree_virtual; % special case, virtual does not use any backend tree(Host, Name) -> - submodule(Host, <<"nodetree_", Name/binary>>). + submodule(Host, <<"nodetree">>, Name). -spec plugin(host(), binary()) -> atom(). plugin(Host, Name) -> - submodule(Host, <<"node_", Name/binary>>). + submodule(Host, <<"node">>, Name). -spec plugins(host()) -> [binary()]. plugins(Host) -> @@ -3390,14 +3396,13 @@ plugins(Host) -> -spec subscription_plugin(host()) -> atom(). subscription_plugin(Host) -> - submodule(Host, <<"pubsub_subscription">>). + submodule(Host, <<"pubsub">>, <<"subscription">>). --spec submodule(host(), binary()) -> atom(). -submodule(Host, Name) -> +-spec submodule(host(), binary(), binary()) -> atom(). +submodule(Host, Type, Name) -> case gen_mod:db_type(serverhost(Host), ?MODULE) of - mnesia -> misc:binary_to_atom(Name); - Type -> misc:binary_to_atom(<<Name/binary, "_", - (misc:atom_to_binary(Type))/binary>>) + mnesia -> ejabberd:module_name([<<"pubsub">>, Type, Name]); + Db -> ejabberd:module_name([<<"pubsub">>, Type, Name, misc:atom_to_binary(Db)]) end. -spec config(binary(), any()) -> any(). @@ -3792,7 +3797,7 @@ purge_offline(Host, LJID, Node) -> Nidx = Node#pubsub_node.id, Type = Node#pubsub_node.type, Options = Node#pubsub_node.options, - case node_action(Host, Type, get_items, [Nidx, service_jid(Host), none]) of + case node_action(Host, Type, get_items, [Nidx, service_jid(Host), undefined]) of {result, {[], _}} -> ok; {result, {Items, _}} -> @@ -3822,9 +3827,6 @@ purge_offline(Host, LJID, Node) -> Error end. -export(Server) -> - pubsub_db_sql:export(Server). - mod_opt_type(access_createnode) -> fun acl:access_rules_validator/1; mod_opt_type(db_type) -> fun(T) -> ejabberd_config:v_db(?MODULE, T) end; mod_opt_type(host) -> fun iolist_to_binary/1; diff --git a/src/mod_push.erl b/src/mod_push.erl index 2ca0bf525..1eaec6ad5 100644 --- a/src/mod_push.erl +++ b/src/mod_push.erl @@ -46,6 +46,9 @@ %% API (used by mod_push_keepalive). -export([notify/1, notify/3, notify/5]). +%% For IQ callbacks +-export([delete_session/3]). + -include("ejabberd.hrl"). -include("ejabberd_commands.hrl"). -include("logger.hrl"). @@ -56,26 +59,27 @@ -type c2s_state() :: ejabberd_c2s:state(). -type timestamp() :: erlang:timestamp(). -type push_session() :: {timestamp(), ljid(), binary(), xdata()}. +-type err_reason() :: notfound | db_failure. -callback init(binary(), gen_mod:opts()) -> any(). -callback store_session(binary(), binary(), timestamp(), jid(), binary(), xdata()) - -> {ok, push_session()} | error. + -> {ok, push_session()} | {error, err_reason()}. -callback lookup_session(binary(), binary(), jid(), binary()) - -> {ok, push_session()} | error. + -> {ok, push_session()} | {error, err_reason()}. -callback lookup_session(binary(), binary(), timestamp()) - -> {ok, push_session()} | error. + -> {ok, push_session()} | {error, err_reason()}. -callback lookup_sessions(binary(), binary(), jid()) - -> {ok, [push_session()]} | error. + -> {ok, [push_session()]} | {error, err_reason()}. -callback lookup_sessions(binary(), binary()) - -> {ok, [push_session()]} | error. + -> {ok, [push_session()]} | {error, err_reason()}. -callback lookup_sessions(binary()) - -> {ok, [push_session()]} | error. + -> {ok, [push_session()]} | {error, err_reason()}. -callback delete_session(binary(), binary(), timestamp()) - -> ok | error. + -> ok | {error, err_reason()}. -callback delete_old_sessions(binary() | global, erlang:timestamp()) - -> any(). + -> ok | {error, err_reason()}. -callback use_cache(binary()) -> boolean(). -callback cache_nodes(binary()) @@ -100,7 +104,12 @@ start(Host, Opts) -> stop(Host) -> unregister_hooks(Host), unregister_iq_handlers(Host), - ejabberd_commands:unregister_commands(get_commands_spec()). + case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of + false -> + ejabberd_commands:unregister_commands(get_commands_spec()); + true -> + ok + end. -spec reload(binary(), gen_mod:opts(), gen_mod:opts()) -> ok. reload(Host, NewOpts, OldOpts) -> @@ -193,7 +202,7 @@ register_hooks(Host) -> c2s_stanza, 50), ejabberd_hooks:add(store_mam_message, Host, ?MODULE, mam_message, 50), - ejabberd_hooks:add(offline_message_hook, Host, ?MODULE, + ejabberd_hooks:add(store_offline_message, Host, ?MODULE, offline_message, 50), ejabberd_hooks:add(remove_user, Host, ?MODULE, remove_user, 50). @@ -212,7 +221,7 @@ unregister_hooks(Host) -> c2s_stanza, 50), ejabberd_hooks:delete(store_mam_message, Host, ?MODULE, mam_message, 50), - ejabberd_hooks:delete(offline_message_hook, Host, ?MODULE, + ejabberd_hooks:delete(store_offline_message, Host, ?MODULE, offline_message, 50), ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 50). @@ -253,29 +262,39 @@ process_iq(#iq{lang = Lang, sub_els = [#push_enable{node = <<>>}]} = IQ) -> xmpp:make_error(IQ, xmpp:err_feature_not_implemented(Txt, Lang)); process_iq(#iq{from = #jid{lserver = LServer} = JID, to = #jid{lserver = LServer}, + lang = Lang, sub_els = [#push_enable{jid = PushJID, node = Node, xdata = XData}]} = IQ) -> case enable(JID, PushJID, Node, XData) of ok -> xmpp:make_iq_result(IQ); - error -> - xmpp:make_error(IQ, xmpp:err_internal_server_error()) + {error, db_failure} -> + Txt = <<"Database failure">>, + xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)); + {error, notfound} -> + Txt = <<"User session not found">>, + xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang)) end; process_iq(#iq{from = #jid{lserver = LServer} = JID, to = #jid{lserver = LServer}, + lang = Lang, sub_els = [#push_disable{jid = PushJID, node = Node}]} = IQ) -> case disable(JID, PushJID, Node) of ok -> xmpp:make_iq_result(IQ); - error -> - xmpp:make_error(IQ, xmpp:err_item_not_found()) + {error, db_failure} -> + Txt = <<"Database failure">>, + xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)); + {error, notfound} -> + Txt = <<"Push record not found">>, + xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang)) end; process_iq(IQ) -> xmpp:make_error(IQ, xmpp:err_not_allowed()). --spec enable(jid(), jid(), binary(), xdata()) -> ok | error. +-spec enable(jid(), jid(), binary(), xdata()) -> ok | {error, err_reason()}. enable(#jid{luser = LUser, lserver = LServer, lresource = LResource} = JID, PushJID, Node, XData) -> case ejabberd_sm:get_session_sid(LUser, LServer, LResource) of @@ -285,18 +304,18 @@ enable(#jid{luser = LUser, lserver = LServer, lresource = LResource} = JID, ?INFO_MSG("Enabling push notifications for ~s", [jid:encode(JID)]), ejabberd_c2s:cast(PID, push_enable); - error -> + {error, _} = Err -> ?ERROR_MSG("Cannot enable push for ~s: database error", [jid:encode(JID)]), - error + Err end; none -> ?WARNING_MSG("Cannot enable push for ~s: session not found", [jid:encode(JID)]), - error + {error, notfound} end. --spec disable(jid(), jid(), binary() | undefined) -> ok | error. +-spec disable(jid(), jid(), binary() | undefined) -> ok | {error, err_reason()}. disable(#jid{luser = LUser, lserver = LServer, lresource = LResource} = JID, PushJID, Node) -> case ejabberd_sm:get_session_sid(LUser, LServer, LResource) of @@ -308,7 +327,7 @@ disable(#jid{luser = LUser, lserver = LServer, lresource = LResource} = JID, ?WARNING_MSG("Session not found while disabling push for ~s", [jid:encode(JID)]) end, - if Node /= undefined -> + if Node /= <<>> -> delete_session(LUser, LServer, PushJID, Node); true -> delete_sessions(LUser, LServer, PushJID) @@ -327,9 +346,6 @@ c2s_stanza(State, _Pkt, _SendResult) -> -spec mam_message(message() | drop, binary(), binary(), jid(), chat | groupchat, recv | send) -> message(). -mam_message(#message{meta = #{push_notified := true}} = Pkt, - _LUser, _LServer, _Peer, _Type, _Dir) -> - Pkt; mam_message(#message{} = Pkt, LUser, LServer, _Peer, chat, _Dir) -> case lookup_sessions(LUser, LServer) of {ok, [_|_] = Clients} -> @@ -343,15 +359,14 @@ mam_message(#message{} = Pkt, LUser, LServer, _Peer, chat, _Dir) -> _ -> ok end, - xmpp:put_meta(Pkt, push_notified, true); + Pkt; mam_message(Pkt, _LUser, _LServer, _Peer, _Type, _Dir) -> Pkt. --spec offline_message({any(), message()}) -> {any(), message()}. -offline_message({_Action, #message{meta = #{push_notified := true}}} = Acc) -> - Acc; -offline_message({Action, #message{to = #jid{luser = LUser, - lserver = LServer}} = Pkt}) -> +-spec offline_message(message()) -> message(). +offline_message(#message{meta = #{mam_archived := true}} = Pkt) -> + Pkt; % Push notification was triggered via MAM. +offline_message(#message{to = #jid{luser = LUser, lserver = LServer}} = Pkt) -> case lookup_sessions(LUser, LServer) of {ok, [_|_] = Clients} -> ?DEBUG("Notifying ~s@~s of offline message", [LUser, LServer]), @@ -359,7 +374,7 @@ offline_message({Action, #message{to = #jid{luser = LUser, _ -> ok end, - {Action, xmpp:put_meta(Pkt, push_notified, true)}. + Pkt. -spec c2s_session_pending(c2s_state()) -> c2s_state(). c2s_session_pending(#{push_enabled := true, mgmt_queue := Queue} = State) -> @@ -388,7 +403,7 @@ c2s_handle_cast(State, push_disable) -> c2s_handle_cast(State, _Msg) -> State. --spec remove_user(binary(), binary()) -> ok | error. +-spec remove_user(binary(), binary()) -> ok | {error, err_reason()}. remove_user(LUser, LServer) -> ?INFO_MSG("Removing any push sessions of ~s@~s", [LUser, LServer]), Mod = gen_mod:db_mod(LServer, ?MODULE), @@ -403,7 +418,7 @@ notify(#{jid := #jid{luser = LUser, lserver = LServer}, sid := {TS, _}}) -> case lookup_session(LUser, LServer, TS) of {ok, Client} -> notify(LUser, LServer, [Client]); - error -> + _Err -> ok end. @@ -414,7 +429,8 @@ notify(LUser, LServer, Clients) -> HandleResponse = fun(#iq{type = result}) -> ok; (#iq{type = error}) -> - delete_session(LUser, LServer, TS); + spawn(?MODULE, delete_session, + [LUser, LServer, TS]); (timeout) -> ok % Hmm. end, @@ -433,14 +449,13 @@ notify(LServer, PushLJID, Node, XData, HandleResponse) -> to = jid:make(PushLJID), id = randoms:get_string(), sub_els = [PubSub]}, - ejabberd_local:route_iq(IQ, HandleResponse), - ok. + ejabberd_router:route_iq(IQ, HandleResponse). %%-------------------------------------------------------------------- %% Internal functions. %%-------------------------------------------------------------------- -spec store_session(binary(), binary(), timestamp(), jid(), binary(), xdata()) - -> {ok, push_session()} | error. + -> {ok, push_session()} | {error, err_reason()}. store_session(LUser, LServer, TS, PushJID, Node, XData) -> Mod = gen_mod:db_mod(LServer, ?MODULE), delete_session(LUser, LServer, PushJID, Node), @@ -460,7 +475,7 @@ store_session(LUser, LServer, TS, PushJID, Node, XData) -> end. -spec lookup_session(binary(), binary(), timestamp()) - -> {ok, push_session()} | error. + -> {ok, push_session()} | error | {error, err_reason()}. lookup_session(LUser, LServer, TS) -> Mod = gen_mod:db_mod(LServer, ?MODULE), case use_cache(Mod, LServer) of @@ -472,7 +487,7 @@ lookup_session(LUser, LServer, TS) -> Mod:lookup_session(LUser, LServer, TS) end. --spec lookup_sessions(binary(), binary()) -> {ok, [push_session()]} | error. +-spec lookup_sessions(binary(), binary()) -> {ok, [push_session()]} | {error, err_reason()}. lookup_sessions(LUser, LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), case use_cache(Mod, LServer) of @@ -484,40 +499,48 @@ lookup_sessions(LUser, LServer) -> Mod:lookup_sessions(LUser, LServer) end. --spec delete_session(binary(), binary(), timestamp()) -> ok | error. +-spec delete_session(binary(), binary(), timestamp()) -> ok | {error, db_failure}. delete_session(LUser, LServer, TS) -> Mod = gen_mod:db_mod(LServer, ?MODULE), - ok = Mod:delete_session(LUser, LServer, TS), - case use_cache(Mod, LServer) of - true -> - ets_cache:delete(?PUSH_CACHE, {LUser, LServer}, - cache_nodes(Mod, LServer)), - ets_cache:delete(?PUSH_CACHE, {LUser, LServer, TS}, - cache_nodes(Mod, LServer)); - false -> - ok + case Mod:delete_session(LUser, LServer, TS) of + ok -> + case use_cache(Mod, LServer) of + true -> + ets_cache:delete(?PUSH_CACHE, {LUser, LServer}, + cache_nodes(Mod, LServer)), + ets_cache:delete(?PUSH_CACHE, {LUser, LServer, TS}, + cache_nodes(Mod, LServer)); + false -> + ok + end; + {error, _} = Err -> + Err end. --spec delete_session(binary(), binary(), jid(), binary()) -> ok | error. +-spec delete_session(binary(), binary(), jid(), binary()) -> ok | {error, err_reason()}. delete_session(LUser, LServer, PushJID, Node) -> Mod = gen_mod:db_mod(LServer, ?MODULE), case Mod:lookup_session(LUser, LServer, PushJID, Node) of {ok, {TS, _, _, _}} -> delete_session(LUser, LServer, TS); error -> - error + {error, notfound}; + {error, _} = Err -> + Err end. --spec delete_sessions(binary(), binary(), jid()) -> ok | error. +-spec delete_sessions(binary(), binary(), jid()) -> ok | {error, err_reason()}. delete_sessions(LUser, LServer, PushJID) -> Mod = gen_mod:db_mod(LServer, ?MODULE), LookupFun = fun() -> Mod:lookup_sessions(LUser, LServer, PushJID) end, delete_sessions(LUser, LServer, LookupFun, Mod). --spec delete_sessions(binary(), binary(), fun(() -> ok | error), module()) - -> ok | error. +-spec delete_sessions(binary(), binary(), fun(() -> any()), module()) + -> ok | {error, err_reason()}. delete_sessions(LUser, LServer, LookupFun, Mod) -> case LookupFun() of + {ok, []} -> + {error, notfound}; {ok, Clients} -> case use_cache(Mod, LServer) of true -> @@ -538,8 +561,8 @@ delete_sessions(LUser, LServer, LookupFun, Mod) -> ok end end, Clients); - error -> - error + {error, _} = Err -> + Err end. -spec drop_online_sessions(binary(), binary(), [push_session()]) diff --git a/src/mod_push_keepalive.erl b/src/mod_push_keepalive.erl index bde62fc67..bcdc0c253 100644 --- a/src/mod_push_keepalive.erl +++ b/src/mod_push_keepalive.erl @@ -91,7 +91,7 @@ mod_opt_type(O) when O == cache_life_time; O == cache_size -> mod_opt_type(O) when O == use_cache; O == cache_missed -> fun (B) when is_boolean(B) -> B end; mod_opt_type(_) -> - [resume_timeout, wake_on_start, wake_on_timeout, db_type, cache_life_time, + [resume_timeout, wake_on_start, wake_on_timeout, cache_life_time, cache_size, use_cache, cache_missed, iqdisc]. %%-------------------------------------------------------------------- diff --git a/src/mod_push_mnesia.erl b/src/mod_push_mnesia.erl index 04ea8d60a..ff12150f2 100644 --- a/src/mod_push_mnesia.erl +++ b/src/mod_push_mnesia.erl @@ -31,18 +31,12 @@ %% API -export([init/2, store_session/6, lookup_session/4, lookup_session/3, lookup_sessions/3, lookup_sessions/2, lookup_sessions/1, - delete_session/3, delete_old_sessions/2]). + delete_session/3, delete_old_sessions/2, transform/1]). -include_lib("stdlib/include/ms_transform.hrl"). -include("logger.hrl"). -include("xmpp.hrl"). - --record(push_session, - {us = {<<"">>, <<"">>} :: {binary(), binary()}, - timestamp = p1_time_compat:timestamp() :: erlang:timestamp(), - service = {<<"">>, <<"">>, <<"">>} :: ljid(), - node = <<"">> :: binary(), - xdata = #xdata{} :: xdata()}). +-include("mod_push.hrl"). %%%------------------------------------------------------------------- %%% API @@ -67,7 +61,7 @@ store_session(LUser, LServer, TS, PushJID, Node, XData) -> timestamp = TS, service = PushLJID, node = Node, - xdata = XData}) + xml = encode_xdata(XData)}) end, case mnesia:transaction(F) of {atomic, ok} -> @@ -75,7 +69,7 @@ store_session(LUser, LServer, TS, PushJID, Node, XData) -> {aborted, E} -> ?ERROR_MSG("Cannot store push session for ~s@~s: ~p", [LUser, LServer, E]), - error + {error, db_failure} end. lookup_session(LUser, LServer, PushJID, Node) -> @@ -89,12 +83,12 @@ lookup_session(LUser, LServer, PushJID, Node) -> Rec end), case mnesia:dirty_select(push_session, MatchSpec) of - [#push_session{timestamp = TS, xdata = XData}] -> - {ok, {TS, PushLJID, Node, XData}}; - _ -> + [#push_session{timestamp = TS, xml = El}] -> + {ok, {TS, PushLJID, Node, decode_xdata(El)}}; + [] -> ?DEBUG("No push session found for ~s@~s (~p, ~s)", [LUser, LServer, PushJID, Node]), - error + {error, notfound} end. lookup_session(LUser, LServer, TS) -> @@ -106,33 +100,31 @@ lookup_session(LUser, LServer, TS) -> Rec end), case mnesia:dirty_select(push_session, MatchSpec) of - [#push_session{service = PushLJID, node = Node, xdata = XData}] -> - {ok, {TS, PushLJID, Node, XData}}; - _ -> + [#push_session{service = PushLJID, node = Node, xml = El}] -> + {ok, {TS, PushLJID, Node, decode_xdata(El)}}; + [] -> ?DEBUG("No push session found for ~s@~s (~p)", [LUser, LServer, TS]), - error + {error, notfound} end. lookup_sessions(LUser, LServer, PushJID) -> PushLJID = jid:tolower(PushJID), MatchSpec = ets:fun2ms( - fun(#push_session{us = {U, S}, service = P, node = N} = Rec) + fun(#push_session{us = {U, S}, service = P, + node = Node, timestamp = TS, + xml = El} = Rec) when U == LUser, S == LServer, P == PushLJID -> Rec end), - {ok, mnesia:dirty_select(push_session, MatchSpec)}. + Records = mnesia:dirty_select(push_session, MatchSpec), + {ok, records_to_sessions(Records)}. lookup_sessions(LUser, LServer) -> Records = mnesia:dirty_read(push_session, {LUser, LServer}), - Clients = [{TS, PushLJID, Node, XData} - || #push_session{timestamp = TS, - service = PushLJID, - node = Node, - xdata = XData} <- Records], - {ok, Clients}. + {ok, records_to_sessions(Records)}. lookup_sessions(LServer) -> MatchSpec = ets:fun2ms( @@ -140,11 +132,12 @@ lookup_sessions(LServer) -> timestamp = TS, service = PushLJID, node = Node, - xdata = XData}) + xml = El}) when S == LServer -> - {TS, PushLJID, Node, XData} + {TS, PushLJID, Node, El} end), - {ok, mnesia:dirty_select(push_session, MatchSpec)}. + Records = mnesia:dirty_select(push_session, MatchSpec), + {ok, records_to_sessions(Records)}. delete_session(LUser, LServer, TS) -> MatchSpec = ets:fun2ms( @@ -162,9 +155,9 @@ delete_session(LUser, LServer, TS) -> {atomic, ok} -> ok; {aborted, E} -> - ?ERROR_MSG("Cannot delete push seesion of ~s@~s: ~p", + ?ERROR_MSG("Cannot delete push session of ~s@~s: ~p", [LUser, LServer, E]), - error + {error, db_failure} end. delete_old_sessions(_LServer, Time) -> @@ -181,9 +174,14 @@ delete_old_sessions(_LServer, Time) -> ok; {aborted, E} -> ?ERROR_MSG("Cannot delete old push sessions: ~p", [E]), - error + {error, db_failure} end. +transform({push_session, US, TS, Service, Node, XData}) -> + ?INFO_MSG("Transforming push_session Mnesia table", []), + #push_session{us = US, timestamp = TS, service = Service, + node = Node, xml = encode_xdata(XData)}. + %%-------------------------------------------------------------------- %% Internal functions. %%-------------------------------------------------------------------- @@ -202,3 +200,20 @@ enforce_max_sessions({U, S} = US, Max) -> true -> ok end. + +decode_xdata(undefined) -> + undefined; +decode_xdata(El) -> + xmpp:decode(El). + +encode_xdata(undefined) -> + undefined; +encode_xdata(XData) -> + xmpp:encode(XData). + +records_to_sessions(Records) -> + [{TS, PushLJID, Node, decode_xdata(El)} + || #push_session{timestamp = TS, + service = PushLJID, + node = Node, + xml = El} <- Records]. diff --git a/src/mod_push_sql.erl b/src/mod_push_sql.erl new file mode 100644 index 000000000..c82d9fc02 --- /dev/null +++ b/src/mod_push_sql.erl @@ -0,0 +1,240 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_push_sql.erl +%%% Author : Evgeniy Khramtsov <ekhramtsov@process-one.net> +%%% Purpose : +%%% Created : 26 Oct 2017 by Evgeny Khramtsov <ekhramtsov@process-one.net> +%%% +%%% +%%% ejabberd, Copyright (C) 2017-2017 ProcessOne +%%% +%%% This program is free software; you can redistribute it and/or +%%% modify it under the terms of the GNU General Public License as +%%% published by the Free Software Foundation; either version 2 of the +%%% License, or (at your option) any later version. +%%% +%%% This program is distributed in the hope that it will be useful, +%%% but WITHOUT ANY WARRANTY; without even the implied warranty of +%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +%%% General Public License for more details. +%%% +%%% You should have received a copy of the GNU General Public License along +%%% with this program; if not, write to the Free Software Foundation, Inc., +%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +%%% +%%%---------------------------------------------------------------------- + +-module(mod_push_sql). +-behaviour(mod_push). +-compile([{parse_transform, ejabberd_sql_pt}]). + +%% API +-export([init/2, store_session/6, lookup_session/4, lookup_session/3, + lookup_sessions/3, lookup_sessions/2, lookup_sessions/1, + delete_session/3, delete_old_sessions/2, export/1]). + +-include("xmpp.hrl"). +-include("logger.hrl"). +-include("ejabberd_sql_pt.hrl"). +-include("mod_push.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ok. + +store_session(LUser, LServer, NowTS, PushJID, Node, XData) -> + XML = encode_xdata(XData), + TS = misc:now_to_usec(NowTS), + PushLJID = jid:tolower(PushJID), + Service = jid:encode(PushLJID), + case ?SQL_UPSERT(LServer, "push_session", + ["!username=%(LUser)s", + "!server_host=%(LServer)s", + "!timestamp=%(TS)d", + "!service=%(Service)s", + "!node=%(Node)s", + "xml=%(XML)s"]) of + ok -> + {ok, {NowTS, PushLJID, Node, XData}}; + Err -> + ?ERROR_MSG("Failed to update 'push_session' table: ~p", [Err]), + {error, db_failure} + end. + +lookup_session(LUser, LServer, PushJID, Node) -> + PushLJID = jid:tolower(PushJID), + Service = jid:encode(PushLJID), + case ejabberd_sql:sql_query( + LServer, + ?SQL("select @(timestamp)d, @(xml)s from push_session " + "where username=%(LUser)s and %(LServer)H " + "and service=%(Service)s " + "and node=%(Node)s")) of + {selected, [{TS, XML}]} -> + NowTS = misc:usec_to_now(TS), + XData = decode_xdata(XML, LUser, LServer), + {ok, {NowTS, PushLJID, Node, XData}}; + {selected, []} -> + {error, notfound}; + Err -> + ?ERROR_MSG("Failed to select from 'push_session' table: ~p", [Err]), + {error, db_failure} + end. + +lookup_session(LUser, LServer, NowTS) -> + TS = misc:now_to_usec(NowTS), + case ejabberd_sql:sql_query( + LServer, + ?SQL("select @(service)s, @(node)s, @(xml)s " + "from push_session where username=%(LUser)s and %(LServer)H " + "and timestamp=%(TS)d")) of + {selected, [{Service, Node, XML}]} -> + PushLJID = jid:tolower(jid:decode(Service)), + XData = decode_xdata(XML, LUser, LServer), + {ok, {NowTS, PushLJID, Node, XData}}; + {selected, []} -> + {error, notfound}; + Err -> + ?ERROR_MSG("Failed to select from 'push_session' table: ~p", [Err]), + {error, db_failure} + end. + +lookup_sessions(LUser, LServer, PushJID) -> + PushLJID = jid:tolower(PushJID), + Service = jid:encode(PushLJID), + case ejabberd_sql:sql_query( + LServer, + ?SQL("select @(timestamp)d, @(xml)s, @(node)s from push_session " + "where username=%(LUser)s and %(LServer)H " + "and service=%(Service)s")) of + {selected, Rows} -> + {ok, lists:map( + fun({TS, XML, Node}) -> + NowTS = misc:usec_to_now(TS), + XData = decode_xdata(XML, LUser, LServer), + {NowTS, PushLJID, Node, XData} + end, Rows)}; + Err -> + ?ERROR_MSG("Failed to select from 'push_session' table: ~p", [Err]), + {error, db_failure} + end. + +lookup_sessions(LUser, LServer) -> + case ejabberd_sql:sql_query( + LServer, + ?SQL("select @(timestamp)d, @(xml)s, @(node)s, @(service)s " + "from push_session " + "where username=%(LUser)s and %(LServer)H")) of + {selected, Rows} -> + {ok, lists:map( + fun({TS, XML, Node, Service}) -> + NowTS = misc:usec_to_now(TS), + XData = decode_xdata(XML, LUser, LServer), + PushLJID = jid:tolower(jid:decode(Service)), + {NowTS, PushLJID,Node, XData} + end, Rows)}; + Err -> + ?ERROR_MSG("Failed to select from 'push_session' table: ~p", [Err]), + {error, db_failure} + end. + +lookup_sessions(LServer) -> + case ejabberd_sql:sql_query( + LServer, + ?SQL("select @(username)s, @(timestamp)d, @(xml)s, " + "@(node)s, @(service)s from push_session " + "where %(LServer)H")) of + {selected, Rows} -> + {ok, lists:map( + fun({LUser, TS, XML, Node, Service}) -> + NowTS = misc:usec_to_now(TS), + XData = decode_xdata(XML, LUser, LServer), + PushLJID = jid:tolower(jid:decode(Service)), + {NowTS, PushLJID, Node, XData} + end, Rows)}; + Err -> + ?ERROR_MSG("Failed to select from 'push_session' table: ~p", [Err]), + {error, db_failure} + end. + +delete_session(LUser, LServer, NowTS) -> + TS = misc:now_to_usec(NowTS), + case ejabberd_sql:sql_query( + LServer, + ?SQL("delete from push_session where " + "username=%(LUser)s and %(LServer)H and timestamp=%(TS)d")) of + {updated, _} -> + ok; + Err -> + ?ERROR_MSG("failed to delete from 'push_session' table: ~p", [Err]), + {error, db_failure} + end. + +delete_old_sessions(LServer, Time) -> + TS = misc:now_to_usec(Time), + case ejabberd_sql:sql_query( + LServer, + ?SQL("delete from push_session where timestamp<%(TS)d " + "and %(LServer)H")) of + {updated, _} -> + ok; + Err -> + ?ERROR_MSG("failed to delete from 'push_session' table: ~p", [Err]), + {error, db_failure} + end. + +export(_Server) -> + [{push_session, + fun(Host, #push_session{us = {LUser, LServer}, + timestamp = NowTS, + service = PushLJID, + node = Node, + xml = XData}) + when LServer == Host -> + TS = misc:now_to_usec(NowTS), + Service = jid:encode(PushLJID), + XML = encode_xdata(XData), + [?SQL("delete from push_session where " + "username=%(LUser)s and %(LServer)H and " + "timestamp=%(TS)d and " + "service=%(Service)s and node=%(Node)s and " + "xml=%(XML)s;"), + ?SQL_INSERT( + "push_session", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "timestamp=%(TS)d", + "service=%(Service)s", + "node=%(Node)s", + "xml=%(XML)s"])]; + (_Host, _R) -> + [] + end}]. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +decode_xdata(<<>>, _LUser, _LServer) -> + undefined; +decode_xdata(XML, LUser, LServer) -> + case fxml_stream:parse_element(XML) of + #xmlel{} = El -> + try xmpp:decode(El) + catch _:{xmpp_codec, Why} -> + ?ERROR_MSG("Failed to decode ~s for user ~s@~s " + "from table 'push_session': ~s", + [XML, LUser, LServer, xmpp:format_error(Why)]), + undefined + end; + Err -> + ?ERROR_MSG("Failed to decode ~s for user ~s@~s from " + "table 'push_session': ~p", + [XML, LUser, LServer, Err]), + undefined + end. + +encode_xdata(undefined) -> + <<>>; +encode_xdata(XData) -> + fxml:element_to_binary(xmpp:encode(XData)). diff --git a/src/mod_register.erl b/src/mod_register.erl index d6227f00f..1715a15e4 100644 --- a/src/mod_register.erl +++ b/src/mod_register.erl @@ -127,7 +127,7 @@ process_iq(#iq{from = From, to = To} = IQ, Source) -> process_iq(#iq{type = set, lang = Lang, sub_els = [#register{remove = true}]} = IQ, _Source, _IsCaptchaEnabled, _AllowRemove = false) -> - Txt = <<"Denied by ACL">>, + Txt = <<"Access denied by service policy">>, xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)); process_iq(#iq{type = set, lang = Lang, to = To, from = From, sub_els = [#register{remove = true, @@ -210,7 +210,14 @@ process_iq(#iq{type = get, from = From, to = To, id = ID, lang = Lang} = IQ, Instr = translate:translate( Lang, <<"Choose a username and password to register " "with this server">>), - if IsCaptchaEnabled and not IsRegistered -> + URL = gen_mod:get_module_opt(Server, ?MODULE, redirect_url, <<"">>), + if (URL /= <<"">>) and not IsRegistered -> + Txt = translate:translate(Lang, <<"To register, visit ~s">>), + Desc = str:format(Txt, [URL]), + xmpp:make_iq_result( + IQ, #register{instructions = Desc, + sub_els = [#oob_x{url = URL}]}); + IsCaptchaEnabled and not IsRegistered -> TopInstr = translate:translate( Lang, <<"You need a client that supports x:data " "and CAPTCHA to register">>), @@ -263,7 +270,7 @@ try_register_or_set_password(User, Server, Password, xmpp:make_error(IQ, Error) end; deny -> - Txt = <<"Denied by ACL">>, + Txt = <<"Access denied by service policy">>, xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)) end; _ -> @@ -315,8 +322,8 @@ try_register(User, Server, Password, SourceRaw, Lang) -> case {acl:match_rule(Server, Access, JID), check_ip_access(SourceRaw, IPAccess)} of - {deny, _} -> {error, xmpp:err_forbidden(<<"Denied by ACL">>, Lang)}; - {_, deny} -> {error, xmpp:err_forbidden(<<"Denied by ACL">>, Lang)}; + {deny, _} -> {error, xmpp:err_forbidden(<<"Access denied by service policy">>, Lang)}; + {_, deny} -> {error, xmpp:err_forbidden(<<"Access denied by service policy">>, Lang)}; {allow, allow} -> Source = may_remove_resource(SourceRaw), case check_timeout(Source) of @@ -614,9 +621,11 @@ mod_opt_type({welcome_message, subject}) -> fun iolist_to_binary/1; mod_opt_type({welcome_message, body}) -> fun iolist_to_binary/1; +mod_opt_type(redirect_url) -> + fun iolist_to_binary/1; mod_opt_type(_) -> [access, access_from, access_remove, captcha_protected, ip_access, - iqdisc, password_strength, registration_watchers, + iqdisc, password_strength, registration_watchers, redirect_url, {welcome_message, subject}, {welcome_message, body}]. -spec opt_type(registration_timeout) -> fun((timeout()) -> timeout()); diff --git a/src/mod_register_web.erl b/src/mod_register_web.erl index 16c2d8020..b7bc2edca 100644 --- a/src/mod_register_web.erl +++ b/src/mod_register_web.erl @@ -156,10 +156,14 @@ process(_Path, _Request) -> %%%---------------------------------------------------------------------- serve_css() -> - {200, - [{<<"Content-Type">>, <<"text/css">>}, last_modified(), - cache_control_public()], - css()}. + case css() of + {ok, CSS} -> + {200, + [{<<"Content-Type">>, <<"text/css">>}, last_modified(), + cache_control_public()], CSS}; + error -> + {404, [], "CSS not found"} + end. last_modified() -> {<<"Last-Modified">>, @@ -168,16 +172,30 @@ last_modified() -> cache_control_public() -> {<<"Cache-Control">>, <<"public">>}. +-spec css() -> {ok, binary()} | error. css() -> - <<"html,body {\nbackground: white;\nmargin: " - "0;\npadding: 0;\nheight: 100%;\n}">>. + Dir = misc:css_dir(), + File = filename:join(Dir, "register.css"), + case file:read_file(File) of + {ok, Data} -> + {ok, Data}; + {error, Why} -> + ?ERROR_MSG("failed to read ~s: ~s", [File, file:format_error(Why)]), + error + end. + +meta() -> + ?XA(<<"meta">>, + [{<<"name">>, <<"viewport">>}, + {<<"content">>, <<"width=device-width, initial-scale=1">>}]). %%%---------------------------------------------------------------------- %%% Index page %%%---------------------------------------------------------------------- index_page(Lang) -> - HeadEls = [?XCT(<<"title">>, + HeadEls = [meta(), + ?XCT(<<"title">>, <<"Jabber Account Registration">>), ?XA(<<"link">>, [{<<"href">>, <<"/register/register.css">>}, @@ -206,7 +224,8 @@ index_page(Lang) -> form_new_get(Host, Lang, IP) -> CaptchaEls = build_captcha_li_list(Lang, IP), - HeadEls = [?XCT(<<"title">>, + HeadEls = [meta(), + ?XCT(<<"title">>, <<"Register a Jabber account">>), ?XA(<<"link">>, [{<<"href">>, <<"/register/register.css">>}, @@ -350,7 +369,8 @@ build_captcha_li_list2(Lang, IP) -> %%%---------------------------------------------------------------------- form_changepass_get(Host, Lang) -> - HeadEls = [?XCT(<<"title">>, <<"Change Password">>), + HeadEls = [meta(), + ?XCT(<<"title">>, <<"Change Password">>), ?XA(<<"link">>, [{<<"href">>, <<"/register/register.css">>}, {<<"type">>, <<"text/css">>}, @@ -456,7 +476,8 @@ check_password(Username, Host, Password) -> %%%---------------------------------------------------------------------- form_del_get(Host, Lang) -> - HeadEls = [?XCT(<<"title">>, + HeadEls = [meta(), + ?XCT(<<"title">>, <<"Unregister a Jabber account">>), ?XA(<<"link">>, [{<<"href">>, <<"/register/register.css">>}, diff --git a/src/mod_roster.erl b/src/mod_roster.erl index 7bc5f7de7..a86b50d98 100644 --- a/src/mod_roster.erl +++ b/src/mod_roster.erl @@ -184,7 +184,7 @@ process_local_iq(#iq{type = set, from = From, lang = Lang, Access = gen_mod:get_module_opt(Server, ?MODULE, access, all), case acl:match_rule(Server, Access, From) of deny -> - Txt = <<"Denied by ACL">>, + Txt = <<"Access denied by service policy">>, xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); allow -> process_iq_set(IQ) @@ -1180,12 +1180,18 @@ import_stop(_LServer, _DBType) -> ets:delete(rostergroups_tmp), ok. +-ifdef(NEW_SQL_SCHEMA). +-define(ROW_LENGTH, 10). +-else. +-define(ROW_LENGTH, 9). +-endif. + import(LServer, {sql, _}, _DBType, <<"rostergroups">>, [LUser, SJID, Group]) -> LJID = jid:tolower(jid:decode(SJID)), ets:insert(rostergroups_tmp, {{LUser, LServer, LJID}, Group}), ok; import(LServer, {sql, _}, DBType, <<"rosterusers">>, Row) -> - I = mod_roster_sql:raw_to_record(LServer, lists:sublist(Row, 9)), + I = mod_roster_sql:raw_to_record(LServer, lists:sublist(Row, ?ROW_LENGTH)), Groups = [G || {_, G} <- ets:lookup(rostergroups_tmp, I#roster.usj)], RosterItem = I#roster{groups = Groups}, Mod = gen_mod:db_mod(DBType, ?MODULE), diff --git a/src/mod_roster_sql.erl b/src/mod_roster_sql.erl index 77899624a..82a3c4951 100644 --- a/src/mod_roster_sql.erl +++ b/src/mod_roster_sql.erl @@ -49,7 +49,7 @@ read_roster_version(LUser, LServer) -> case ejabberd_sql:sql_query( LServer, ?SQL("select @(version)s from roster_version" - " where username = %(LUser)s")) of + " where username = %(LUser)s and %(LServer)H")) of {selected, [{Version}]} -> {ok, Version}; {selected, []} -> error; _ -> {error, db_failure} @@ -57,11 +57,11 @@ read_roster_version(LUser, LServer) -> write_roster_version(LUser, LServer, InTransaction, Ver) -> if InTransaction -> - set_roster_version(LUser, Ver); + set_roster_version(LUser, LServer, Ver); true -> transaction( LServer, - fun () -> set_roster_version(LUser, Ver) end) + fun () -> set_roster_version(LUser, LServer, Ver) end) end. get_roster(LUser, LServer) -> @@ -69,7 +69,8 @@ get_roster(LUser, LServer) -> 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")) of + "@(type)s from rosterusers " + "where username=%(LUser)s and %(LServer)H")) of {selected, Items} when is_list(Items) -> JIDGroups = case get_roster_jid_groups(LServer, LUser) of {selected, JGrps} when is_list(JGrps) -> @@ -130,36 +131,42 @@ remove_user(LUser, LServer) -> LServer, fun () -> ejabberd_sql:sql_query_t( - ?SQL("delete from rosterusers where username=%(LUser)s")), + ?SQL("delete from rosterusers" + " where username=%(LUser)s and %(LServer)H")), ejabberd_sql:sql_query_t( - ?SQL("delete from rostergroups where username=%(LUser)s")) + ?SQL("delete from rostergroups" + " where username=%(LUser)s and %(LServer)H")) end), ok. -update_roster(LUser, _LServer, LJID, Item) -> +update_roster(LUser, LServer, LJID, Item) -> SJID = jid:encode(LJID), ItemVals = record_to_row(Item), ItemGroups = Item#roster.groups, roster_subscribe(ItemVals), ejabberd_sql:sql_query_t( ?SQL("delete from rostergroups" - " where username=%(LUser)s and jid=%(SJID)s")), + " where username=%(LUser)s and %(LServer)H 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)")) + ?SQL_INSERT( + "rostergroups", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "jid=%(SJID)s", + "grp=%(ItemGroup)s"])) end, ItemGroups). -del_roster(LUser, _LServer, LJID) -> +del_roster(LUser, LServer, LJID) -> SJID = jid:encode(LJID), ejabberd_sql:sql_query_t( ?SQL("delete from rosterusers" - " where username=%(LUser)s and jid=%(SJID)s")), + " where username=%(LUser)s and %(LServer)H and jid=%(SJID)s")), ejabberd_sql:sql_query_t( ?SQL("delete from rostergroups" - " where username=%(LUser)s and jid=%(SJID)s")). + " where username=%(LUser)s and %(LServer)H and jid=%(SJID)s")). read_subscription_and_groups(LUser, LServer, LJID) -> SJID = jid:encode(LJID), @@ -200,9 +207,13 @@ export(_Server) -> {roster_version, fun(Host, #roster_version{us = {LUser, LServer}, version = Ver}) when LServer == Host -> - [?SQL("delete from roster_version where username=%(LUser)s;"), - ?SQL("insert into roster_version(username, version) values(" - " %(LUser)s, %(Ver)s);")]; + [?SQL("delete from roster_version" + " where username=%(LUser)s and %(LServer)H;"), + ?SQL_INSERT( + "roster_version", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "version=%(Ver)s"])]; (_Host, _R) -> [] end}]. @@ -213,27 +224,29 @@ import(_, _, _) -> %%%=================================================================== %%% Internal functions %%%=================================================================== -set_roster_version(LUser, Version) -> +set_roster_version(LUser, LServer, Version) -> ?SQL_UPSERT_T( "roster_version", ["!username=%(LUser)s", + "!server_host=%(LServer)s", "version=%(Version)s"]). get_roster_jid_groups(LServer, LUser) -> ejabberd_sql:sql_query( LServer, ?SQL("select @(jid)s, @(grp)s from rostergroups where " - "username=%(LUser)s")). + "username=%(LUser)s and %(LServer)H")). -get_roster_groups(_LServer, LUser, SJID) -> +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")). + " where username=%(LUser)s and %(LServer)H and jid=%(SJID)s")). -roster_subscribe({LUser, SJID, Name, SSubscription, SAsk, AskMessage}) -> +roster_subscribe({LUser, LServer, SJID, Name, SSubscription, SAsk, AskMessage}) -> ?SQL_UPSERT_T( "rosterusers", ["!username=%(LUser)s", + "!server_host=%(LServer)s", "!jid=%(SJID)s", "nick=%(Name)s", "subscription=%(SSubscription)s", @@ -243,57 +256,67 @@ roster_subscribe({LUser, SJID, Name, SSubscription, SAsk, AskMessage}) -> "subscribe=''", "type='item'"]). -get_roster_by_jid(_LServer, LUser, SJID) -> +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")). + " where username=%(LUser)s and %(LServer)H 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")). + " where username=%(LUser)s and %(LServer)H and jid=%(SJID)s")). get_subscription(LServer, LUser, SJID) -> ejabberd_sql:sql_query( LServer, ?SQL("select @(subscription)s from rosterusers " - "where username=%(LUser)s and jid=%(SJID)s")). + "where username=%(LUser)s and %(LServer)H and jid=%(SJID)s")). -update_roster_sql({LUser, SJID, Name, SSubscription, SAsk, AskMessage}, +update_roster_sql({LUser, LServer, SJID, Name, SSubscription, SAsk, AskMessage}, ItemGroups) -> [?SQL("delete from rosterusers where" - " username=%(LUser)s and jid=%(SJID)s;"), - ?SQL("insert into rosterusers(" - " username, jid, nick," - " subscription, ask, askmessage," - " server, subscribe, type) " - "values (" - "%(LUser)s, " - "%(SJID)s, " - "%(Name)s, " - "%(SSubscription)s, " - "%(SAsk)s, " - "%(AskMessage)s, " - "'N', '', 'item');"), + " username=%(LUser)s and %(LServer)H and jid=%(SJID)s;"), + ?SQL_INSERT( + "rosterusers", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "jid=%(SJID)s", + "nick=%(Name)s", + "subscription=%(SSubscription)s", + "ask=%(SAsk)s", + "askmessage=%(AskMessage)s", + "server='N'", + "subscribe=''", + "type='item'"]), ?SQL("delete from rostergroups where" - " username=%(LUser)s and jid=%(SJID)s;")] + " username=%(LUser)s and %(LServer)H and jid=%(SJID)s;")] ++ - [?SQL("insert into rostergroups(username, jid, grp) " - "values (%(LUser)s, %(SJID)s, %(ItemGroup)s);") + [?SQL_INSERT( + "rostergroups", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "jid=%(SJID)s", + "grp=%(ItemGroup)s"]) || ItemGroup <- ItemGroups]. raw_to_record(LServer, - [User, SJID, Nick, SSubscription, SAsk, SAskMessage, + [User, LServer, SJID, Nick, SSubscription, SAsk, SAskMessage, _SServer, _SSubscribe, _SType]) -> raw_to_record(LServer, - {User, SJID, Nick, SSubscription, SAsk, SAskMessage, + {User, LServer, 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, LServer, SJID, Nick, SSubscription, SAsk, SAskMessage, + _SServer, _SSubscribe, _SType}); +raw_to_record(LServer, + {User, LServer, SJID, Nick, SSubscription, SAsk, SAskMessage, + _SServer, _SSubscribe, _SType}) -> try jid:decode(SJID) of JID -> LJID = jid:tolower(JID), @@ -331,7 +354,7 @@ raw_to_record(LServer, end. record_to_row( - #roster{us = {LUser, _LServer}, + #roster{us = {LUser, LServer}, jid = JID, name = Name, subscription = Subscription, ask = Ask, askmessage = AskMessage}) -> SJID = jid:encode(jid:tolower(JID)), @@ -349,7 +372,7 @@ record_to_row( in -> <<"I">>; none -> <<"N">> end, - {LUser, SJID, Name, SSubscription, SAsk, AskMessage}. + {LUser, LServer, SJID, Name, SSubscription, SAsk, AskMessage}. format_row_error(User, Server, Why) -> [case Why of diff --git a/src/mod_s2s_dialback.erl b/src/mod_s2s_dialback.erl index ab33597a5..b4c2ed9df 100644 --- a/src/mod_s2s_dialback.erl +++ b/src/mod_s2s_dialback.erl @@ -320,7 +320,7 @@ check_from_to(From, To) -> -spec mk_error(term()) -> stanza_error(). mk_error(forbidden) -> - xmpp:err_forbidden(<<"Denied by ACL">>, ?MYLANG); + xmpp:err_forbidden(<<"Access denied by service policy">>, ?MYLANG); mk_error(host_unknown) -> xmpp:err_not_allowed(<<"Host unknown">>, ?MYLANG); mk_error({codec_error, Why}) -> @@ -353,9 +353,9 @@ format_stanza_error(#stanza_error{reason = Reason, text = Txt}) -> #redirect{} -> <<"redirect">>; _ -> erlang:atom_to_binary(Reason, latin1) end, - case Txt of - undefined -> Slogan; - #text{data = <<"">>} -> Slogan; - #text{data = Data} -> + case xmpp:get_text(Txt) of + <<"">> -> + Slogan; + Data -> <<Data/binary, " (", Slogan/binary, ")">> end. diff --git a/src/mod_shared_roster_sql.erl b/src/mod_shared_roster_sql.erl index 51b332455..488e0ec76 100644 --- a/src/mod_shared_roster_sql.erl +++ b/src/mod_shared_roster_sql.erl @@ -50,7 +50,7 @@ init(_Host, _Opts) -> list_groups(Host) -> case ejabberd_sql:sql_query( Host, - ?SQL("select @(name)s from sr_group")) of + ?SQL("select @(name)s from sr_group where %(Host)H")) of {selected, Rs} -> [G || {G} <- Rs]; _ -> [] end. @@ -58,7 +58,7 @@ list_groups(Host) -> groups_with_opts(Host) -> case ejabberd_sql:sql_query( Host, - ?SQL("select @(name)s, @(opts)s from sr_group")) + ?SQL("select @(name)s, @(opts)s from sr_group where %(Host)H")) of {selected, Rs} -> [{G, mod_shared_roster:opts_to_binary(ejabberd_sql:decode_term(Opts))} @@ -72,6 +72,7 @@ create_group(Host, Group, Opts) -> ?SQL_UPSERT_T( "sr_group", ["!name=%(Group)s", + "!server_host=%(Host)s", "opts=%(SOpts)s"]) end, ejabberd_sql:sql_transaction(Host, F). @@ -79,9 +80,9 @@ create_group(Host, Group, Opts) -> delete_group(Host, Group) -> F = fun () -> ejabberd_sql:sql_query_t( - ?SQL("delete from sr_group where name=%(Group)s")), + ?SQL("delete from sr_group where name=%(Group)s and %(Host)H")), ejabberd_sql:sql_query_t( - ?SQL("delete from sr_user where grp=%(Group)s")) + ?SQL("delete from sr_user where grp=%(Group)s and %(Host)H")) end, case ejabberd_sql:sql_transaction(Host, F) of {atomic,{updated,_}} -> {atomic, ok}; @@ -91,7 +92,8 @@ delete_group(Host, Group) -> get_group_opts(Host, Group) -> case catch ejabberd_sql:sql_query( Host, - ?SQL("select @(opts)s from sr_group where name=%(Group)s")) of + ?SQL("select @(opts)s from sr_group" + " where name=%(Group)s and %(Host)H")) of {selected, [{SOpts}]} -> mod_shared_roster:opts_to_binary(ejabberd_sql:decode_term(SOpts)); _ -> error @@ -103,6 +105,7 @@ set_group_opts(Host, Group, Opts) -> ?SQL_UPSERT_T( "sr_group", ["!name=%(Group)s", + "!server_host=%(Host)s", "opts=%(SOpts)s"]) end, ejabberd_sql:sql_transaction(Host, F). @@ -111,7 +114,8 @@ get_user_groups(US, Host) -> SJID = make_jid_s(US), case catch ejabberd_sql:sql_query( Host, - ?SQL("select @(grp)s from sr_user where jid=%(SJID)s")) of + ?SQL("select @(grp)s from sr_user" + " where jid=%(SJID)s and %(Host)H")) of {selected, Rs} -> [G || {G} <- Rs]; _ -> [] end. @@ -119,7 +123,8 @@ get_user_groups(US, Host) -> get_group_explicit_users(Host, Group) -> case catch ejabberd_sql:sql_query( Host, - ?SQL("select @(jid)s from sr_user where grp=%(Group)s")) of + ?SQL("select @(jid)s from sr_user" + " where grp=%(Group)s and %(Host)H")) of {selected, Rs} -> lists:map( fun({JID}) -> @@ -134,7 +139,8 @@ get_user_displayed_groups(LUser, LServer, GroupsOpts) -> SJID = make_jid_s(LUser, LServer), case catch ejabberd_sql:sql_query( LServer, - ?SQL("select @(grp)s from sr_user where jid=%(SJID)s")) of + ?SQL("select @(grp)s from sr_user" + " where jid=%(SJID)s and %(LServer)H")) of {selected, Rs} -> [{Group, proplists:get_value(Group, GroupsOpts, [])} || {Group} <- Rs]; @@ -146,7 +152,7 @@ is_user_in_group(US, Group, Host) -> case catch ejabberd_sql:sql_query( Host, ?SQL("select @(jid)s from sr_user where jid=%(SJID)s" - " and grp=%(Group)s")) of + " and %(Host)H and grp=%(Group)s")) of {selected, []} -> false; _ -> true end. @@ -155,15 +161,18 @@ add_user_to_group(Host, US, Group) -> SJID = make_jid_s(US), ejabberd_sql:sql_query( Host, - ?SQL("insert into sr_user(jid, grp) values (" - "%(SJID)s, %(Group)s)")). + ?SQL_INSERT( + "sr_user", + ["jid=%(SJID)s", + "server_host=%(Host)s", + "grp=%(Group)s"])). remove_user_from_group(Host, US, Group) -> SJID = make_jid_s(US), F = fun () -> ejabberd_sql:sql_query_t( - ?SQL("delete from sr_user where jid=%(SJID)s and" - " grp=%(Group)s")), + ?SQL("delete from sr_user where jid=%(SJID)s and %(Host)H" + " and grp=%(Group)s")), ok end, ejabberd_sql:sql_transaction(Host, F). @@ -173,9 +182,12 @@ export(_Server) -> fun(Host, #sr_group{group_host = {Group, LServer}, opts = Opts}) when LServer == Host -> SOpts = misc:term_to_expr(Opts), - [?SQL("delete from sr_group where name=%(Group)s;"), - ?SQL("insert into sr_group(name, opts) values (" - "%(Group)s, %(SOpts)s);")]; + [?SQL("delete from sr_group where name=%(Group)s and %(Host)H;"), + ?SQL_INSERT( + "sr_group", + ["name=%(Group)s", + "server_host=%(Host)s", + "opts=%(SOpts)s"])]; (_Host, _R) -> [] end}, @@ -184,9 +196,12 @@ export(_Server) -> when LServer == Host -> SJID = make_jid_s(U, S), [?SQL("select @(jid)s from sr_user where jid=%(SJID)s" - " and grp=%(Group)s;"), - ?SQL("insert into sr_user(jid, grp) values (" - "%(SJID)s, %(Group)s);")]; + " and %(Host)H and grp=%(Group)s;"), + ?SQL_INSERT( + "sr_user", + ["jid=%(SJID)s", + "server_host=%(Host)s", + "grp=%(Group)s"])]; (_Host, _R) -> [] end}]. diff --git a/src/mod_sip.erl b/src/mod_sip.erl index 7c3e60917..01327c77d 100644 --- a/src/mod_sip.erl +++ b/src/mod_sip.erl @@ -20,8 +20,9 @@ %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - +%%% %%%------------------------------------------------------------------- + -module(mod_sip). -protocol({rfc, 3261}). diff --git a/src/mod_sip_proxy.erl b/src/mod_sip_proxy.erl index 25f035377..d600da3d0 100644 --- a/src/mod_sip_proxy.erl +++ b/src/mod_sip_proxy.erl @@ -20,8 +20,9 @@ %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - +%%% %%%------------------------------------------------------------------- + -module(mod_sip_proxy). -ifndef(SIP). diff --git a/src/mod_sip_registrar.erl b/src/mod_sip_registrar.erl index 0e131eee6..a47de6974 100644 --- a/src/mod_sip_registrar.erl +++ b/src/mod_sip_registrar.erl @@ -20,8 +20,9 @@ %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - +%%% %%%------------------------------------------------------------------- + -module(mod_sip_registrar). -ifndef(SIP). diff --git a/src/mod_stream_mgmt.erl b/src/mod_stream_mgmt.erl index 2f6b0fc71..658bd504e 100644 --- a/src/mod_stream_mgmt.erl +++ b/src/mod_stream_mgmt.erl @@ -36,6 +36,7 @@ %% adjust pending session timeout -export([get_resume_timeout/1, set_resume_timeout/2]). +-include("ejabberd.hrl"). -include("xmpp.hrl"). -include("logger.hrl"). -include("p1_queue.hrl"). @@ -247,7 +248,10 @@ c2s_handle_info(#{mgmt_state := pending, {timeout, TRef, pending_timeout}) -> ?DEBUG("Timed out waiting for resumption of stream for ~s", [jid:encode(JID)]), - Mod:stop(State#{mgmt_state => timeout}); + Txt = <<"Timed out waiting for stream resumption">>, + Err = xmpp:serr_connection_timeout(Txt, ?MYLANG), + Mod:stop(State#{mgmt_state => timeout, + stop_reason => {stream, {out, Err}}}); c2s_handle_info(#{jid := JID} = State, {_Ref, {resume, OldState}}) -> %% This happens if the resume_session/1 request timed out; the new session %% now receives the late response. @@ -709,7 +713,7 @@ bounce_message_queue() -> %%%=================================================================== get_max_ack_queue(Host, Opts) -> gen_mod:get_module_opt(Host, ?MODULE, max_ack_queue, - gen_mod:get_opt(max_ack_queue, Opts, 1000)). + gen_mod:get_opt(max_ack_queue, Opts, 5000)). get_resume_timeout(Host, Opts) -> gen_mod:get_module_opt(Host, ?MODULE, resume_timeout, diff --git a/src/mod_vcard.erl b/src/mod_vcard.erl index 67d01a085..378b9430f 100644 --- a/src/mod_vcard.erl +++ b/src/mod_vcard.erl @@ -38,7 +38,7 @@ remove_user/2, export/1, import_info/0, import/5, import_start/2, depends/2, process_search/1, process_vcard/1, get_vcard/2, disco_items/5, disco_features/5, disco_identity/5, - decode_iq_subel/1, mod_opt_type/1, set_vcard/3, make_vcard_search/4]). + vcard_iq_set/1, mod_opt_type/1, set_vcard/3, make_vcard_search/4]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). @@ -95,6 +95,7 @@ init([Host, Opts]) -> ?NS_VCARD, ?MODULE, process_sm_iq, IQDisc), ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, get_sm_features, 50), + ejabberd_hooks:add(vcard_iq_set, Host, ?MODULE, vcard_iq_set, 50), MyHosts = gen_mod:get_opt_hosts(Host, Opts, <<"vjud.@HOST@">>), Search = gen_mod:get_opt(search, Opts, false), if Search -> @@ -152,6 +153,7 @@ terminate(_Reason, #state{hosts = MyHosts, server_host = Host}) -> gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_VCARD), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_VCARD), ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, get_sm_features, 50), + ejabberd_hooks:delete(vcard_iq_set, Host, ?MODULE, vcard_iq_set, 50), Mod = gen_mod:db_mod(Host, ?MODULE), Mod:stop(Host), lists:foreach( @@ -191,14 +193,6 @@ get_sm_features(Acc, _From, _To, Node, _Lang) -> _ -> Acc end. --spec decode_iq_subel(xmpp_element() | xmlel()) -> xmpp_element() | xmlel(). -%% Tell gen_iq_handler not to decode vcard elements -decode_iq_subel(El) -> - case xmpp:get_ns(El) of - ?NS_VCARD -> xmpp:encode(El); - _ -> xmpp:decode(El) - end. - -spec process_local_iq(iq()) -> iq(). process_local_iq(#iq{type = set, lang = Lang} = IQ) -> Txt = <<"Value 'set' of 'type' attribute is not allowed">>, @@ -212,13 +206,15 @@ process_local_iq(#iq{type = get, lang = Lang} = IQ) -> bday = <<"2002-11-16">>}). -spec process_sm_iq(iq()) -> iq(). -process_sm_iq(#iq{type = set, lang = Lang, from = From, - sub_els = [SubEl]} = IQ) -> - #jid{user = User, lserver = LServer} = From, +process_sm_iq(#iq{type = set, lang = Lang, from = From} = IQ) -> + #jid{lserver = LServer} = From, case lists:member(LServer, ?MYHOSTS) of true -> - set_vcard(User, LServer, SubEl), - xmpp:make_iq_result(IQ); + case ejabberd_hooks:run_fold(vcard_iq_set, LServer, IQ, []) of + drop -> ignore; + #stanza_error{} = Err -> xmpp:make_error(IQ, Err); + _ -> xmpp:make_iq_result(IQ) + end; false -> Txt = <<"The query is only allowed from local users">>, xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)) @@ -380,19 +376,33 @@ make_vcard_search(User, LUser, LServer, VCARD) -> orgunit = OrgUnit, lorgunit = LOrgUnit}. --spec set_vcard(binary(), binary(), xmlel()) -> {error, badarg} | ok. +-spec vcard_iq_set(iq()) -> iq() | {stop, stanza_error()}. +vcard_iq_set(#iq{from = From, lang = Lang, sub_els = [VCard]} = IQ) -> + #jid{user = User, lserver = LServer} = From, + case set_vcard(User, LServer, VCard) of + {error, badarg} -> + %% Should not be here? + Txt = <<"Nodeprep has failed">>, + {stop, xmpp:err_internal_server_error(Txt, Lang)}; + ok -> + IQ + end; +vcard_iq_set(Acc) -> + Acc. + +-spec set_vcard(binary(), binary(), xmlel() | vcard_temp()) -> {error, badarg} | ok. set_vcard(User, LServer, VCARD) -> case jid:nodeprep(User) of error -> {error, badarg}; LUser -> - VCardSearch = make_vcard_search(User, LUser, LServer, VCARD), + VCardEl = xmpp:encode(VCARD), + VCardSearch = make_vcard_search(User, LUser, LServer, VCardEl), Mod = gen_mod:db_mod(LServer, ?MODULE), - Mod:set_vcard(LUser, LServer, VCARD, VCardSearch), + Mod:set_vcard(LUser, LServer, VCardEl, VCardSearch), ets_cache:delete(?VCARD_CACHE, {LUser, LServer}, cache_nodes(Mod, LServer)), - ejabberd_hooks:run(vcard_set, LServer, - [LUser, LServer, VCARD]) + ok end. -spec string2lower(binary()) -> binary(). diff --git a/src/mod_vcard_ldap.erl b/src/mod_vcard_ldap.erl index 38c4747e6..88621fc0e 100644 --- a/src/mod_vcard_ldap.erl +++ b/src/mod_vcard_ldap.erl @@ -42,6 +42,7 @@ -include("logger.hrl"). -include("eldap.hrl"). -include("xmpp.hrl"). +-include("translate.hrl"). -define(PROCNAME, ejabberd_mod_vcard_ldap). @@ -324,31 +325,31 @@ default_vcard_map() -> {<<"PHOTO">>, <<"%s">>, [<<"jpegPhoto">>]}]. default_search_fields() -> - [{<<"User">>, <<"%u">>}, - {<<"Full Name">>, <<"displayName">>}, - {<<"Given Name">>, <<"givenName">>}, - {<<"Middle Name">>, <<"initials">>}, - {<<"Family Name">>, <<"sn">>}, - {<<"Nickname">>, <<"%u">>}, - {<<"Birthday">>, <<"birthDay">>}, - {<<"Country">>, <<"c">>}, - {<<"City">>, <<"l">>}, - {<<"Email">>, <<"mail">>}, - {<<"Organization Name">>, <<"o">>}, - {<<"Organization Unit">>, <<"ou">>}]. + [{?T("User"), <<"%u">>}, + {?T("Full Name"), <<"displayName">>}, + {?T("Given Name"), <<"givenName">>}, + {?T("Middle Name"), <<"initials">>}, + {?T("Family Name"), <<"sn">>}, + {?T("Nickname"), <<"%u">>}, + {?T("Birthday"), <<"birthDay">>}, + {?T("Country"), <<"c">>}, + {?T("City"), <<"l">>}, + {?T("Email"), <<"mail">>}, + {?T("Organization Name"), <<"o">>}, + {?T("Organization Unit"), <<"ou">>}]. default_search_reported() -> - [{<<"Full Name">>, <<"FN">>}, - {<<"Given Name">>, <<"FIRST">>}, - {<<"Middle Name">>, <<"MIDDLE">>}, - {<<"Family Name">>, <<"LAST">>}, - {<<"Nickname">>, <<"NICK">>}, - {<<"Birthday">>, <<"BDAY">>}, - {<<"Country">>, <<"CTRY">>}, - {<<"City">>, <<"LOCALITY">>}, - {<<"Email">>, <<"EMAIL">>}, - {<<"Organization Name">>, <<"ORGNAME">>}, - {<<"Organization Unit">>, <<"ORGUNIT">>}]. + [{?T("Full Name"), <<"FN">>}, + {?T("Given Name"), <<"FIRST">>}, + {?T("Middle Name"), <<"MIDDLE">>}, + {?T("Family Name"), <<"LAST">>}, + {?T("Nickname"), <<"NICK">>}, + {?T("Birthday"), <<"BDAY">>}, + {?T("Country"), <<"CTRY">>}, + {?T("City"), <<"LOCALITY">>}, + {?T("Email"), <<"EMAIL">>}, + {?T("Organization Name"), <<"ORGNAME">>}, + {?T("Organization Unit"), <<"ORGUNIT">>}]. parse_options(Host, Opts) -> MyHosts = gen_mod:get_opt_hosts(Host, Opts, <<"vjud.@HOST@">>), diff --git a/src/mod_vcard_mnesia.erl b/src/mod_vcard_mnesia.erl index d2f4ef52d..3e742ec15 100644 --- a/src/mod_vcard_mnesia.erl +++ b/src/mod_vcard_mnesia.erl @@ -36,6 +36,7 @@ -include("xmpp.hrl"). -include("mod_vcard.hrl"). -include("logger.hrl"). +-include("translate.hrl"). %%%=================================================================== %%% API @@ -95,32 +96,32 @@ search(LServer, Data, AllowReturnAll, MaxMatch) -> end. search_fields(_LServer) -> - [{<<"User">>, <<"user">>}, - {<<"Full Name">>, <<"fn">>}, - {<<"Name">>, <<"first">>}, - {<<"Middle Name">>, <<"middle">>}, - {<<"Family Name">>, <<"last">>}, - {<<"Nickname">>, <<"nick">>}, - {<<"Birthday">>, <<"bday">>}, - {<<"Country">>, <<"ctry">>}, - {<<"City">>, <<"locality">>}, - {<<"Email">>, <<"email">>}, - {<<"Organization Name">>, <<"orgname">>}, - {<<"Organization Unit">>, <<"orgunit">>}]. + [{?T("User"), <<"user">>}, + {?T("Full Name"), <<"fn">>}, + {?T("Name"), <<"first">>}, + {?T("Middle Name"), <<"middle">>}, + {?T("Family Name"), <<"last">>}, + {?T("Nickname"), <<"nick">>}, + {?T("Birthday"), <<"bday">>}, + {?T("Country"), <<"ctry">>}, + {?T("City"), <<"locality">>}, + {?T("Email"), <<"email">>}, + {?T("Organization Name"), <<"orgname">>}, + {?T("Organization Unit"), <<"orgunit">>}]. search_reported(_LServer) -> - [{<<"Jabber ID">>, <<"jid">>}, - {<<"Full Name">>, <<"fn">>}, - {<<"Name">>, <<"first">>}, - {<<"Middle Name">>, <<"middle">>}, - {<<"Family Name">>, <<"last">>}, - {<<"Nickname">>, <<"nick">>}, - {<<"Birthday">>, <<"bday">>}, - {<<"Country">>, <<"ctry">>}, - {<<"City">>, <<"locality">>}, - {<<"Email">>, <<"email">>}, - {<<"Organization Name">>, <<"orgname">>}, - {<<"Organization Unit">>, <<"orgunit">>}]. + [{?T("Jabber ID"), <<"jid">>}, + {?T("Full Name"), <<"fn">>}, + {?T("Name"), <<"first">>}, + {?T("Middle Name"), <<"middle">>}, + {?T("Family Name"), <<"last">>}, + {?T("Nickname"), <<"nick">>}, + {?T("Birthday"), <<"bday">>}, + {?T("Country"), <<"ctry">>}, + {?T("City"), <<"locality">>}, + {?T("Email"), <<"email">>}, + {?T("Organization Name"), <<"orgname">>}, + {?T("Organization Unit"), <<"orgunit">>}]. remove_user(LUser, LServer) -> US = {LUser, LServer}, diff --git a/src/mod_vcard_sql.erl b/src/mod_vcard_sql.erl index fd1d05478..07d90b69e 100644 --- a/src/mod_vcard_sql.erl +++ b/src/mod_vcard_sql.erl @@ -37,6 +37,13 @@ -include("mod_vcard.hrl"). -include("logger.hrl"). -include("ejabberd_sql_pt.hrl"). +-include("translate.hrl"). + +-ifdef(NEW_SQL_SCHEMA). +-define(USE_NEW_SCHEMA, true). +-else. +-define(USE_NEW_SCHEMA, false). +-endif. %%%=================================================================== %%% API @@ -53,7 +60,8 @@ is_search_supported(_LServer) -> get_vcard(LUser, LServer) -> case ejabberd_sql:sql_query( LServer, - ?SQL("select @(vcard)s from vcard where username=%(LUser)s")) of + ?SQL("select @(vcard)s from vcard" + " where username=%(LUser)s and %(LServer)H")) of {selected, [{SVCARD}]} -> case fxml_stream:parse_element(SVCARD) of {error, _Reason} -> error; @@ -93,10 +101,12 @@ set_vcard(LUser, LServer, VCARD, fun() -> ?SQL_UPSERT(LServer, "vcard", ["!username=%(LUser)s", + "!server_host=%(LServer)s", "vcard=%(SVCARD)s"]), ?SQL_UPSERT(LServer, "vcard_search", ["username=%(User)s", "!lusername=%(LUser)s", + "!server_host=%(LServer)s", "fn=%(FN)s", "lfn=%(LFN)s", "family=%(Family)s", @@ -150,41 +160,43 @@ search(LServer, Data, AllowReturnAll, MaxMatch) -> end. search_fields(_LServer) -> - [{<<"User">>, <<"user">>}, - {<<"Full Name">>, <<"fn">>}, - {<<"Name">>, <<"first">>}, - {<<"Middle Name">>, <<"middle">>}, - {<<"Family Name">>, <<"last">>}, - {<<"Nickname">>, <<"nick">>}, - {<<"Birthday">>, <<"bday">>}, - {<<"Country">>, <<"ctry">>}, - {<<"City">>, <<"locality">>}, - {<<"Email">>, <<"email">>}, - {<<"Organization Name">>, <<"orgname">>}, - {<<"Organization Unit">>, <<"orgunit">>}]. + [{?T("User"), <<"user">>}, + {?T("Full Name"), <<"fn">>}, + {?T("Name"), <<"first">>}, + {?T("Middle Name"), <<"middle">>}, + {?T("Family Name"), <<"last">>}, + {?T("Nickname"), <<"nick">>}, + {?T("Birthday"), <<"bday">>}, + {?T("Country"), <<"ctry">>}, + {?T("City"), <<"locality">>}, + {?T("Email"), <<"email">>}, + {?T("Organization Name"), <<"orgname">>}, + {?T("Organization Unit"), <<"orgunit">>}]. search_reported(_LServer) -> - [{<<"Jabber ID">>, <<"jid">>}, - {<<"Full Name">>, <<"fn">>}, - {<<"Name">>, <<"first">>}, - {<<"Middle Name">>, <<"middle">>}, - {<<"Family Name">>, <<"last">>}, - {<<"Nickname">>, <<"nick">>}, - {<<"Birthday">>, <<"bday">>}, - {<<"Country">>, <<"ctry">>}, - {<<"City">>, <<"locality">>}, - {<<"Email">>, <<"email">>}, - {<<"Organization Name">>, <<"orgname">>}, - {<<"Organization Unit">>, <<"orgunit">>}]. + [{?T("Jabber ID"), <<"jid">>}, + {?T("Full Name"), <<"fn">>}, + {?T("Name"), <<"first">>}, + {?T("Middle Name"), <<"middle">>}, + {?T("Family Name"), <<"last">>}, + {?T("Nickname"), <<"nick">>}, + {?T("Birthday"), <<"bday">>}, + {?T("Country"), <<"ctry">>}, + {?T("City"), <<"locality">>}, + {?T("Email"), <<"email">>}, + {?T("Organization Name"), <<"orgname">>}, + {?T("Organization Unit"), <<"orgunit">>}]. remove_user(LUser, LServer) -> ejabberd_sql:sql_transaction( LServer, fun() -> ejabberd_sql:sql_query_t( - ?SQL("delete from vcard where username=%(LUser)s")), + ?SQL("delete from vcard" + " where username=%(LUser)s and %(LServer)H")), ejabberd_sql:sql_query_t( - ?SQL("delete from vcard_search where lusername=%(LUser)s")) + ?SQL("delete from vcard_search" + " where lusername=%(LUser)s and %(LServer)H")) end). export(_Server) -> @@ -192,9 +204,12 @@ export(_Server) -> fun(Host, #vcard{us = {LUser, LServer}, vcard = VCARD}) when LServer == Host -> SVCARD = fxml:element_to_binary(VCARD), - [?SQL("delete from vcard where username=%(LUser)s;"), - ?SQL("insert into vcard(username, vcard) values (" - "%(LUser)s, %(SVCARD)s);")]; + [?SQL("delete from vcard" + " where username=%(LUser)s and %(LServer)H;"), + ?SQL_INSERT("vcard", + ["username=%(LUser)s", + "server_host=%(LServer)s", + "vcard=%(SVCARD)s"])]; (_Host, _R) -> [] end}, @@ -211,26 +226,34 @@ export(_Server) -> orgname = OrgName, lorgname = LOrgName, orgunit = OrgUnit, lorgunit = LOrgUnit}) when LServer == Host -> - [?SQL("delete from vcard_search where lusername=%(LUser)s;"), - ?SQL("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 (" - " %(LUser)s, %(User)s," - " %(FN)s, %(LFN)s," - " %(Family)s, %(LFamily)s," - " %(Given)s, %(LGiven)s," - " %(Middle)s, %(LMiddle)s," - " %(Nickname)s, %(LNickname)s," - " %(BDay)s, %(LBDay)s," - " %(CTRY)s, %(LCTRY)s," - " %(Locality)s, %(LLocality)s," - " %(EMail)s, %(LEMail)s," - " %(OrgName)s, %(LOrgName)s," - " %(OrgUnit)s, %(LOrgUnit)s);")]; + [?SQL("delete from vcard_search" + " where lusername=%(LUser)s and %(LServer)H;"), + ?SQL_INSERT("vcard_search", + ["username=%(User)s", + "lusername=%(LUser)s", + "server_host=%(LServer)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"])]; (_Host, _R) -> [] end}]. @@ -244,10 +267,19 @@ import(_, _, _) -> make_matchspec(LServer, Data) -> filter_fields(Data, <<"">>, LServer). -filter_fields([], Match, _LServer) -> - case Match of - <<"">> -> <<"">>; - _ -> [<<" where ">>, Match] +filter_fields([], Match, LServer) -> + case ?USE_NEW_SCHEMA of + true -> + SServer = ejabberd_sql:escape(LServer), + case Match of + <<"">> -> [<<"where server_host='">>, SServer, <<"'">>]; + _ -> [<<" where server_host='">>, SServer, <<"' and ">>, Match] + end; + false -> + case Match of + <<"">> -> <<"">>; + _ -> [<<" where ">>, Match] + end end; filter_fields([{SVar, [Val]} | Ds], Match, LServer) when is_binary(Val) and (Val /= <<"">>) -> diff --git a/src/mod_vcard_xupdate.erl b/src/mod_vcard_xupdate.erl index c9819913b..7643fed4a 100644 --- a/src/mod_vcard_xupdate.erl +++ b/src/mod_vcard_xupdate.erl @@ -30,8 +30,8 @@ %% gen_mod callbacks -export([start/2, stop/1, reload/3]). --export([update_presence/1, vcard_set/3, remove_user/2, - mod_opt_type/1, depends/2]). +-export([update_presence/1, vcard_set/1, remove_user/2, + user_send_packet/1, mod_opt_type/1, depends/2]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -47,15 +47,19 @@ start(Host, Opts) -> init_cache(Host, Opts), ejabberd_hooks:add(c2s_self_presence, Host, ?MODULE, update_presence, 100), - ejabberd_hooks:add(vcard_set, Host, ?MODULE, vcard_set, - 100), + ejabberd_hooks:add(user_send_packet, Host, ?MODULE, + user_send_packet, 50), + ejabberd_hooks:add(vcard_iq_set, Host, ?MODULE, vcard_set, + 90), ejabberd_hooks:add(remove_user, Host, ?MODULE, remove_user, 50). stop(Host) -> ejabberd_hooks:delete(c2s_self_presence, Host, ?MODULE, update_presence, 100), - ejabberd_hooks:delete(vcard_set, Host, ?MODULE, - vcard_set, 100), + ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, + user_send_packet, 50), + ejabberd_hooks:delete(vcard_iq_set, Host, ?MODULE, + vcard_set, 90), ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 50). reload(Host, NewOpts, _OldOpts) -> @@ -71,16 +75,33 @@ depends(_Host, _Opts) -> -> {presence(), ejabberd_c2s:state()}. update_presence({#presence{type = available} = Pres, #{jid := #jid{luser = LUser, lserver = LServer}} = State}) -> - Hash = get_xupdate(LUser, LServer), - Pres1 = xmpp:set_subtag(Pres, #vcard_xupdate{hash = Hash}), + Pres1 = case get_xupdate(LUser, LServer) of + undefined -> xmpp:remove_subtag(Pres, #vcard_xupdate{}); + XUpdate -> xmpp:set_subtag(Pres, XUpdate) + end, {Pres1, State}; update_presence(Acc) -> Acc. --spec vcard_set(binary(), binary(), xmlel()) -> ok. -vcard_set(LUser, LServer, _VCARD) -> +-spec user_send_packet({presence(), ejabberd_c2s:state()}) + -> {presence(), ejabberd_c2s:state()}. +user_send_packet({#presence{type = available, + to = #jid{luser = U, lserver = S, + lresource = <<"">>}}, + #{jid := #jid{luser = U, lserver = S}}} = Acc) -> + %% This is processed by update_presence/2 explicitly, we don't + %% want to call this multiple times for performance reasons + Acc; +user_send_packet(Acc) -> + update_presence(Acc). + +-spec vcard_set(iq()) -> iq(). +vcard_set(#iq{from = #jid{luser = LUser, lserver = LServer}} = IQ) -> ets_cache:delete(?VCARD_XUPDATE_CACHE, {LUser, LServer}), - ejabberd_sm:force_update_presence({LUser, LServer}). + ejabberd_sm:force_update_presence({LUser, LServer}), + IQ; +vcard_set(Acc) -> + Acc. -spec remove_user(binary(), binary()) -> ok. remove_user(User, Server) -> @@ -91,7 +112,7 @@ remove_user(User, Server) -> %%==================================================================== %% Storage %%==================================================================== --spec get_xupdate(binary(), binary()) -> binary() | undefined. +-spec get_xupdate(binary(), binary()) -> vcard_xupdate() | undefined. get_xupdate(LUser, LServer) -> Result = case use_cache(LServer) of true -> @@ -102,11 +123,12 @@ get_xupdate(LUser, LServer) -> db_get_xupdate(LUser, LServer) end, case Result of - {ok, Hash} -> Hash; - error -> undefined + {ok, external} -> undefined; + {ok, Hash} -> #vcard_xupdate{hash = Hash}; + error -> #vcard_xupdate{} end. --spec db_get_xupdate(binary(), binary()) -> {ok, binary()} | error. +-spec db_get_xupdate(binary(), binary()) -> {ok, binary() | external} | error. db_get_xupdate(LUser, LServer) -> case mod_vcard:get_vcard(LUser, LServer) of [VCard] -> @@ -147,17 +169,21 @@ use_cache(Host) -> Host, ?MODULE, use_cache, ejabberd_config:use_cache(Host)). --spec compute_hash(xmlel()) -> binary(). +-spec compute_hash(xmlel()) -> binary() | external. compute_hash(VCard) -> - case fxml:get_path_s(VCard, - [{elem, <<"PHOTO">>}, - {elem, <<"BINVAL">>}, - cdata]) of - <<>> -> + case fxml:get_subtag(VCard, <<"PHOTO">>) of + false -> <<>>; - BinVal -> - try str:sha(base64:decode(BinVal)) - catch _:badarg -> <<>> + Photo -> + try xmpp:decode(Photo, ?NS_VCARD, []) of + #vcard_photo{binval = <<_, _/binary>> = BinVal} -> + str:sha(BinVal); + #vcard_photo{extval = <<_, _/binary>>} -> + external; + _ -> + <<>> + catch _:{xmpp_codec, _} -> + <<>> end end. diff --git a/src/node_flat.erl b/src/node_flat.erl index 3989e0d94..18d4f4745 100644 --- a/src/node_flat.erl +++ b/src/node_flat.erl @@ -88,7 +88,6 @@ options() -> {max_payload_size, ?MAX_PAYLOAD_SIZE}, {send_last_published_item, on_sub_and_presence}, {deliver_notifications, true}, - {title, <<>>}, {presence_based_delivery, false}, {itemreply, none}]. @@ -376,23 +375,26 @@ publish_item(Nidx, Publisher, PublishModel, MaxItems, ItemId, Payload, true -> if MaxItems > 0 -> Now = p1_time_compat:timestamp(), - PubId = {Now, SubKey}, - Item = case get_item(Nidx, ItemId) of - {result, OldItem} -> - OldItem#pubsub_item{modification = PubId, - payload = Payload}; + case get_item(Nidx, ItemId) of + {result, #pubsub_item{creation = {_, GenKey}} = OldItem} -> + set_item(OldItem#pubsub_item{ + modification = {Now, SubKey}, + payload = Payload}), + {result, {default, broadcast, []}}; + {result, _} -> + {error, xmpp:err_forbidden()}; _ -> - #pubsub_item{itemid = {ItemId, Nidx}, - nodeidx = Nidx, - creation = {Now, GenKey}, - modification = PubId, - payload = Payload} - end, - Items = [ItemId | GenState#pubsub_state.items -- [ItemId]], - {result, {NI, OI}} = remove_extra_items(Nidx, MaxItems, Items), - set_item(Item), - set_state(GenState#pubsub_state{items = NI}), - {result, {default, broadcast, OI}}; + Items = [ItemId | GenState#pubsub_state.items], + {result, {NI, OI}} = remove_extra_items(Nidx, MaxItems, Items), + set_state(GenState#pubsub_state{items = NI}), + set_item(#pubsub_item{ + itemid = {ItemId, Nidx}, + nodeidx = Nidx, + creation = {Now, GenKey}, + modification = {Now, SubKey}, + payload = Payload}), + {result, {default, broadcast, OI}} + end; true -> {result, {default, broadcast, []}} end @@ -443,21 +445,30 @@ delete_item(Nidx, Publisher, PublishModel, ItemId) -> case Affiliation of owner -> {result, States} = get_states(Nidx), + Records = States ++ mnesia:read({pubsub_orphan, Nidx}), lists:foldl(fun - (#pubsub_state{items = PI} = S, Res) -> - case lists:member(ItemId, PI) of + (#pubsub_state{items = RI} = S, Res) -> + case lists:member(ItemId, RI) of true -> - Nitems = lists:delete(ItemId, PI), + NI = lists:delete(ItemId, RI), del_item(Nidx, ItemId), - set_state(S#pubsub_state{items = Nitems}), + mnesia:write(S#pubsub_state{items = NI}), {result, {default, broadcast}}; false -> Res end; - (_, Res) -> - Res + (#pubsub_orphan{items = RI} = S, Res) -> + case lists:member(ItemId, RI) of + true -> + NI = lists:delete(ItemId, RI), + del_item(Nidx, ItemId), + mnesia:write(S#pubsub_orphan{items = NI}), + {result, {default, broadcast}}; + false -> + Res + end end, - {error, xmpp:err_item_not_found()}, States); + {error, xmpp:err_item_not_found()}, Records); _ -> {error, xmpp:err_forbidden()} end @@ -725,9 +736,53 @@ del_state(#pubsub_state{stateid = {Key, Nidx}, items = Items}) -> %% mod_pubsub module.</p> %% <p>PubSub plugins can store the items where they wants (for example in a %% relational database), or they can even decide not to persist any items.</p> -get_items(Nidx, _From, _RSM) -> - Items = mnesia:index_read(pubsub_item, Nidx, #pubsub_item.nodeidx), - {result, {lists:reverse(lists:keysort(#pubsub_item.modification, Items)), undefined}}. +get_items(Nidx, _From, undefined) -> + RItems = lists:keysort(#pubsub_item.creation, + mnesia:index_read(pubsub_item, Nidx, #pubsub_item.nodeidx)), + Count = length(RItems), + if Count =< ?MAXITEMS -> + {result, {RItems, undefined}}; + true -> + ItemsPage = lists:sublist(RItems, ?MAXITEMS), + Rsm = rsm_page(Count, 0, 0, ItemsPage), + {result, {ItemsPage, Rsm}} + end; + +get_items(Nidx, _From, #rsm_set{max = Max, index = IncIndex, + 'after' = After, before = Before}) -> + RItems = lists:keysort(#pubsub_item.creation, + mnesia:index_read(pubsub_item, Nidx, #pubsub_item.nodeidx)), + Count = length(RItems), + Limit = case Max of + undefined -> ?MAXITEMS; + _ -> Max + end, + {Offset, ItemsPage} = + case {IncIndex, Before, After} of + {I, undefined, undefined} -> + SubList = lists:nthtail(I, RItems), + {I, lists:sublist(SubList, Limit)}; + {_, <<>>, undefined} -> + %% 2.5 Requesting the Last Page in a Result Set + SubList = lists:reverse(RItems), + {0, lists:sublist(SubList, Limit)}; + {_, Stamp, undefined} -> + BeforeNow = encode_stamp(Stamp), + SubList = lists:dropwhile( + fun(#pubsub_item{creation = {Now, _}}) -> + Now >= BeforeNow + end, lists:reverse(RItems)), + {0, lists:sublist(SubList, Limit)}; + {_, undefined, Stamp} -> + AfterNow = encode_stamp(Stamp), + SubList = lists:dropwhile( + fun(#pubsub_item{creation = {Now, _}}) -> + Now =< AfterNow + end, RItems), + {0, lists:sublist(SubList, Limit)} + end, + Rsm = rsm_page(Count, IncIndex, Offset, ItemsPage), + {result, {ItemsPage, Rsm}}. get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId, RSM) -> SubKey = jid:tolower(JID), @@ -765,9 +820,10 @@ get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId, RSM get_items(Nidx, JID, RSM) end. -get_last_items(Nidx, From, Count) when Count > 0 -> - {result, {Items, _}} = get_items(Nidx, From, undefined), - {result, lists:sublist(Items, Count)}; +get_last_items(Nidx, _From, Count) when Count > 0 -> + Items = mnesia:index_read(pubsub_item, Nidx, #pubsub_item.nodeidx), + LastItems = lists:reverse(lists:keysort(#pubsub_item.modification, Items)), + {result, lists:sublist(LastItems, Count)}; get_last_items(_Nidx, _From, _Count) -> {result, []}. @@ -876,6 +932,26 @@ first_in_list(Pred, [H | T]) -> _ -> first_in_list(Pred, T) end. +rsm_page(Count, Index, Offset, Items) -> + FirstItem = hd(Items), + LastItem = lists:last(Items), + First = decode_stamp(element(1, FirstItem#pubsub_item.creation)), + Last = decode_stamp(element(1, LastItem#pubsub_item.creation)), + #rsm_set{count = Count, index = Index, + first = #rsm_first{index = Offset, data = First}, + last = Last}. + +encode_stamp(Stamp) -> + case catch xmpp_util:decode_timestamp(Stamp) of + {MS,S,US} -> {MS,S,US}; + _ -> Stamp + end. +decode_stamp(Stamp) -> + case catch xmpp_util:encode_timestamp(Stamp) of + TimeStamp when is_binary(TimeStamp) -> TimeStamp; + _ -> Stamp + end. + transform({pubsub_state, {Id, Nidx}, Is, A, Ss}) -> {pubsub_state, {Id, Nidx}, Nidx, Is, A, Ss}; transform({pubsub_item, {Id, Nidx}, C, M, P}) -> diff --git a/src/node_flat_sql.erl b/src/node_flat_sql.erl index 72281a970..8057cf2e1 100644 --- a/src/node_flat_sql.erl +++ b/src/node_flat_sql.erl @@ -243,20 +243,31 @@ publish_item(Nidx, Publisher, PublishModel, MaxItems, ItemId, Payload, if not ((PublishModel == open) or (PublishModel == publishers) and ((Affiliation == owner) - or (Affiliation == publisher) - or (Affiliation == publish_only)) + or (Affiliation == publisher) + or (Affiliation == publish_only)) or (Subscribed == true)) -> {error, xmpp:err_forbidden()}; true -> if MaxItems > 0 -> - PubId = {p1_time_compat:timestamp(), SubKey}, - set_item(#pubsub_item{itemid = {ItemId, Nidx}, - creation = {p1_time_compat:timestamp(), GenKey}, - modification = PubId, - payload = Payload}), - Items = [ItemId | itemids(Nidx, GenKey) -- [ItemId]], - {result, {_, OI}} = remove_extra_items(Nidx, MaxItems, Items), - {result, {default, broadcast, OI}}; + Now = p1_time_compat:timestamp(), + case get_item(Nidx, ItemId) of + {result, #pubsub_item{creation = {_, GenKey}} = OldItem} -> + set_item(OldItem#pubsub_item{ + modification = {Now, SubKey}, + payload = Payload}), + {result, {default, broadcast, []}}; + {result, _} -> + {error, xmpp:err_forbidden()}; + _ -> + Items = [ItemId | itemids(Nidx, GenKey)], + {result, {_NI, OI}} = remove_extra_items(Nidx, MaxItems, Items), + set_item(#pubsub_item{ + itemid = {ItemId, Nidx}, + creation = {Now, GenKey}, + modification = {Now, SubKey}, + payload = Payload}), + {result, {default, broadcast, OI}} + end; true -> {result, {default, broadcast, []}} end @@ -284,9 +295,23 @@ delete_item(Nidx, Publisher, PublishModel, ItemId) -> if not Allowed -> {error, xmpp:err_forbidden()}; true -> - case del_item(Nidx, ItemId) of - {updated, 1} -> {result, {default, broadcast}}; - _ -> {error, xmpp:err_item_not_found()} + Items = itemids(Nidx, GenKey), + case lists:member(ItemId, Items) of + true -> + case del_item(Nidx, ItemId) of + {updated, 1} -> {result, {default, broadcast}}; + _ -> {error, xmpp:err_item_not_found()} + end; + false -> + case Affiliation of + owner -> + case del_item(Nidx, ItemId) of + {updated, 1} -> {result, {default, broadcast}}; + _ -> {error, xmpp:err_item_not_found()} + end; + _ -> + {error, xmpp:err_forbidden()} + end end end. @@ -647,95 +672,61 @@ del_state(Nidx, JID) -> " where jid=%(J)s and nodeid=%(Nidx)d")), ok. -get_items(Nidx, From, undefined) -> - MaxItems = case ejabberd_sql:sql_query_t( - ?SQL("select @(val)s from pubsub_node_option " - "where nodeid=%(Nidx)d and name='max_items'")) of - {selected, [{Value}]} -> - misc:expr_to_term(Value); - _ -> - ?MAXITEMS - end, - get_items(Nidx, From, #rsm_set{max = MaxItems}); +get_items(Nidx, _From, undefined) -> + SNidx = misc:i2l(Nidx), + case ejabberd_sql:sql_query_t( + [<<"select itemid, publisher, creation, modification, payload", + " from pubsub_item where nodeid='", SNidx/binary, "'", + " order by creation asc">>]) of + {selected, _, AllItems} -> + Count = length(AllItems), + if Count =< ?MAXITEMS -> + {result, {[raw_to_item(Nidx, RItem) || RItem <- AllItems], undefined}}; + true -> + RItems = lists:sublist(AllItems, ?MAXITEMS), + Rsm = rsm_page(Count, 0, 0, RItems), + {result, {[raw_to_item(Nidx, RItem) || RItem <- RItems], Rsm}} + end; + _ -> + {result, {[], undefined}} + end; get_items(Nidx, _From, #rsm_set{max = Max, index = IncIndex, 'after' = After, before = Before}) -> - {Way, Order} = if After == <<>> -> {<<"is not">>, <<"desc">>}; - After /= undefined -> {<<"<">>, <<"desc">>}; - Before == <<>> -> {<<"is not">>, <<"asc">>}; - Before /= undefined -> {<<">">>, <<"asc">>}; - true -> {<<"is not">>, <<"desc">>} - end, - SNidx = misc:i2l(Nidx), - I = if After /= undefined -> After; - Before /= undefined -> Before; - true -> undefined - end, - [AttrName, Id] = - case I of - undefined when IncIndex =/= undefined -> - case ejabberd_sql:sql_query_t( - [<<"select creation from pubsub_item pi " - "where exists ( select count(*) as count1 " - "from pubsub_item where nodeid='">>, SNidx, - <<"' and creation > pi.creation having count1 = ">>, - integer_to_binary(IncIndex), <<" );">>]) of - {selected, [_], [[O]]} -> - [<<"creation">>, <<"'", O/binary, "'">>]; - _ -> - [<<"creation">>, <<"null">>] - end; - undefined -> - [<<"creation">>, <<"null">>]; - <<>> -> - [<<"creation">>, <<"null">>]; - I -> - [A, B] = str:tokens(ejabberd_sql:escape(I), <<"@">>), - [A, <<"'", B/binary, "'">>] - end, - Count = case ejabberd_sql:sql_query_t( - [<<"select count(*) from pubsub_item where nodeid='">>, - SNidx, <<"';">>]) of - {selected, [_], [[C]]} -> binary_to_integer(C); + Count = case catch ejabberd_sql:sql_query_t( + ?SQL("select @(count(itemid))d from pubsub_item" + " where nodeid=%(Nidx)d")) of + {selected, [{C}]} -> C; _ -> 0 end, + Offset = case {IncIndex, Before, After} of + {I, undefined, undefined} when is_integer(I) -> I; + _ -> 0 + end, + Limit = case Max of + undefined -> ?MAXITEMS; + _ -> Max + end, + Filters = rsm_filters(misc:i2l(Nidx), Before, After), Query = fun(mssql, _) -> ejabberd_sql:sql_query_t( - [<<"select top ">>, integer_to_binary(Max), - <<" itemid, publisher, creation, modification, payload " - "from pubsub_item where nodeid='">>, SNidx, - <<"' and ">>, AttrName, <<" ">>, Way, <<" ">>, Id, <<" order by ">>, - AttrName, <<" ">>, Order, <<";">>]); + [<<"select top ", (integer_to_binary(Limit))/binary, + " itemid, publisher, creation, modification, payload", + " from pubsub_item", Filters/binary>>]); + %OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY; (_, _) -> ejabberd_sql:sql_query_t( - [<<"select itemid, publisher, creation, modification, payload " - "from pubsub_item where nodeid='">>, SNidx, - <<"' and ">>, AttrName, <<" ">>, Way, <<" ">>, Id, <<" order by ">>, - AttrName, <<" ">>, Order, <<" limit ">>, - integer_to_binary(Max), <<" ;">>]) + [<<"select itemid, publisher, creation, modification, payload", + " from pubsub_item", Filters/binary, + " limit ", (integer_to_binary(Limit))/binary, + " offset ", (integer_to_binary(Offset))/binary>>]) end, case ejabberd_sql:sql_query_t(Query) of + {selected, _, []} -> + {result, {[], #rsm_set{count = Count}}}; {selected, [<<"itemid">>, <<"publisher">>, <<"creation">>, <<"modification">>, <<"payload">>], RItems} -> - case RItems of - [[_, _, _, F, _]|_] -> - Index = case catch ejabberd_sql:sql_query_t( - [<<"select count(*) from pubsub_item " - "where nodeid='">>, SNidx, <<"' and ">>, - AttrName, <<" > '">>, F, <<"';">>]) of - {selected, [_], [[In]]} -> binary_to_integer(In); - _ -> 0 - end, - [_, _, _, L, _] = lists:last(RItems), - RsmOut = #rsm_set{count = Count, - index = Index, - first = #rsm_first{ - index = Index, - data = <<"creation@", F/binary>>}, - last = <<"creation@", L/binary>>}, - {result, {[raw_to_item(Nidx, RItem) || RItem <- RItems], RsmOut}}; - [] -> - {result, {[], #rsm_set{count = Count}}} - end; + Rsm = rsm_page(Count, IncIndex, Offset, RItems), + {result, {[raw_to_item(Nidx, RItem) || RItem <- RItems], Rsm}}; _ -> {result, {[], undefined}} end. @@ -773,24 +764,24 @@ get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId, RSM get_items(Nidx, JID, RSM) end. -get_last_items(Nidx, _From, Count) -> - Limit = misc:i2l(Count), +get_last_items(Nidx, _From, Limit) -> SNidx = misc:i2l(Nidx), Query = fun(mssql, _) -> ejabberd_sql:sql_query_t( - [<<"select top ">>, Limit, - <<" itemid, publisher, creation, modification, payload " - "from pubsub_item where nodeid='">>, SNidx, - <<"' order by modification desc ;">>]); + [<<"select top ", (integer_to_binary(Limit))/binary, + " itemid, publisher, creation, modification, payload", + " from pubsub_item where nodeid='", SNidx/binary, + "' order by modification desc">>]); (_, _) -> ejabberd_sql:sql_query_t( - [<<"select itemid, publisher, creation, modification, payload " - "from pubsub_item where nodeid='">>, SNidx, - <<"' order by modification desc limit ">>, Limit, <<";">>]) + [<<"select itemid, publisher, creation, modification, payload", + " from pubsub_item where nodeid='", SNidx/binary, + "' order by modification desc ", + " limit ", (integer_to_binary(Limit))/binary>>]) end, case catch ejabberd_sql:sql_query_t(Query) of - {selected, - [<<"itemid">>, <<"publisher">>, <<"creation">>, <<"modification">>, <<"payload">>], RItems} -> + {selected, [<<"itemid">>, <<"publisher">>, <<"creation">>, + <<"modification">>, <<"payload">>], RItems} -> {result, [raw_to_item(Nidx, RItem) || RItem <- RItems]}; _ -> {result, []} @@ -798,9 +789,9 @@ get_last_items(Nidx, _From, Count) -> get_item(Nidx, ItemId) -> case catch ejabberd_sql:sql_query_t( - ?SQL("select @(itemid)s, @(publisher)s, @(creation)s," - " @(modification)s, @(payload)s from pubsub_item" - " where nodeid=%(Nidx)d and itemid=%(ItemId)s")) + ?SQL("select @(itemid)s, @(publisher)s, @(creation)s," + " @(modification)s, @(payload)s from pubsub_item" + " where nodeid=%(Nidx)d and itemid=%(ItemId)s")) of {selected, [RItem]} -> {result, raw_to_item(Nidx, RItem)}; @@ -850,11 +841,8 @@ set_item(Item) -> P = encode_jid(JID), Payload = Item#pubsub_item.payload, XML = str:join([fxml:element_to_binary(X) || X<-Payload], <<>>), - S = fun ({T1, T2, T3}) -> - str:join([misc:i2l(T1, 6), misc:i2l(T2, 6), misc:i2l(T3, 6)], <<":">>) - end, - SM = S(M), - SC = S(C), + SM = encode_now(M), + SC = encode_now(C), ?SQL_UPSERT_T( "pubsub_item", ["!nodeid=%(Nidx)d", @@ -1029,15 +1017,53 @@ raw_to_item(Nidx, [ItemId, SJID, Creation, Modification, XML]) -> raw_to_item(Nidx, {ItemId, SJID, Creation, Modification, XML}); raw_to_item(Nidx, {ItemId, SJID, Creation, Modification, XML}) -> JID = decode_jid(SJID), - ToTime = fun (Str) -> - [T1, T2, T3] = str:tokens(Str, <<":">>), - {misc:l2i(T1), misc:l2i(T2), misc:l2i(T3)} - end, Payload = case fxml_stream:parse_element(XML) of {error, _Reason} -> []; El -> [El] end, #pubsub_item{itemid = {ItemId, Nidx}, - creation = {ToTime(Creation), jid:remove_resource(JID)}, - modification = {ToTime(Modification), JID}, + creation = {decode_now(Creation), jid:remove_resource(JID)}, + modification = {decode_now(Modification), JID}, payload = Payload}. + +rsm_filters(SNidx, undefined, undefined) -> + <<" where nodeid='", SNidx/binary, "'", + " order by creation asc">>; +rsm_filters(SNidx, undefined, After) -> + <<" where nodeid='", SNidx/binary, "'", + " and creation>'", (encode_stamp(After))/binary, "'", + " order by creation asc">>; +rsm_filters(SNidx, <<>>, undefined) -> + %% 2.5 Requesting the Last Page in a Result Set + <<" where nodeid='", SNidx/binary, "'", + " order by creation desc">>; +rsm_filters(SNidx, Before, undefined) -> + <<" where nodeid='", SNidx/binary, "'", + " and creation<'", (encode_stamp(Before))/binary, "'", + " order by creation desc">>. + +rsm_page(Count, Index, Offset, Items) -> + First = decode_stamp(lists:nth(3, hd(Items))), + Last = decode_stamp(lists:nth(3, lists:last(Items))), + #rsm_set{count = Count, index = Index, + first = #rsm_first{index = Offset, data = First}, + last = Last}. + +encode_stamp(Stamp) -> + case catch xmpp_util:decode_timestamp(Stamp) of + {MS,S,US} -> encode_now({MS,S,US}); + _ -> Stamp + end. +decode_stamp(Stamp) -> + case catch xmpp_util:encode_timestamp(decode_now(Stamp)) of + TimeStamp when is_binary(TimeStamp) -> TimeStamp; + _ -> Stamp + end. + +encode_now({T1, T2, T3}) -> + <<(misc:i2l(T1, 6))/binary, ":", + (misc:i2l(T2, 6))/binary, ":", + (misc:i2l(T3, 6))/binary>>. +decode_now(NowStr) -> + [MS, S, US] = binary:split(NowStr, <<":">>, [global]), + {binary_to_integer(MS), binary_to_integer(S), binary_to_integer(US)}. diff --git a/src/nodetree_tree.erl b/src/nodetree_tree.erl index f87582c9f..317240366 100644 --- a/src/nodetree_tree.erl +++ b/src/nodetree_tree.erl @@ -86,15 +86,26 @@ get_nodes(Host, _From) -> get_nodes(Host) -> mnesia:match_object(#pubsub_node{nodeid = {Host, '_'}, _ = '_'}). -get_parentnodes(_Host, _Node, _From) -> - []. +get_parentnodes(Host, Node, _From) -> + case catch mnesia:read({pubsub_node, {Host, Node}}) of + [Record] when is_record(Record, pubsub_node) -> + Record#pubsub_node.parents; + _ -> + [] + end. -%% @doc <p>Default node tree does not handle parents, return a list -%% containing just this node.</p> get_parentnodes_tree(Host, Node, _From) -> + get_parentnodes_tree(Host, Node, 0, []). +get_parentnodes_tree(Host, Node, Level, Acc) -> case catch mnesia:read({pubsub_node, {Host, Node}}) of - [Record] when is_record(Record, pubsub_node) -> [{0, [Record]}]; - _ -> [] + [Record] when is_record(Record, pubsub_node) -> + Tree = [{Level, [Record]}|Acc], + case Record#pubsub_node.parents of + [Parent] -> get_parentnodes_tree(Host, Parent, Level+1, Tree); + _ -> Tree + end; + _ -> + Acc end. get_subnodes(Host, Node, _From) -> diff --git a/src/nodetree_tree_sql.erl b/src/nodetree_tree_sql.erl index 3af035f6c..73ab74e8a 100644 --- a/src/nodetree_tree_sql.erl +++ b/src/nodetree_tree_sql.erl @@ -160,15 +160,26 @@ get_nodes(Host) -> [] end. -get_parentnodes(_Host, _Node, _From) -> - []. +get_parentnodes(Host, Node, _From) -> + case get_node(Host, Node) of + Record when is_record(Record, pubsub_node) -> + Record#pubsub_node.parents; + _ -> + [] + end. -%% @doc <p>Default node tree does not handle parents, return a list -%% containing just this node.</p> -get_parentnodes_tree(Host, Node, From) -> - case get_node(Host, Node, From) of - {error, _} -> []; - Record -> [{0, [Record]}] +get_parentnodes_tree(Host, Node, _From) -> + get_parentnodes_tree(Host, Node, 0, []). +get_parentnodes_tree(Host, Node, Level, Acc) -> + case get_node(Host, Node) of + Record when is_record(Record, pubsub_node) -> + Tree = [{Level, [Record]}|Acc], + case Record#pubsub_node.parents of + [Parent] -> get_parentnodes_tree(Host, Parent, Level+1, Tree); + _ -> Tree + end; + _ -> + Acc end. get_subnodes(Host, Node, _From) -> @@ -287,7 +298,7 @@ raw_to_node(Host, {Node, Parent, Type, Nidx}) -> Module = misc:binary_to_atom(<<"node_", Type/binary, "_sql">>), StdOpts = Module:options(), lists:foldl(fun ({Key, Value}, Acc) -> - lists:keyreplace(Key, 1, Acc, {Key, Value}) + lists:keystore(Key, 1, Acc, {Key, Value}) end, StdOpts, DbOpts); _ -> diff --git a/src/pubsub_db_sql.erl b/src/pubsub_db_sql.erl index ae28184db..028c99b8e 100644 --- a/src/pubsub_db_sql.erl +++ b/src/pubsub_db_sql.erl @@ -144,92 +144,61 @@ sql_to_boolean(B) -> B == <<"1">>. sql_to_timestamp(T) -> xmpp_util:decode_timestamp(T). -%% REVIEW: -%% * this code takes NODEID from Itemid2, and forgets about Nodeidx -%% * this code assumes Payload only contains one xmlelement() -%% * PUBLISHER is taken from Creation export(_Server) -> - [{pubsub_item, - fun(_Host, #pubsub_item{itemid = {Itemid1, NODEID}, - %nodeidx = _Nodeidx, - creation = {{C1, C2, C3}, Cusr}, - modification = {{M1, M2, M3}, _Musr}, - payload = Payload}) -> - ITEMID = ejabberd_sql:escape(Itemid1), - CREATION = ejabberd_sql:escape(list_to_binary( - string:join([string:right(integer_to_list(I),6,$0)||I<-[C1,C2,C3]],":"))), - MODIFICATION = ejabberd_sql:escape(list_to_binary( - string:join([string:right(integer_to_list(I),6,$0)||I<-[M1,M2,M3]],":"))), - PUBLISHER = ejabberd_sql:escape(jid:encode(Cusr)), - [PayloadEl] = [El || {xmlel,_,_,_} = El <- Payload], - PAYLOAD = ejabberd_sql:escape(fxml:element_to_binary(PayloadEl)), - [?SQL("delete from pubsub_item where itemid=%(ITEMID)s;"), - ?SQL("insert into pubsub_item(itemid,nodeid,creation,modification,publisher,payload) \n" - " values (%(ITEMID)s, %(NODEID)d, %(CREATION)s, - %(MODIFICATION)s, %(PUBLISHER)s, %(PAYLOAD)s);")]; - (_Host, _R) -> - [] - end}, -%% REVIEW: -%% * From the mnesia table, the #pubsub_state.items is not used in ODBC -%% * Right now AFFILIATION is the first letter of Affiliation -%% * Right now SUBSCRIPTIONS expects only one Subscription -%% * Right now SUBSCRIPTIONS letter is the first letter of Subscription - {pubsub_state, - fun(_Host, #pubsub_state{stateid = {Jid, Stateid}, - %nodeidx = Nodeidx, - items = _Items, - affiliation = Affiliation, - subscriptions = Subscriptions}) -> - STATEID = list_to_binary(integer_to_list(Stateid)), - JID = ejabberd_sql:escape(jid:encode(Jid)), - NODEID = <<"unknown">>, %% TODO: integer_to_list(Nodeidx), - AFFILIATION = list_to_binary(string:substr(atom_to_list(Affiliation),1,1)), - SUBSCRIPTIONS = list_to_binary(parse_subscriptions(Subscriptions)), - [?SQL("delete from pubsub_state where stateid=%(STATEID)s;"), - ?SQL("insert into pubsub_state(stateid,jid,nodeid,affiliation,subscriptions)\n" - " values (%(STATEID)s, %(JID)s, %(NODEID)s, %(AFFILIATION)s, %(SUBSCRIPTIONS)s);")]; - (_Host, _R) -> - [] - end}, - -%% REVIEW: -%% * Parents is not migrated to PARENTs -%% * Probably some option VALs are not correctly represented in mysql - {pubsub_node, - fun(_Host, #pubsub_node{nodeid = {Hostid, Nodeid}, - id = Id, - parents = _Parents, - type = Type, - owners = Owners, - options = Options}) -> - HOST = case Hostid of - {U,S,R} -> ejabberd_sql:escape(jid:encode({U,S,R})); - _ -> ejabberd_sql:escape(Hostid) - end, - NODE = ejabberd_sql:escape(Nodeid), - PARENT = <<"">>, - IdB = integer_to_binary(Id), - TYPE = ejabberd_sql:escape(<<Type/binary, "_odbc">>), - [?SQL("delete from pubsub_node where nodeid=%(Id)d;"), - ?SQL("insert into pubsub_node(host,node,nodeid,parent,type) \n" - " values (%(HOST)s, %(NODE)s, %(Id)d, %(PARENT)s, %(TYPE)s);"), - ?SQL("delete from pubsub_node_option where nodeid=%(Id)d;"), - [["insert into pubsub_node_option(nodeid,name,val)\n" - " values (", IdB, ", '", atom_to_list(Name), "', '", - io_lib:format("~p", [Val]), "');\n"] || {Name,Val} <- Options], - ?SQL("delete from pubsub_node_owner where nodeid=%(Id)d;"), - [["insert into pubsub_node_owner(nodeid,owner)\n" - " values (", IdB, ", '", jid:encode(Usr), "');\n"] || Usr <- Owners],"\n"]; - (_Host, _R) -> - [] - end}]. - -parse_subscriptions([]) -> - ""; -parse_subscriptions([{State, Item}]) -> - STATE = case State of - subscribed -> "s" - end, - string:join([STATE, Item],":"). - + [{pubsub_node, + fun(_Host, #pubsub_node{nodeid = {Host, Node}, id = Nidx, + parents = Parents, type = Type, + options = Options}) -> + H = node_flat_sql:encode_host(Host), + Parent = case Parents of + [] -> <<>>; + [First | _] -> First + end, + [?SQL("delete from pubsub_node where nodeid=%(Nidx)d;"), + ?SQL("delete from pubsub_node_option where nodeid=%(Nidx)d;"), + ?SQL("delete from pubsub_node_owner where nodeid=%(Nidx)d;"), + ?SQL("delete from pubsub_state where nodeid=%(Nidx)d;"), + ?SQL("delete from pubsub_item where nodeid=%(Nidx)d;"), + ?SQL("insert into pubsub_node(host,node,nodeid,parent,type)" + " values (%(H)s, %(Node)s, %(Nidx)d, %(Parent)s, %(Type)s);")] + ++ lists:map( + fun ({Key, Value}) -> + SKey = iolist_to_binary(atom_to_list(Key)), + SValue = misc:term_to_expr(Value), + ?SQL("insert into pubsub_node_option(nodeid,name,val)" + " values (%(Nidx)d, %(SKey)s, %(SValue)s);") + end, Options); + (_Host, _R) -> + [] + end}, + {pubsub_state, + fun(_Host, #pubsub_state{stateid = {JID, Nidx}, + affiliation = Affiliation, + subscriptions = Subscriptions}) -> + J = jid:encode(JID), + S = node_flat_sql:encode_subscriptions(Subscriptions), + A = node_flat_sql:encode_affiliation(Affiliation), + [?SQL("insert into pubsub_state(nodeid,jid,affiliation,subscriptions)" + " values (%(Nidx)d, %(J)s, %(A)s, %(S)s);")]; + (_Host, _R) -> + [] + end}, + {pubsub_item, + fun(_Host, #pubsub_item{itemid = {ItemId, Nidx}, + creation = {C, _}, + modification = {M, JID}, + payload = Payload}) -> + P = jid:encode(JID), + XML = str:join([fxml:element_to_binary(X) || X<-Payload], <<>>), + SM = encode_now(M), + SC = encode_now(C), + [?SQL("insert into pubsub_item(itemid,nodeid,creation,modification,publisher,payload)" + " values (%(ItemId)s, %(Nidx)d, %(SC)s, %(SM)s, %(P)s, %(XML)s);")]; + (_Host, _R) -> + [] + end}]. + +encode_now({T1, T2, T3}) -> + <<(misc:i2l(T1, 6))/binary, ":", + (misc:i2l(T2, 6))/binary, ":", + (misc:i2l(T3, 6))/binary>>. diff --git a/src/xmpp_stream_in.erl b/src/xmpp_stream_in.erl index 253adbf95..329ebad61 100644 --- a/src/xmpp_stream_in.erl +++ b/src/xmpp_stream_in.erl @@ -671,7 +671,7 @@ process_stream_established(#{stream_state := StateName} = State) when StateName == disconnected; StateName == established -> State; process_stream_established(#{mod := Mod} = State) -> - State1 = State#{stream_authenticated := true, + State1 = State#{stream_authenticated => true, stream_state => established, stream_timeout => infinity}, try Mod:handle_stream_established(State1) @@ -1117,17 +1117,17 @@ format_inet_error(Reason) -> Txt -> Txt end. --spec format_stream_error(atom() | 'see-other-host'(), undefined | text()) -> string(). +-spec format_stream_error(atom() | 'see-other-host'(), [text()]) -> string(). format_stream_error(Reason, Txt) -> Slogan = case Reason of undefined -> "no reason"; #'see-other-host'{} -> "see-other-host"; _ -> atom_to_list(Reason) end, - case Txt of - undefined -> Slogan; - #text{data = <<"">>} -> Slogan; - #text{data = Data} -> + case xmpp:get_text(Txt) of + <<"">> -> + Slogan; + Data -> binary_to_list(Data) ++ " (" ++ Slogan ++ ")" end. diff --git a/src/xmpp_stream_out.erl b/src/xmpp_stream_out.erl index af5c67c66..7ddc183bf 100644 --- a/src/xmpp_stream_out.erl +++ b/src/xmpp_stream_out.erl @@ -25,6 +25,7 @@ -protocol({rfc, 6120}). -protocol({xep, 114, '1.6'}). +-protocol({xep, 368, '1.0.0'}). %% API -export([start/3, start_link/3, call/3, cast/2, reply/2, connect/1, @@ -48,16 +49,19 @@ -type state() :: map(). -type noreply() :: {noreply, state(), timeout()}. --type host_port() :: {inet:hostname(), inet:port_number()}. --type ip_port() :: {inet:ip_address(), inet:port_number()}. +-type host_port() :: {inet:hostname(), inet:port_number(), boolean()}. +-type ip_port() :: {inet:ip_address(), inet:port_number(), boolean()}. +-type h_addr_list() :: {{integer(), integer(), inet:port_number(), string()}, boolean()}. -type network_error() :: {error, inet:posix() | inet_res:res_error()}. +-type tls_error_reason() :: inet:posix() | atom() | binary(). +-type socket_error_reason() :: inet:posix() | atom(). -type stop_reason() :: {idna, bad_string} | {dns, inet:posix() | inet_res:res_error()} | {stream, reset | {in | out, stream_error()}} | - {tls, inet:posix() | atom() | binary()} | + {tls, tls_error_reason()} | {pkix, binary()} | {auth, atom() | binary() | string()} | - {socket, inet:posix() | atom()} | + {socket, socket_error_reason()} | internal_failure. -export_type([state/0, stop_reason/0]). -callback init(list()) -> {ok, state()} | {error, term()} | ignore. @@ -278,15 +282,16 @@ handle_cast(connect, #{remote_server := RemoteServer, case resolve(binary_to_list(ASCIIName), State) of {ok, AddrPorts} -> case connect(AddrPorts, State) of - {ok, Socket, AddrPort} -> + {ok, Socket, {Addr, Port, Encrypted}} -> SocketMonitor = SockMod:monitor(Socket), - State1 = State#{ip => AddrPort, + State1 = State#{ip => {Addr, Port}, socket => Socket, + stream_encrypted => Encrypted, socket_monitor => SocketMonitor}, State2 = State1#{stream_state => wait_for_stream}, send_header(State2); - {error, Why} -> - process_stream_end({socket, Why}, State) + {error, {Class, Why}} -> + process_stream_end({Class, Why}, State) end; {error, Why} -> process_stream_end({dns, Why}, State) @@ -578,11 +583,8 @@ process_sasl_mechanisms(Mechs, #{user := User, server := Server} = State) -> end. -spec process_starttls(state()) -> state(). -process_starttls(#{sockmod := SockMod, socket := Socket, mod := Mod} = State) -> - TLSOpts = try Mod:tls_options(State) - catch _:undef -> [] - end, - case SockMod:starttls(Socket, [connect|TLSOpts]) of +process_starttls(#{socket := Socket} = State) -> + case starttls(Socket, State) of {ok, TLSSocket} -> State1 = State#{socket => TLSSocket, stream_id => new_id(), @@ -770,6 +772,19 @@ close_socket(State) -> State#{stream_timeout => infinity, stream_state => disconnected}. +-spec starttls(term(), state()) -> {ok, term()} | {error, tls_error_reason()}. +starttls(Socket, #{sockmod := SockMod, mod := Mod, + xmlns := NS, remote_server := RemoteServer} = State) -> + TLSOpts = try Mod:tls_options(State) + catch _:undef -> [] + end, + SNI = idna_to_ascii(RemoteServer), + ALPN = case NS of + ?NS_SERVER -> <<"xmpp-server">>; + ?NS_CLIENT -> <<"xmpp-client">> + end, + SockMod:starttls(Socket, [connect, {sni, SNI}, {alpn, [ALPN]}|TLSOpts]). + -spec select_lang(binary(), binary()) -> binary(). select_lang(Lang, <<"">>) -> Lang; select_lang(_, Lang) -> Lang. @@ -783,17 +798,17 @@ format_inet_error(Reason) -> Txt -> Txt end. --spec format_stream_error(atom() | 'see-other-host'(), undefined | text()) -> string(). +-spec format_stream_error(atom() | 'see-other-host'(), [text()]) -> string(). format_stream_error(Reason, Txt) -> Slogan = case Reason of undefined -> "no reason"; #'see-other-host'{} -> "see-other-host"; _ -> atom_to_list(Reason) end, - case Txt of - undefined -> Slogan; - #text{data = <<"">>} -> Slogan; - #text{data = Data} -> + case xmpp:get_text(Txt) of + <<"">> -> + Slogan; + Data -> binary_to_list(Data) ++ " (" ++ Slogan ++ ")" end. @@ -846,7 +861,7 @@ resolve(Host, State) -> case srv_lookup(Host, State) of {error, _Reason} -> DefaultPort = get_default_port(State), - a_lookup([{Host, DefaultPort}], State); + a_lookup([{Host, DefaultPort, false}], State); {ok, HostPorts} -> a_lookup(HostPorts, State) end. @@ -867,39 +882,66 @@ srv_lookup(Host, State) -> {error, _} -> Timeout = get_dns_timeout(State), Retries = get_dns_retries(State), - srv_lookup(Host, Timeout, Retries) + case srv_lookup(Host, State, Timeout, Retries) of + {ok, AddrList} -> + h_addr_list_to_host_ports(AddrList); + {error, _} = Err -> + Err + end end end. +srv_lookup(Host, State, Timeout, Retries) -> + TLSAddrs = case is_starttls_available(State) of + true -> + case srv_lookup("_xmpps-server._tcp." ++ Host, + Timeout, Retries) of + {ok, HostEnt} -> + [{A, true} || A <- HostEnt#hostent.h_addr_list]; + {error, _} -> + [] + end; + false -> + [] + end, + case srv_lookup("_xmpp-server._tcp." ++ Host, Timeout, Retries) of + {ok, HostEntry} -> + Addrs = [{A, false} || A <- HostEntry#hostent.h_addr_list], + {ok, TLSAddrs ++ Addrs}; + {error, _} when TLSAddrs /= [] -> + {ok, TLSAddrs}; + {error, _} = Err -> + Err + end. + -spec srv_lookup(string(), timeout(), integer()) -> - {ok, [host_port()]} | network_error(). -srv_lookup(_Host, _Timeout, Retries) when Retries < 1 -> + {ok, inet:hostent()} | network_error(). +srv_lookup(_SRVName, _Timeout, Retries) when Retries < 1 -> {error, timeout}; -srv_lookup(Host, Timeout, Retries) -> - SRVName = "_xmpp-server._tcp." ++ Host, +srv_lookup(SRVName, Timeout, Retries) -> case inet_res:getbyname(SRVName, srv, Timeout) of {ok, HostEntry} -> - host_entry_to_host_ports(HostEntry); + {ok, HostEntry}; {error, timeout} -> - srv_lookup(Host, Timeout, Retries - 1); + srv_lookup(SRVName, Timeout, Retries - 1); {error, _} = Err -> Err end. --spec a_lookup([{inet:hostname(), inet:port_number()}], state()) -> +-spec a_lookup([host_port()], state()) -> {ok, [ip_port()]} | network_error(). a_lookup(HostPorts, State) -> - HostPortFamilies = [{Host, Port, Family} - || {Host, Port} <- HostPorts, + HostPortFamilies = [{Host, Port, TLS, Family} + || {Host, Port, TLS} <- HostPorts, Family <- get_address_families(State)], a_lookup(HostPortFamilies, State, [], {error, nxdomain}). --spec a_lookup([{inet:hostname(), inet:port_number(), inet:address_family()}], +-spec a_lookup([{inet:hostname(), inet:port_number(), boolean(), inet:address_family()}], state(), [ip_port()], network_error()) -> {ok, [ip_port()]} | network_error(). -a_lookup([{Host, Port, Family}|HostPortFamilies], State, Acc, Err) -> +a_lookup([{Host, Port, TLS, Family}|HostPortFamilies], State, Acc, Err) -> Timeout = get_dns_timeout(State), Retries = get_dns_retries(State), - case a_lookup(Host, Port, Family, Timeout, Retries) of + case a_lookup(Host, Port, TLS, Family, Timeout, Retries) of {error, Reason} -> a_lookup(HostPortFamilies, State, Acc, {error, Reason}); {ok, AddrPorts} -> @@ -910,11 +952,11 @@ a_lookup([], _State, [], Err) -> a_lookup([], _State, Acc, _) -> {ok, Acc}. --spec a_lookup(inet:hostname(), inet:port_number(), inet:address_family(), +-spec a_lookup(inet:hostname(), inet:port_number(), boolean(), inet:address_family(), timeout(), integer()) -> {ok, [ip_port()]} | network_error(). -a_lookup(_Host, _Port, _Family, _Timeout, Retries) when Retries < 1 -> +a_lookup(_Host, _Port, _TLS, _Family, _Timeout, Retries) when Retries < 1 -> {error, timeout}; -a_lookup(Host, Port, Family, Timeout, Retries) -> +a_lookup(Host, Port, TLS, Family, Timeout, Retries) -> Start = p1_time_compat:monotonic_time(milli_seconds), case inet:gethostbyname(Host, Family, Timeout) of {error, nxdomain} = Err -> @@ -925,43 +967,43 @@ a_lookup(Host, Port, Family, Timeout, Retries) -> %% it ignores DNS configuration settings (/etc/hosts, etc) End = p1_time_compat:monotonic_time(milli_seconds), if (End - Start) >= Timeout -> - a_lookup(Host, Port, Family, Timeout, Retries - 1); + a_lookup(Host, Port, TLS, Family, Timeout, Retries - 1); true -> Err end; {error, _} = Err -> Err; {ok, HostEntry} -> - host_entry_to_addr_ports(HostEntry, Port) + host_entry_to_addr_ports(HostEntry, Port, TLS) end. --spec host_entry_to_host_ports(inet:hostent()) -> {ok, [host_port()]} | +-spec h_addr_list_to_host_ports(h_addr_list()) -> {ok, [host_port()]} | {error, nxdomain}. -host_entry_to_host_ports(#hostent{h_addr_list = AddrList}) -> +h_addr_list_to_host_ports(AddrList) -> PrioHostPorts = lists:flatmap( - fun({Priority, Weight, Port, Host}) -> + fun({{Priority, Weight, Port, Host}, TLS}) -> N = case Weight of 0 -> 0; _ -> (Weight + 1) * randoms:uniform() end, - [{Priority * 65536 - N, Host, Port}]; + [{Priority * 65536 - N, Host, Port, TLS}]; (_) -> [] end, AddrList), - HostPorts = [{Host, Port} - || {_Priority, Host, Port} <- lists:usort(PrioHostPorts)], + HostPorts = [{Host, Port, TLS} + || {_Priority, Host, Port, TLS} <- lists:usort(PrioHostPorts)], case HostPorts of [] -> {error, nxdomain}; _ -> {ok, HostPorts} end. --spec host_entry_to_addr_ports(inet:hostent(), inet:port_number()) -> +-spec host_entry_to_addr_ports(inet:hostent(), inet:port_number(), boolean()) -> {ok, [ip_port()]} | {error, nxdomain}. -host_entry_to_addr_ports(#hostent{h_addr_list = AddrList}, Port) -> +host_entry_to_addr_ports(#hostent{h_addr_list = AddrList}, Port, TLS) -> AddrPorts = lists:flatmap( fun(Addr) -> try get_addr_type(Addr) of - _ -> [{Addr, Port}] + _ -> [{Addr, Port, TLS}] catch _:_ -> [] end @@ -971,14 +1013,26 @@ host_entry_to_addr_ports(#hostent{h_addr_list = AddrList}, Port) -> _ -> {ok, AddrPorts} end. --spec connect([ip_port()], state()) -> {ok, term(), ip_port()} | network_error(). +-spec connect([ip_port()], state()) -> {ok, term(), ip_port()} | + {error, {socket, socket_error_reason()}} | + {error, {tls, tls_error_reason()}}. connect(AddrPorts, #{sockmod := SockMod} = State) -> Timeout = get_connect_timeout(State), - connect(AddrPorts, SockMod, Timeout, {error, nxdomain}). + case connect(AddrPorts, SockMod, Timeout, {error, nxdomain}) of + {ok, Socket, {Addr, Port, TLS = true}} -> + case starttls(Socket, State) of + {ok, TLSSocket} -> {ok, TLSSocket, {Addr, Port, TLS}}; + {error, Why} -> {error, {tls, Why}} + end; + {ok, Socket, {Addr, Port, TLS = false}} -> + {ok, Socket, {Addr, Port, TLS}}; + {error, Why} -> + {error, {socket, Why}} + end. -spec connect([ip_port()], module(), timeout(), network_error()) -> {ok, term(), ip_port()} | network_error(). -connect([{Addr, Port}|AddrPorts], SockMod, Timeout, _) -> +connect([{Addr, Port, TLS}|AddrPorts], SockMod, Timeout, _) -> Type = get_addr_type(Addr), try SockMod:connect(Addr, Port, [binary, {packet, 0}, @@ -987,7 +1041,7 @@ connect([{Addr, Port}|AddrPorts], SockMod, Timeout, _) -> {active, false}, Type], Timeout) of {ok, Socket} -> - {ok, Socket, {Addr, Port}}; + {ok, Socket, {Addr, Port, TLS}}; Err -> connect(AddrPorts, SockMod, Timeout, Err) catch _:badarg -> |