aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/acme_challenge.erl22
-rw-r--r--src/ejabberd.erl28
-rw-r--r--src/ejabberd_acme.erl3
-rw-r--r--src/ejabberd_app.erl15
-rw-r--r--src/ejabberd_auth.erl9
-rw-r--r--src/ejabberd_auth_mnesia.erl30
-rw-r--r--src/ejabberd_auth_riak.erl13
-rw-r--r--src/ejabberd_auth_sql.erl81
-rw-r--r--src/ejabberd_c2s.erl30
-rw-r--r--src/ejabberd_config.erl7
-rw-r--r--src/ejabberd_http.erl20
-rw-r--r--src/ejabberd_iq.erl176
-rw-r--r--src/ejabberd_local.erl140
-rw-r--r--src/ejabberd_mnesia.erl2
-rw-r--r--src/ejabberd_oauth.erl125
-rw-r--r--src/ejabberd_pkix.erl405
-rw-r--r--src/ejabberd_redis.erl2
-rw-r--r--src/ejabberd_router.erl49
-rw-r--r--src/ejabberd_s2s.erl37
-rw-r--r--src/ejabberd_s2s_in.erl4
-rw-r--r--src/ejabberd_service.erl2
-rw-r--r--src/ejabberd_sip.erl5
-rw-r--r--src/ejabberd_sm.erl15
-rw-r--r--src/ejabberd_sm_sql.erl6
-rw-r--r--src/ejabberd_sql_pt.erl242
-rw-r--r--src/ejabberd_stun.erl3
-rw-r--r--src/ejabberd_sup.erl3
-rw-r--r--src/ejabberd_web_admin.erl320
-rw-r--r--src/ejd2sql.erl15
-rw-r--r--src/gen_iq_handler.erl2
-rw-r--r--src/misc.erl65
-rw-r--r--src/mod_admin_extra.erl21
-rw-r--r--src/mod_admin_update_sql.erl365
-rw-r--r--src/mod_announce.erl20
-rw-r--r--src/mod_announce_sql.erl27
-rw-r--r--src/mod_avatar.erl450
-rw-r--r--src/mod_block_strangers.erl101
-rw-r--r--src/mod_bosh.erl64
-rw-r--r--src/mod_bosh_redis.erl31
-rw-r--r--src/mod_bosh_sql.erl31
-rw-r--r--src/mod_caps.erl51
-rw-r--r--src/mod_carboncopy_sql.erl5
-rw-r--r--src/mod_client_state.erl29
-rw-r--r--src/mod_configure.erl10
-rw-r--r--src/mod_delegation.erl87
-rw-r--r--src/mod_fail2ban.erl6
-rw-r--r--src/mod_http_fileserver.erl53
-rw-r--r--src/mod_http_upload.erl206
-rw-r--r--src/mod_irc.erl2
-rw-r--r--src/mod_irc_connection.erl2
-rw-r--r--src/mod_irc_sql.erl14
-rw-r--r--src/mod_last_sql.erl14
-rw-r--r--src/mod_legacy_auth.erl2
-rw-r--r--src/mod_mam.erl270
-rw-r--r--src/mod_mam_mnesia.erl19
-rw-r--r--src/mod_mam_sql.erl127
-rw-r--r--src/mod_muc.erl46
-rw-r--r--src/mod_muc_admin.erl62
-rw-r--r--src/mod_muc_log.erl369
-rw-r--r--src/mod_muc_mnesia.erl10
-rw-r--r--src/mod_muc_riak.erl10
-rw-r--r--src/mod_muc_room.erl245
-rw-r--r--src/mod_muc_sql.erl132
-rw-r--r--src/mod_multicast.erl2
-rw-r--r--src/mod_offline.erl27
-rw-r--r--src/mod_offline_sql.erl39
-rw-r--r--src/mod_ping.erl15
-rw-r--r--src/mod_privacy.erl54
-rw-r--r--src/mod_privacy_sql.erl83
-rw-r--r--src/mod_private.erl2
-rw-r--r--src/mod_private_sql.erl20
-rw-r--r--src/mod_privilege.erl2
-rw-r--r--src/mod_proxy65_service.erl8
-rw-r--r--src/mod_pubsub.erl118
-rw-r--r--src/mod_push.erl135
-rw-r--r--src/mod_push_keepalive.erl2
-rw-r--r--src/mod_push_mnesia.erl79
-rw-r--r--src/mod_push_sql.erl240
-rw-r--r--src/mod_register.erl21
-rw-r--r--src/mod_register_web.erl41
-rw-r--r--src/mod_roster.erl10
-rw-r--r--src/mod_roster_sql.erl115
-rw-r--r--src/mod_s2s_dialback.erl10
-rw-r--r--src/mod_shared_roster_sql.erl53
-rw-r--r--src/mod_sip.erl3
-rw-r--r--src/mod_sip_proxy.erl3
-rw-r--r--src/mod_sip_registrar.erl3
-rw-r--r--src/mod_stream_mgmt.erl8
-rw-r--r--src/mod_vcard.erl48
-rw-r--r--src/mod_vcard_ldap.erl47
-rw-r--r--src/mod_vcard_mnesia.erl49
-rw-r--r--src/mod_vcard_sql.erl140
-rw-r--r--src/mod_vcard_xupdate.erl74
-rw-r--r--src/node_flat.erl136
-rw-r--r--src/node_flat_sql.erl256
-rw-r--r--src/nodetree_tree.erl23
-rw-r--r--src/nodetree_tree_sql.erl29
-rw-r--r--src/pubsub_db_sql.erl145
-rw-r--r--src/xmpp_stream_in.erl12
-rw-r--r--src/xmpp_stream_out.erl154
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 ->