aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--include/ejabberd_commands.hrl25
-rw-r--r--include/ejabberd_oauth.hrl26
-rw-r--r--include/ejabberd_sm.hrl4
-rw-r--r--include/mod_muc_room.hrl3
-rw-r--r--include/ns.hrl8
-rw-r--r--lib/ct_formatter.ex2
-rw-r--r--mix.exs9
-rw-r--r--mix.lock23
-rw-r--r--rebar.config14
-rw-r--r--rebar.config.script31
-rw-r--r--src/acl.erl12
-rw-r--r--src/ejabberd_admin.erl6
-rw-r--r--src/ejabberd_c2s.erl24
-rw-r--r--src/ejabberd_commands.erl304
-rw-r--r--src/ejabberd_config.erl3
-rw-r--r--src/ejabberd_http.erl3
-rw-r--r--src/ejabberd_oauth.erl321
-rw-r--r--src/ejabberd_oauth_mnesia.erl65
-rw-r--r--src/ejabberd_service.erl10
-rw-r--r--src/ejabberd_sm.erl42
-rw-r--r--src/ejabberd_sql.erl2
-rw-r--r--src/ejabberd_web_admin.erl40
-rw-r--r--src/ejd2sql.erl2
-rw-r--r--src/gen_mod.erl75
-rw-r--r--src/jid.erl26
-rw-r--r--src/jlib.erl29
-rw-r--r--src/mod_adhoc.erl5
-rw-r--r--src/mod_admin_extra.erl23
-rw-r--r--src/mod_announce.erl5
-rw-r--r--src/mod_blocking.erl5
-rw-r--r--src/mod_caps.erl5
-rw-r--r--src/mod_carboncopy.erl5
-rw-r--r--src/mod_client_state.erl7
-rw-r--r--src/mod_configure.erl42
-rw-r--r--src/mod_configure2.erl5
-rw-r--r--src/mod_disco.erl5
-rw-r--r--src/mod_echo.erl5
-rw-r--r--src/mod_fail2ban.erl5
-rw-r--r--src/mod_http_api.erl111
-rw-r--r--src/mod_http_bind.erl4
-rw-r--r--src/mod_http_fileserver.erl5
-rw-r--r--src/mod_http_upload.erl6
-rw-r--r--src/mod_http_upload_quota.erl12
-rw-r--r--src/mod_ip_blacklist.erl5
-rw-r--r--src/mod_irc.erl5
-rw-r--r--src/mod_last.erl5
-rw-r--r--src/mod_mam.erl56
-rw-r--r--src/mod_mam_sql.erl22
-rw-r--r--src/mod_metrics.erl6
-rw-r--r--src/mod_mix.erl5
-rw-r--r--src/mod_muc.erl43
-rw-r--r--src/mod_muc_admin.erl5
-rw-r--r--src/mod_muc_log.erl5
-rw-r--r--src/mod_muc_room.erl1157
-rw-r--r--src/mod_multicast.erl5
-rw-r--r--src/mod_offline.erl6
-rw-r--r--src/mod_ping.erl5
-rw-r--r--src/mod_pres_counter.erl5
-rw-r--r--src/mod_privacy.erl5
-rw-r--r--src/mod_private.erl5
-rw-r--r--src/mod_proxy65.erl5
-rw-r--r--src/mod_pubsub.erl17
-rw-r--r--src/mod_register.erl5
-rw-r--r--src/mod_register_web.erl5
-rw-r--r--src/mod_roster.erl5
-rw-r--r--src/mod_roster_sql.erl25
-rw-r--r--src/mod_service_log.erl5
-rw-r--r--src/mod_shared_roster.erl5
-rw-r--r--src/mod_shared_roster_ldap.erl5
-rw-r--r--src/mod_sic.erl5
-rw-r--r--src/mod_sip.erl5
-rw-r--r--src/mod_stats.erl5
-rw-r--r--src/mod_time.erl5
-rw-r--r--src/mod_vcard.erl5
-rw-r--r--src/mod_vcard_ldap.erl5
-rw-r--r--src/mod_vcard_xupdate.erl5
-rw-r--r--src/mod_version.erl5
-rw-r--r--src/node_flat_sql.erl31
-rw-r--r--src/node_pep.erl24
-rw-r--r--src/node_pep_sql.erl24
-rw-r--r--src/nodetree_tree_sql.erl36
-rw-r--r--test/acl_test.exs1
-rw-r--r--test/ejabberd_commands_mock_test.exs52
-rw-r--r--test/ejabberd_commands_test.exs22
-rw-r--r--test/ejabberd_cyrsasl_test.exs10
-rw-r--r--test/ejabberd_oauth_mock.exs2
-rw-r--r--test/mod_admin_extra_test.exs3
-rw-r--r--test/mod_http_api_mock_test.exs96
-rw-r--r--test/mod_http_api_test.exs28
-rw-r--r--test/test_helper.exs7
90 files changed, 2120 insertions, 1042 deletions
diff --git a/include/ejabberd_commands.hrl b/include/ejabberd_commands.hrl
index 81be06dc3..c5c34b743 100644
--- a/include/ejabberd_commands.hrl
+++ b/include/ejabberd_commands.hrl
@@ -26,6 +26,25 @@
{tuple, [rterm()]} | {list, rterm()} |
rescode | restuple.
+-type oauth_scope() :: atom().
+
+%% ejabberd_commands OAuth ReST ACL definition:
+%% Two fields exist that are used to control access on a command from ReST API:
+%% 1. Policy
+%% If policy is:
+%% - restricted: command is not exposed as OAuth Rest API.
+%% - admin: Command is allowed for user that have Admin Rest command enabled by access rule: commands_admin_access
+%% - user: Command might be called by any server user.
+%% - open: Command can be called by anyone.
+%%
+%% Policy is just used to control who can call the command. A specific additional access rules can be performed, as
+%% defined by access option.
+%% Access option can be a list of:
+%% - {Module, accessName, DefaultValue}: Reference and existing module access to limit who can use the command.
+%% - AccessRule name: direct name of the access rule to check in config file.
+%% TODO: Access option could be atom command (not a list). In the case, User performing the command, will be added as first parameter
+%% to command, so that the command can perform additional check.
+
-record(ejabberd_commands,
{name :: atom(),
tags = [] :: [atom()] | '_' | '$2',
@@ -36,19 +55,25 @@
function :: atom() | '_',
args = [] :: [aterm()] | '_' | '$1' | '$2',
policy = restricted :: open | restricted | admin | user,
+ %% access is: [accessRuleName] or [{Module, AccessOption, DefaultAccessRuleName}]
+ access = [] :: [{atom(),atom(),atom()}|atom()],
result = {res, rescode} :: rterm() | '_' | '$2',
args_desc = none :: none | [string()] | '_',
result_desc = none :: none | string() | '_',
args_example = none :: none | [any()] | '_',
result_example = none :: any()}).
+%% TODO Fix me: Type is not up to date
-type ejabberd_commands() :: #ejabberd_commands{name :: atom(),
tags :: [atom()],
desc :: string(),
longdesc :: string(),
+ version :: integer(),
module :: atom(),
function :: atom(),
args :: [aterm()],
+ policy :: open | restricted | admin | user,
+ access :: [{atom(),atom(),atom()}|atom()],
result :: rterm()}.
%% @type ejabberd_commands() = #ejabberd_commands{
diff --git a/include/ejabberd_oauth.hrl b/include/ejabberd_oauth.hrl
new file mode 100644
index 000000000..6b5a9bcc8
--- /dev/null
+++ b/include/ejabberd_oauth.hrl
@@ -0,0 +1,26 @@
+%%%----------------------------------------------------------------------
+%%%
+%%% ejabberd, Copyright (C) 2002-2016 ProcessOne
+%%%
+%%% This program is free software; you can redistribute it and/or
+%%% modify it under the terms of the GNU General Public License as
+%%% published by the Free Software Foundation; either version 2 of the
+%%% License, or (at your option) any later version.
+%%%
+%%% This program is distributed in the hope that it will be useful,
+%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
+%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%%% General Public License for more details.
+%%%
+%%% You should have received a copy of the GNU General Public License along
+%%% with this program; if not, write to the Free Software Foundation, Inc.,
+%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+%%%
+%%%----------------------------------------------------------------------
+
+-record(oauth_token, {
+ token = <<"">> :: binary() | '_',
+ us = {<<"">>, <<"">>} :: {binary(), binary()} | '_',
+ scope = [] :: [binary()] | '_',
+ expire :: integer() | '$1'
+ }).
diff --git a/include/ejabberd_sm.hrl b/include/ejabberd_sm.hrl
index 38298d66a..f86ab1c15 100644
--- a/include/ejabberd_sm.hrl
+++ b/include/ejabberd_sm.hrl
@@ -1,9 +1,9 @@
-ifndef(EJABBERD_SM_HRL).
-define(EJABBERD_SM_HRL, true).
--record(session, {sid, usr, us, priority, info}).
+-record(session, {sid, usr, us, priority, info = []}).
-record(session_counter, {vhost, count}).
--type sid() :: {erlang:timestamp(), pid()} | {erlang:timestamp(), undefined}.
+-type sid() :: {erlang:timestamp(), pid()}.
-type ip() :: {inet:ip_address(), inet:port_number()} | undefined.
-type info() :: [{conn, atom()} | {ip, ip()} | {node, atom()}
| {oor, boolean()} | {auth_module, atom()}
diff --git a/include/mod_muc_room.hrl b/include/mod_muc_room.hrl
index 4d82856ca..d985f3f3b 100644
--- a/include/mod_muc_room.hrl
+++ b/include/mod_muc_room.hrl
@@ -53,6 +53,7 @@
members_by_default = true :: boolean(),
members_only = false :: boolean(),
allow_user_invites = false :: boolean(),
+ allow_subscription = false :: boolean(),
password_protected = false :: boolean(),
password = <<"">> :: binary(),
anonymous = true :: boolean(),
@@ -76,6 +77,8 @@
jid :: jid(),
nick :: binary(),
role :: role(),
+ is_subscriber = false :: boolean(),
+ subscriptions = [] :: [binary()],
last_presence :: xmlel()
}).
diff --git a/include/ns.hrl b/include/ns.hrl
index c7f556372..a150746e7 100644
--- a/include/ns.hrl
+++ b/include/ns.hrl
@@ -164,3 +164,11 @@
-define(NS_MIX_NODES_PARTICIPANTS, <<"urn:xmpp:mix:nodes:participants">>).
-define(NS_MIX_NODES_SUBJECT, <<"urn:xmpp:mix:nodes:subject">>).
-define(NS_MIX_NODES_CONFIG, <<"urn:xmpp:mix:nodes:config">>).
+-define(NS_MUCSUB, <<"urn:xmpp:mucsub:0">>).
+-define(NS_MUCSUB_NODES_PRESENCE, <<"urn:xmpp:mucsub:nodes:presence">>).
+-define(NS_MUCSUB_NODES_MESSAGES, <<"urn:xmpp:mucsub:nodes:messages">>).
+-define(NS_MUCSUB_NODES_PARTICIPANTS, <<"urn:xmpp:mucsub:nodes:participants">>).
+-define(NS_MUCSUB_NODES_AFFILIATIONS, <<"urn:xmpp:mucsub:nodes:affiliations">>).
+-define(NS_MUCSUB_NODES_SUBJECT, <<"urn:xmpp:mucsub:nodes:subject">>).
+-define(NS_MUCSUB_NODES_CONFIG, <<"urn:xmpp:mucsub:nodes:config">>).
+-define(NS_MUCSUB_NODES_SYSTEM, <<"urn:xmpp:mucsub:nodes:system">>).
diff --git a/lib/ct_formatter.ex b/lib/ct_formatter.ex
index 47c487ac4..0c301353b 100644
--- a/lib/ct_formatter.ex
+++ b/lib/ct_formatter.ex
@@ -3,7 +3,7 @@ defmodule ExUnit.CTFormatter do
use GenEvent
- import ExUnit.Formatter, only: [format_time: 2, format_filters: 2, format_test_failure: 5,
+ import ExUnit.Formatter, only: [format_time: 2, format_test_failure: 5,
format_test_case_failure: 5]
def init(opts) do
diff --git a/mix.exs b/mix.exs
index 0806e1210..7453ea473 100644
--- a/mix.exs
+++ b/mix.exs
@@ -11,6 +11,8 @@ defmodule Ejabberd.Mixfile do
compilers: [:asn1] ++ Mix.compilers,
erlc_options: erlc_options,
erlc_paths: ["asn1", "src"],
+ # Elixir tests are starting the part of ejabberd they need
+ aliases: [test: "test --no-start"],
package: package,
deps: deps]
end
@@ -56,7 +58,12 @@ defmodule Ejabberd.Mixfile do
{:ezlib, "~> 1.0"},
{:iconv, "~> 1.0"},
{:eredis, "~> 1.0"},
- {:exrm, "~> 1.0.0-rc7", only: :dev}]
+ {:exrm, "~> 1.0.0", only: :dev},
+ # relx is used by exrm. Lock version as for now, ejabberd doesn not compile fine with
+ # version 3.20:
+ {:relx, "~> 3.19.0", only: :dev},
+ {:meck, "~> 0.8.4", only: :test},
+ {:moka, github: "processone/moka", tag: "1.0.5b", only: :test}]
end
defp package do
diff --git a/mix.lock b/mix.lock
index d576c518f..467f142ba 100644
--- a/mix.lock
+++ b/mix.lock
@@ -2,25 +2,28 @@
"cache_tab": {:hex, :cache_tab, "1.0.3", "0e3c40dde2fe2a6a4db241d7583cea0cc1bcf29e546a0a22f15b75366b2f336e", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]},
"cf": {:hex, :cf, "0.2.1", "69d0b1349fd4d7d4dc55b7f407d29d7a840bf9a1ef5af529f1ebe0ce153fc2ab", [:rebar3], []},
"eredis": {:hex, :eredis, "1.0.8", "ab4fda1c4ba7fbe6c19c26c249dc13da916d762502c4b4fa2df401a8d51c5364", [:rebar], []},
- "erlware_commons": {:hex, :erlware_commons, "0.21.0", "a04433071ad7d112edefc75ac77719dd3e6753e697ac09428fc83d7564b80b15", [:rebar3], [{:cf, "0.2.1", [hex: :cf, optional: false]}]},
- "esip": {:hex, :esip, "1.0.6", "cb1ced88fae4c4a4888d9023c2c13b2239e14f8e360aee134c964b4a36dcc34d", [:rebar3], [{:stun, "1.0.5", [hex: :stun, optional: false]}, {:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}, {:fast_tls, "1.0.5", [hex: :fast_tls, optional: false]}]},
- "exrm": {:hex, :exrm, "1.0.6", "f708fc091dcacb93c1da58254a1ab34166d5ac3dca162877e878fe5d7a9e9dce", [:mix], [{:relx, "~> 3.5", [hex: :relx, optional: false]}]},
+ "erlware_commons": {:hex, :erlware_commons, "0.19.0", "7b43caf2c91950c5f60dc20451e3c3afba44d3d4f7f27bcdc52469285a5a3e70", [:rebar3], [{:cf, "0.2.1", [hex: :cf, optional: false]}]},
+ "esip": {:hex, :esip, "1.0.7", "f75f6a5cac6814e506f0ff96141fbe276dee3261fca1471c8edfdde25b74f877", [:rebar3], [{:fast_tls, "1.0.6", [hex: :fast_tls, optional: false]}, {:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}, {:stun, "1.0.6", [hex: :stun, optional: false]}]},
+ "exrm": {:hex, :exrm, "1.0.8", "5aa8990cdfe300282828b02cefdc339e235f7916388ce99f9a1f926a9271a45d", [:mix], [{:relx, "~> 3.5", [hex: :relx, optional: false]}]},
"ezlib": {:hex, :ezlib, "1.0.1", "add8b2770a1a70c174aaea082b4a8668c0c7fdb03ee6cc81c6c68d3a6c3d767d", [:rebar3], []},
- "fast_tls": {:hex, :fast_tls, "1.0.5", "8b970a91d4131fe5b9d47ffaccc2466944293c88dc5cc75a25548d73d57f7b77", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]},
- "fast_xml": {:hex, :fast_xml, "1.1.13", "85eca0a003598dbb0644320bd9bdc5fef30ad6285ab2aa80e2b5b82e65b79aa8", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]},
- "fast_yaml": {:hex, :fast_yaml, "1.0.4", "075ffb55f6ff3aa2f0461b8bfd1218e2f91e632c1675fc535963b9de7834800e", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]},
+ "fast_tls": {:hex, :fast_tls, "1.0.6", "750a74aabb05056f0f222910f0955883649e6c5d67df6ca504ff676160d22b89", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]},
+ "fast_xml": {:hex, :fast_xml, "1.1.14", "23d4de66e645bca1d8a557444e83062cc42f235d7a7e2d4072d525bac3986f04", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]},
+ "fast_yaml": {:hex, :fast_yaml, "1.0.5", "a67772c75abb84181c6c9899e1f988b08ac214ea0d764ff1f9889bb7e27f74d4", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]},
"getopt": {:hex, :getopt, "0.8.2", "b17556db683000ba50370b16c0619df1337e7af7ecbf7d64fbf8d1d6bce3109b", [:rebar], []},
"goldrush": {:hex, :goldrush, "0.1.7", "349a351d17c71c2fdaa18a6c2697562abe136fec945f147b381f0cf313160228", [:rebar3], []},
- "iconv": {:hex, :iconv, "1.0.0", "5ff1c54e5b3b9a8235de872632e9612c7952acdf89bc21db2f2efae0e72647be", [:rebar3], []},
+ "iconv": {:hex, :iconv, "1.0.1", "dbb8700070577e7a021a095cc5ead221069a0c4034bfadca2516c1f1109ee7fd", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]},
"jiffy": {:hex, :jiffy, "0.14.7", "9f33b893edd6041ceae03bc1e50b412e858cc80b46f3d7535a7a9940a79a1c37", [:rebar, :make], []},
"lager": {:hex, :lager, "3.0.2", "25dc81bc3659b62f5ab9bd073e97ddd894fc4c242019fccef96f3889d7366c97", [:rebar3], [{:goldrush, "0.1.7", [hex: :goldrush, optional: false]}]},
+ "meck": {:hex, :meck, "0.8.4", "59ca1cd971372aa223138efcf9b29475bde299e1953046a0c727184790ab1520", [:rebar, :make], []},
+ "moka": {:git, "https://github.com/processone/moka.git", "768efea96443c57125e6247dbebee687f17be149", [tag: "1.0.5b"]},
"p1_mysql": {:hex, :p1_mysql, "1.0.1", "d2be1cfc71bb4f1391090b62b74c3f5cb8e7a45b0076b8cb290cd6b2856c581b", [:rebar3], []},
"p1_oauth2": {:hex, :p1_oauth2, "0.6.1", "4e021250cc198c538b097393671a41e7cebf463c248980320e038fe0316eb56b", [:rebar3], []},
"p1_pgsql": {:hex, :p1_pgsql, "1.1.0", "ca525c42878eac095e5feb19563acc9915c845648f48fdec7ba6266c625d4ac7", [:rebar3], []},
"p1_utils": {:hex, :p1_utils, "1.0.4", "7face65db102b5d1ebe7ad3c7517c5ee8cfbe174c6658e3affbb00eb66e06787", [:rebar3], []},
"p1_xmlrpc": {:hex, :p1_xmlrpc, "1.15.1", "a382b62dc21bb372281c2488f99294d84f2b4020ed0908a1c4ad710ace3cf35a", [:rebar3], []},
"providers": {:hex, :providers, "1.6.0", "db0e2f9043ae60c0155205fcd238d68516331d0e5146155e33d1e79dc452964a", [:rebar3], [{:getopt, "0.8.2", [hex: :getopt, optional: false]}]},
- "relx": {:hex, :relx, "3.20.0", "b515b8317d25b3a1508699294c3d1fa6dc0527851dffc87446661bce21a36710", [:rebar3], [{:providers, "1.6.0", [hex: :providers, optional: false]}, {:getopt, "0.8.2", [hex: :getopt, optional: false]}, {:erlware_commons, "0.21.0", [hex: :erlware_commons, optional: false]}, {:cf, "0.2.1", [hex: :cf, optional: false]}, {:bbmustache, "1.0.4", [hex: :bbmustache, optional: false]}]},
+ "relx": {:hex, :relx, "3.19.0", "286dd5244b4786f56aac75d5c8e2d1fb4cfd306810d4ec8548f3ae1b3aadb8f7", [:rebar3], [{:bbmustache, "1.0.4", [hex: :bbmustache, optional: false]}, {:cf, "0.2.1", [hex: :cf, optional: false]}, {:erlware_commons, "0.19.0", [hex: :erlware_commons, optional: false]}, {:getopt, "0.8.2", [hex: :getopt, optional: false]}, {:providers, "1.6.0", [hex: :providers, optional: false]}]},
+ "samerlib": {:git, "https://github.com/processone/samerlib", "9158f65d18ec63f8b409543b6fb46dd5fce46160", [tag: "0.8.0b"]},
"sqlite3": {:hex, :sqlite3, "1.1.5", "794738b6d07b6d36ec6d42492cb9d629bad9cf3761617b8b8d728e765db19840", [:rebar3], []},
- "stringprep": {:hex, :stringprep, "1.0.4", "f8f94d838ed202787699ff71d67b65481d350bda32b232ba1db52faca8eaed39", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]},
- "stun": {:hex, :stun, "1.0.5", "ec1d9928f25451d6fd2d2ade58c46b58b8d2c8326ddea3a667e926d04792f82c", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}, {:fast_tls, "1.0.5", [hex: :fast_tls, optional: false]}]}}
+ "stringprep": {:hex, :stringprep, "1.0.5", "f29395275c35af5051b29bf875b44ac632dc4d0287880f0e143b536c61fd0ed5", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]},
+ "stun": {:hex, :stun, "1.0.6", "1ca9dea574e09f60971bd8de9cb7e34f327cbf435462cf56aa30f05c1ee2f231", [:rebar3], [{:fast_tls, "1.0.6", [hex: :fast_tls, optional: false]}, {:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]}}
diff --git a/rebar.config b/rebar.config
index a07498be6..693f4f58b 100644
--- a/rebar.config
+++ b/rebar.config
@@ -10,12 +10,12 @@
{deps, [{lager, ".*", {git, "https://github.com/basho/lager", {tag, "3.2.1"}}},
{p1_utils, ".*", {git, "https://github.com/processone/p1_utils", {tag, "1.0.5"}}},
{cache_tab, ".*", {git, "https://github.com/processone/cache_tab", {tag, "1.0.3"}}},
- {fast_tls, ".*", {git, "https://github.com/processone/fast_tls", {tag, "1.0.5"}}},
- {stringprep, ".*", {git, "https://github.com/processone/stringprep", {tag, "1.0.4"}}},
+ {fast_tls, ".*", {git, "https://github.com/processone/fast_tls", {tag, "1.0.6"}}},
+ {stringprep, ".*", {git, "https://github.com/processone/stringprep", {tag, "1.0.5"}}},
{fast_xml, ".*", {git, "https://github.com/processone/fast_xml", {tag, "1.1.14"}}},
- {stun, ".*", {git, "https://github.com/processone/stun", {tag, "1.0.5"}}},
- {esip, ".*", {git, "https://github.com/processone/esip", {tag, "1.0.6"}}},
- {fast_yaml, ".*", {git, "https://github.com/processone/fast_yaml", {tag, "1.0.4"}}},
+ {stun, ".*", {git, "https://github.com/processone/stun", {tag, "1.0.6"}}},
+ {esip, ".*", {git, "https://github.com/processone/esip", {tag, "1.0.7"}}},
+ {fast_yaml, ".*", {git, "https://github.com/processone/fast_yaml", {tag, "1.0.5"}}},
{jiffy, ".*", {git, "https://github.com/davisp/jiffy", {tag, "0.14.7"}}},
{p1_oauth2, ".*", {git, "https://github.com/processone/p1_oauth2", {tag, "0.6.1"}}},
{p1_xmlrpc, ".*", {git, "https://github.com/processone/p1_xmlrpc", {tag, "1.15.1"}}},
@@ -39,12 +39,12 @@
"6e7fc924506e2dc166a6170e580ce1d95ebbd5bd"}}}, % for riak_pb-2.1.0.7 with correct meck dependency
%% Elixir support, needed to run tests
{if_var_true, elixir, {elixir, ".*", {git, "https://github.com/elixir-lang/elixir",
- {tag, "v1.1.1"}}}},
+ {tag, {if_version_above, "17", "v1.2.6", "v1.1.1"}}}}},
%% TODO: When modules are fully migrated to new structure and mix, we will not need anymore rebar_elixir_plugin
{if_var_true, elixir, {rebar_elixir_plugin, ".*",
{git, "https://github.com/processone/rebar_elixir_plugin", "0.1.0"}}},
{if_var_true, iconv, {iconv, ".*", {git, "https://github.com/processone/iconv",
- {tag, "1.0.0"}}}},
+ {tag, "1.0.1"}}}},
{if_var_true, tools, {meck, "0.8.*", {git, "https://github.com/eproxus/meck",
{tag, "0.8.4"}}}},
{if_var_true, tools, {moka, ".*", {git, "https://github.com/processone/moka.git",
diff --git a/rebar.config.script b/rebar.config.script
index 166f1cbec..ccafba7ec 100644
--- a/rebar.config.script
+++ b/rebar.config.script
@@ -19,7 +19,7 @@ ModCfg0 = fun(F, Cfg, [Key|Tail], Op, Default) ->
[{Key, F(F, OldVal, Tail, Op, Default)} | PartCfg]
end
end,
-ModCfg = fun(Cfg, Keys, Op, Default) -> ModCfg0(ModCfg0, Cfg, Keys, Op, Default) end.
+ModCfg = fun(Cfg, Keys, Op, Default) -> ModCfg0(ModCfg0, Cfg, Keys, Op, Default) end,
Cfg = case file:consult(filename:join(filename:dirname(SCRIPT), "vars.config")) of
{ok, Terms} ->
@@ -28,6 +28,13 @@ Cfg = case file:consult(filename:join(filename:dirname(SCRIPT), "vars.config"))
[]
end,
+ProcessSingleVar = fun(F, Var, Tail) ->
+ case F(F, [Var], []) of
+ [] -> Tail;
+ [Val] -> [Val | Tail]
+ end
+ end,
+
ProcessVars = fun(_F, [], Acc) ->
lists:reverse(Acc);
(F, [{Type, Ver, Value} | Tail], Acc) when
@@ -40,17 +47,31 @@ ProcessVars = fun(_F, [], Acc) ->
SysVer < Ver
end,
if Include ->
- F(F, Tail, [Value | Acc]);
+ F(F, Tail, ProcessSingleVar(F, Value, Acc));
true ->
F(F, Tail, Acc)
end;
+ (F, [{Type, Ver, Value, ElseValue} | Tail], Acc) when
+ Type == if_version_above orelse
+ Type == if_version_below ->
+ SysVer = erlang:system_info(otp_release),
+ Include = if Type == if_version_above ->
+ SysVer > Ver;
+ true ->
+ SysVer < Ver
+ end,
+ if Include ->
+ F(F, Tail, ProcessSingleVar(F, Value, Acc));
+ true ->
+ F(F, Tail, ProcessSingleVar(F, ElseValue, Acc))
+ end;
(F, [{Type, Var, Value} | Tail], Acc) when
Type == if_var_true orelse
Type == if_var_false ->
Flag = Type == if_var_true,
case proplists:get_bool(Var, Cfg) of
V when V == Flag ->
- F(F, Tail, [Value | Acc]);
+ F(F, Tail, ProcessSingleVar(F, Value, Acc));
_ ->
F(F, Tail, Acc)
end;
@@ -59,7 +80,7 @@ ProcessVars = fun(_F, [], Acc) ->
Type == if_var_no_match ->
case proplists:get_value(Var, Cfg) of
V when V == Match ->
- F(F, Tail, [Value | Acc]);
+ F(F, Tail, ProcessSingleVar(F, Value, Acc));
_ ->
F(F, Tail, Acc)
end;
@@ -146,7 +167,7 @@ Conf6 = case {lists:keyfind(cover_enabled, 1, Conf5), os:getenv("TRAVIS")} of
Conf5
end,
-%io:format("ejabberd configuration:~n ~p~n", [Conf5]),
+%io:format("ejabberd configuration:~n ~p~n", [Conf6]),
Conf6.
diff --git a/src/acl.erl b/src/acl.erl
index 31a7547dd..897996976 100644
--- a/src/acl.erl
+++ b/src/acl.erl
@@ -31,10 +31,11 @@
-export([add_access/3, clear/0]).
-export([start/0, add/3, add_list/3, add_local/3, add_list_local/3,
- load_from_config/0, match_rule/3,
+ load_from_config/0, match_rule/3, any_rules_allowed/3,
transform_options/1, opt_type/1, acl_rule_matches/3,
acl_rule_verify/1, access_matches/3,
transform_access_rules_config/1,
+ parse_ip_netmask/1,
access_rules_validator/1, shaper_rules_validator/1]).
-include("ejabberd.hrl").
@@ -274,6 +275,15 @@ normalize_spec(Spec) ->
end
end.
+-spec any_rules_allowed(global | binary(), access_name(),
+ jid() | ljid() | inet:ip_address()) -> boolean().
+
+any_rules_allowed(Host, Access, Entity) ->
+ lists:any(fun (Rule) ->
+ allow == acl:match_rule(Host, Rule, Entity)
+ end,
+ Access).
+
-spec match_rule(global | binary(), access_name(),
jid() | ljid() | inet:ip_address()) -> any().
diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl
index 87ac76875..f20aeebf0 100644
--- a/src/ejabberd_admin.erl
+++ b/src/ejabberd_admin.erl
@@ -129,6 +129,8 @@ get_commands_spec() ->
#ejabberd_commands{name = register, tags = [accounts],
desc = "Register a user",
+ policy = admin,
+ access = [{mod_register, access, configure}],
module = ?MODULE, function = register,
args = [{user, binary}, {host, binary}, {password, binary}],
result = {res, restuple}},
@@ -166,7 +168,7 @@ get_commands_spec() ->
#ejabberd_commands{name = list_cluster, tags = [cluster],
desc = "List nodes that are part of the cluster handled by Node",
module = ?MODULE, function = list_cluster,
- args = [],
+ args = [],
result = {nodes, {list, {node, atom}}}},
#ejabberd_commands{name = import_file, tags = [mnesia],
@@ -220,7 +222,7 @@ get_commands_spec() ->
desc = "Delete offline messages older than DAYS",
module = ?MODULE, function = delete_old_messages,
args = [{days, integer}], result = {res, rescode}},
-
+
#ejabberd_commands{name = export2sql, tags = [mnesia],
desc = "Export virtual host information from Mnesia tables to SQL files",
module = ejd2sql, function = export,
diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl
index f431f6890..0ffca7179 100644
--- a/src/ejabberd_c2s.erl
+++ b/src/ejabberd_c2s.erl
@@ -1632,11 +1632,18 @@ handle_info({route, From, To,
<<"groupchat">> -> ok;
<<"headline">> -> ok;
_ ->
- Err =
- jlib:make_error_reply(Packet,
- ?ERR_SERVICE_UNAVAILABLE),
- ejabberd_router:route(To, From,
- Err)
+ case fxml:get_subtag_with_xmlns(Packet,
+ <<"x">>,
+ ?NS_MUC_USER)
+ of
+ false ->
+ Err =
+ jlib:make_error_reply(Packet,
+ ?ERR_SERVICE_UNAVAILABLE),
+ ejabberd_router:route(To, From,
+ Err);
+ _ -> ok
+ end
end,
{false, Attrs, StateData}
end;
@@ -2873,8 +2880,8 @@ handle_unacked_stanzas(#state{mgmt_state = MgmtState} = StateData, F)
0 ->
ok;
N ->
- ?INFO_MSG("~B stanzas were not acknowledged by ~s",
- [N, jid:to_string(StateData#state.jid)]),
+ ?DEBUG("~B stanza(s) were not acknowledged by ~s",
+ [N, jid:to_string(StateData#state.jid)]),
lists:foreach(
fun({_, Time, #xmlel{attrs = Attrs} = El}) ->
From_s = fxml:get_attr_s(<<"from">>, Attrs),
@@ -2951,6 +2958,9 @@ handle_unacked_stanzas(#state{mgmt_state = MgmtState} = StateData)
[StateData, From,
StateData#state.jid, El]) of
true ->
+ ?DEBUG("Dropping archived message stanza from ~s",
+ [fxml:get_attr_s(<<"from">>,
+ El#xmlel.attrs)]),
ok;
false ->
ReRoute(From, To, El, Time)
diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl
index 9d41f50c2..33edcb7c7 100644
--- a/src/ejabberd_commands.erl
+++ b/src/ejabberd_commands.erl
@@ -218,14 +218,15 @@
get_command_format/1,
get_command_format/2,
get_command_format/3,
- get_command_policy/1,
+ get_command_policy_and_scope/1,
get_command_definition/1,
get_command_definition/2,
get_tags_commands/0,
get_tags_commands/1,
- get_commands/0,
+ get_exposed_commands/0,
register_commands/1,
- unregister_commands/1,
+ unregister_commands/1,
+ expose_commands/1,
execute_command/2,
execute_command/3,
execute_command/4,
@@ -275,10 +276,10 @@ get_commands_spec() ->
init() ->
mnesia:delete_table(ejabberd_commands),
mnesia:create_table(ejabberd_commands,
- [{ram_copies, [node()]},
+ [{ram_copies, [node()]},
{local_content, true},
- {attributes, record_info(fields, ejabberd_commands)},
- {type, bag}]),
+ {attributes, record_info(fields, ejabberd_commands)},
+ {type, bag}]),
mnesia:add_table_copy(ejabberd_commands, node(), ram_copies),
register_commands(get_commands_spec()).
@@ -287,12 +288,14 @@ init() ->
%% @doc Register ejabberd commands.
%% If a command is already registered, a warning is printed and the
%% old command is preserved.
+%% A registered command is not directly available to be called through
+%% ejabberd ReST API. It need to be exposed to be available through API.
register_commands(Commands) ->
lists:foreach(
fun(Command) ->
- % XXX check if command exists
- mnesia:dirty_write(Command)
- % ?DEBUG("This command is already defined:~n~p", [Command])
+ %% XXX check if command exists
+ mnesia:dirty_write(Command)
+ %% ?DEBUG("This command is already defined:~n~p", [Command])
end,
Commands).
@@ -306,6 +309,25 @@ unregister_commands(Commands) ->
end,
Commands).
+%% @doc Expose command through ejabberd ReST API.
+%% Pass a list of command names or policy to expose.
+-spec expose_commands([ejabberd_commands()|atom()|open|user|admin|restricted]) -> ok | {error, atom()}.
+
+expose_commands(Commands) ->
+ Names = lists:map(fun(#ejabberd_commands{name = Name}) ->
+ Name;
+ (Name) when is_atom(Name) ->
+ Name
+ end,
+ Commands),
+
+ case ejabberd_config:add_local_option(commands, [{add_commands, Names}]) of
+ {aborted, Reason} ->
+ {error, Reason};
+ {atomic, Result} ->
+ Result
+ end.
+
-spec list_commands() -> [{atom(), [aterm()], string()}].
%% @doc Get a list of all the available commands, arguments and description.
@@ -319,8 +341,8 @@ list_commands() ->
list_commands(Version) ->
Commands = get_commands_definition(Version),
[{Name, Args, Desc} || #ejabberd_commands{name = Name,
- args = Args,
- desc = Desc} <- Commands].
+ args = Args,
+ desc = Desc} <- Commands].
-spec list_commands_policy(integer()) ->
@@ -331,10 +353,10 @@ list_commands(Version) ->
list_commands_policy(Version) ->
Commands = get_commands_definition(Version),
[{Name, Args, Desc, Policy} ||
- #ejabberd_commands{name = Name,
- args = Args,
- desc = Desc,
- policy = Policy} <- Commands].
+ #ejabberd_commands{name = Name,
+ args = Args,
+ desc = Desc,
+ policy = Policy} <- Commands].
-spec get_command_format(atom()) -> {[aterm()], rterm()}.
@@ -356,27 +378,33 @@ get_command_format(Name, Auth, Version) ->
Admin = is_admin(Name, Auth, #{}),
#ejabberd_commands{args = Args,
result = Result,
- policy = Policy} =
- get_command_definition(Name, Version),
+ policy = Policy} =
+ get_command_definition(Name, Version),
case Policy of
- user when Admin;
- Auth == noauth ->
- {[{user, binary}, {server, binary} | Args], Result};
- _ ->
- {Args, Result}
+ user when Admin;
+ Auth == noauth ->
+ {[{user, binary}, {server, binary} | Args], Result};
+ _ ->
+ {Args, Result}
end.
--spec get_command_policy(atom()) -> {ok, open|user|admin|restricted} | {error, command_not_found}.
+-spec get_command_policy_and_scope(atom()) -> {ok, open|user|admin|restricted, [oauth_scope()]} | {error, command_not_found}.
%% @doc return command policy.
-get_command_policy(Name) ->
+get_command_policy_and_scope(Name) ->
case get_command_definition(Name) of
- #ejabberd_commands{policy = Policy} ->
- {ok, Policy};
+ #ejabberd_commands{policy = Policy} = Cmd ->
+ {ok, Policy, cmd_scope(Cmd)};
command_not_found ->
{error, command_not_found}
end.
+%% The oauth scopes for a command are the command name itself,
+%% also might include either 'ejabberd:user' or 'ejabberd:admin'
+cmd_scope(#ejabberd_commands{policy = Policy, name = Name}) ->
+ [erlang:atom_to_binary(Name,utf8)] ++ [<<"ejabberd:user">> || Policy == user] ++ [<<"ejabberd:admin">> || Policy == admin].
+
+
-spec get_command_definition(atom()) -> ejabberd_commands().
%% @doc Get the definition record of a command.
@@ -388,16 +416,16 @@ get_command_definition(Name) ->
%% @doc Get the definition record of a command in a given API version.
get_command_definition(Name, Version) ->
case lists:reverse(
- lists:sort(
- mnesia:dirty_select(
- ejabberd_commands,
- ets:fun2ms(
- fun(#ejabberd_commands{name = N, version = V} = C)
- when N == Name, V =< Version ->
- {V, C}
- end)))) of
- [{_, Command} | _ ] -> Command;
- _E -> throw(unknown_command)
+ lists:sort(
+ mnesia:dirty_select(
+ ejabberd_commands,
+ ets:fun2ms(
+ fun(#ejabberd_commands{name = N, version = V} = C)
+ when N == Name, V =< Version ->
+ {V, C}
+ end)))) of
+ [{_, Command} | _ ] -> Command;
+ _E -> throw(unknown_command)
end.
-spec get_commands_definition(integer()) -> [ejabberd_commands()].
@@ -405,20 +433,20 @@ get_command_definition(Name, Version) ->
% @doc Returns all commands for a given API version
get_commands_definition(Version) ->
L = lists:reverse(
- lists:sort(
- mnesia:dirty_select(
- ejabberd_commands,
- ets:fun2ms(
- fun(#ejabberd_commands{name = Name, version = V} = C)
- when V =< Version ->
- {Name, V, C}
- end)))),
+ lists:sort(
+ mnesia:dirty_select(
+ ejabberd_commands,
+ ets:fun2ms(
+ fun(#ejabberd_commands{name = Name, version = V} = C)
+ when V =< Version ->
+ {Name, V, C}
+ end)))),
F = fun({_Name, _V, Command}, []) ->
- [Command];
- ({Name, _V, _Command}, [#ejabberd_commands{name=Name}|_T] = Acc) ->
- Acc;
- ({_Name, _V, Command}, Acc) -> [Command | Acc]
- end,
+ [Command];
+ ({Name, _V, _Command}, [#ejabberd_commands{name=Name}|_T] = Acc) ->
+ Acc;
+ ({_Name, _V, Command}, Acc) -> [Command | Acc]
+ end,
lists:foldl(F, [], L).
%% @spec (Name::atom(), Arguments) -> ResultTerm
@@ -427,7 +455,7 @@ get_commands_definition(Version) ->
%% @doc Execute a command.
%% Can return the following exceptions:
%% command_unknown | account_unprivileged | invalid_account_data |
-%% no_auth_provided
+%% no_auth_provided | access_rules_unauthorized
execute_command(Name, Arguments) ->
execute_command(Name, Arguments, ?DEFAULT_VERSION).
@@ -488,41 +516,62 @@ execute_command(AccessCommands, Auth, Name, Arguments) ->
%%
%% @doc Execute a command in a given API version
%% Can return the following exceptions:
-%% command_unknown | account_unprivileged | invalid_account_data | no_auth_provided
+%% command_unknown | account_unprivileged | invalid_account_data | no_auth_provided | access_rules_unauthorized
execute_command(AccessCommands1, Auth1, Name, Arguments, Version) ->
-execute_command(AccessCommands1, Auth1, Name, Arguments, Version, #{}).
+ execute_command(AccessCommands1, Auth1, Name, Arguments, Version, #{}).
execute_command(AccessCommands1, Auth1, Name, Arguments, Version, CallerInfo) ->
Auth = case is_admin(Name, Auth1, CallerInfo) of
true -> admin;
false -> Auth1
end,
+ TokenJID = oauth_token_user(Auth1),
Command = get_command_definition(Name, Version),
- AccessCommands = get_access_commands(AccessCommands1, Version),
+ AccessCommands = get_all_access_commands(AccessCommands1),
+
case check_access_commands(AccessCommands, Auth, Name, Command, Arguments, CallerInfo) of
- ok -> execute_command2(Auth, Command, Arguments)
+ ok -> execute_check_policy(Auth, TokenJID, Command, Arguments)
+ end.
+
+
+execute_check_policy(
+ _Auth, _JID, #ejabberd_commands{policy = open} = Command, Arguments) ->
+ do_execute_command(Command, Arguments);
+execute_check_policy(
+ _Auth, _JID, #ejabberd_commands{policy = restricted} = Command, Arguments) ->
+ do_execute_command(Command, Arguments);
+execute_check_policy(
+ _Auth, JID, #ejabberd_commands{policy = admin} = Command, Arguments) ->
+ execute_check_access(JID, Command, Arguments);
+execute_check_policy(
+ admin, JID, #ejabberd_commands{policy = user} = Command, Arguments) ->
+ execute_check_access(JID, Command, Arguments);
+execute_check_policy(
+ noauth, _JID, #ejabberd_commands{policy = user} = Command, Arguments) ->
+ do_execute_command(Command, Arguments);
+execute_check_policy(
+ {User, Server, _, _}, JID, #ejabberd_commands{policy = user} = Command, Arguments) ->
+ execute_check_access(JID, Command, [User, Server | Arguments]).
+
+execute_check_access(_FromJID, #ejabberd_commands{access = []} = Command, Arguments) ->
+ do_execute_command(Command, Arguments);
+execute_check_access(FromJID, #ejabberd_commands{access = AccessRefs} = Command, Arguments) ->
+ %% TODO Review: Do we have smarter / better way to check rule on other Host than global ?
+ Host = global,
+ Rules = lists:map(fun({Mod, AccessName, Default}) ->
+ gen_mod:get_module_opt(Host, Mod,
+ AccessName, fun(A) -> A end, Default);
+ (Default) ->
+ Default
+ end, AccessRefs),
+ case acl:any_rules_allowed(Host, Rules, FromJID) of
+ true ->
+ do_execute_command(Command, Arguments);
+ false ->
+ throw({error, access_rules_unauthorized})
end.
-execute_command2(
- _Auth, #ejabberd_commands{policy = open} = Command, Arguments) ->
- execute_command2(Command, Arguments);
-execute_command2(
- _Auth, #ejabberd_commands{policy = restricted} = Command, Arguments) ->
- execute_command2(Command, Arguments);
-execute_command2(
- _Auth, #ejabberd_commands{policy = admin} = Command, Arguments) ->
- execute_command2(Command, Arguments);
-execute_command2(
- admin, #ejabberd_commands{policy = user} = Command, Arguments) ->
- execute_command2(Command, Arguments);
-execute_command2(
- noauth, #ejabberd_commands{policy = user} = Command, Arguments) ->
- execute_command2(Command, Arguments);
-execute_command2(
- {User, Server, _, _}, #ejabberd_commands{policy = user} = Command, Arguments) ->
- execute_command2(Command, [User, Server | Arguments]).
-
-execute_command2(Command, Arguments) ->
+do_execute_command(Command, Arguments) ->
Module = Command#ejabberd_commands.module,
Function = Command#ejabberd_commands.function,
?DEBUG("Executing command ~p:~p with Args=~p", [Module, Function, Arguments]),
@@ -592,31 +641,31 @@ check_access_commands(AccessCommands, Auth, Method, Command1, Arguments, CallerI
Command1
end,
AccessCommandsAllowed =
- lists:filter(
- fun({Access, Commands, ArgumentRestrictions}) ->
- case check_access(Command, Access, Auth, CallerInfo) of
- true ->
- check_access_command(Commands, Command,
- ArgumentRestrictions,
- Method, Arguments);
- false ->
- false
- end;
- ({Access, Commands}) ->
- ArgumentRestrictions = [],
- case check_access(Command, Access, Auth, CallerInfo) of
- true ->
- check_access_command(Commands, Command,
- ArgumentRestrictions,
- Method, Arguments);
- false ->
- false
- end
- end,
- AccessCommands),
+ lists:filter(
+ fun({Access, Commands, ArgumentRestrictions}) ->
+ case check_access(Command, Access, Auth, CallerInfo) of
+ true ->
+ check_access_command(Commands, Command,
+ ArgumentRestrictions,
+ Method, Arguments);
+ false ->
+ false
+ end;
+ ({Access, Commands}) ->
+ ArgumentRestrictions = [],
+ case check_access(Command, Access, Auth, CallerInfo) of
+ true ->
+ check_access_command(Commands, Command,
+ ArgumentRestrictions,
+ Method, Arguments);
+ false ->
+ false
+ end
+ end,
+ AccessCommands),
case AccessCommandsAllowed of
- [] -> throw({error, account_unprivileged});
- L when is_list(L) -> ok
+ [] -> throw({error, account_unprivileged});
+ L when is_list(L) -> ok
end.
-spec check_auth(ejabberd_commands(), noauth) -> noauth_provided;
@@ -627,8 +676,8 @@ check_access_commands(AccessCommands, Auth, Method, Command1, Arguments, CallerI
check_auth(_Command, noauth) ->
no_auth_provided;
check_auth(Command, {User, Server, {oauth, Token}, _}) ->
- Scope = erlang:atom_to_binary(Command#ejabberd_commands.name, utf8),
- case ejabberd_oauth:check_token(User, Server, Scope, Token) of
+ ScopeList = cmd_scope(Command),
+ case ejabberd_oauth:check_token(User, Server, ScopeList, Token) of
true ->
{ok, User, Server};
false ->
@@ -680,9 +729,9 @@ check_access2(Access, AccessInfo, Server) ->
check_access_command(Commands, Command, ArgumentRestrictions,
Method, Arguments) ->
case Commands==all orelse lists:member(Method, Commands) of
- true -> check_access_arguments(Command, ArgumentRestrictions,
- Arguments);
- false -> false
+ true -> check_access_arguments(Command, ArgumentRestrictions,
+ Arguments);
+ false -> false
end.
check_access_arguments(Command, ArgumentRestrictions, Arguments) ->
@@ -705,19 +754,23 @@ tag_arguments(ArgsDefs, Args) ->
Args).
+%% Get commands for all version
+get_all_access_commands(AccessCommands) ->
+ get_access_commands(AccessCommands, ?DEFAULT_VERSION).
+
get_access_commands(undefined, Version) ->
- Cmds = get_commands(Version),
+ Cmds = get_exposed_commands(Version),
[{?POLICY_ACCESS, Cmds, []}];
get_access_commands(AccessCommands, _Version) ->
AccessCommands.
-get_commands() ->
- get_commands(?DEFAULT_VERSION).
-get_commands(Version) ->
+get_exposed_commands() ->
+ get_exposed_commands(?DEFAULT_VERSION).
+get_exposed_commands(Version) ->
Opts0 = ejabberd_config:get_option(
commands,
fun(V) when is_list(V) -> V end,
- []),
+ []),
Opts = lists:map(fun(V) when is_tuple(V) -> [V]; (V) -> V end, Opts0),
CommandsList = list_commands_policy(Version),
OpenCmds = [N || {N, _, _, open} <- CommandsList],
@@ -727,27 +780,32 @@ get_commands(Version) ->
Cmds =
lists:foldl(
fun([{add_commands, L}], Acc) ->
- Cmds = case L of
- open -> OpenCmds;
- restricted -> RestrictedCmds;
- admin -> AdminCmds;
- user -> UserCmds;
- _ when is_list(L) -> L
- end,
+ Cmds = expand_commands(L, OpenCmds, UserCmds, AdminCmds, RestrictedCmds),
lists:usort(Cmds ++ Acc);
([{remove_commands, L}], Acc) ->
- Cmds = case L of
- open -> OpenCmds;
- restricted -> RestrictedCmds;
- admin -> AdminCmds;
- user -> UserCmds;
- _ when is_list(L) -> L
- end,
+ Cmds = expand_commands(L, OpenCmds, UserCmds, AdminCmds, RestrictedCmds),
Acc -- Cmds;
(_, Acc) -> Acc
- end, AdminCmds ++ UserCmds, Opts),
+ end, [], Opts),
Cmds.
+%% This is used to allow mixing command policy (like open, user, admin, restricted), with command entry
+expand_commands(L, OpenCmds, UserCmds, AdminCmds, RestrictedCmds) when is_list(L) ->
+ lists:foldl(fun(open, Acc) -> OpenCmds ++ Acc;
+ (user, Acc) -> UserCmds ++ Acc;
+ (admin, Acc) -> AdminCmds ++ Acc;
+ (restricted, Acc) -> RestrictedCmds ++ Acc;
+ (Command, Acc) when is_atom(Command) ->
+ [Command|Acc]
+ end, [], L).
+
+oauth_token_user(noauth) ->
+ undefined;
+oauth_token_user(admin) ->
+ undefined;
+oauth_token_user({User, Server, _, _}) ->
+ jid:make(User, Server, <<>>).
+
is_admin(_Name, admin, _Extra) ->
true;
is_admin(_Name, {_User, _Server, _, false}, _Extra) ->
diff --git a/src/ejabberd_config.erl b/src/ejabberd_config.erl
index df9debd62..87a918704 100644
--- a/src/ejabberd_config.erl
+++ b/src/ejabberd_config.erl
@@ -220,9 +220,6 @@ env_binary_to_list(Application, Parameter) ->
%% in which the options 'include_config_file' were parsed
%% and the terms in those files were included.
%% @spec(iolist()) -> [term()]
-get_plain_terms_file(File) ->
- get_plain_terms_file(File, [{include_files, true}]).
-
get_plain_terms_file(File, Opts) when is_binary(File) ->
get_plain_terms_file(binary_to_list(File), Opts);
get_plain_terms_file(File1, Opts) ->
diff --git a/src/ejabberd_http.erl b/src/ejabberd_http.erl
index 6b53f46c6..a79f26305 100644
--- a/src/ejabberd_http.erl
+++ b/src/ejabberd_http.erl
@@ -763,7 +763,8 @@ parse_auth(<<"Basic ", Auth64/binary>>) ->
undefined;
Pos ->
{User, <<$:, Pass/binary>>} = erlang:split_binary(Auth, Pos-1),
- {User, Pass}
+ PassUtf8 = unicode:characters_to_binary(binary_to_list(Pass), utf8),
+ {User, PassUtf8}
end;
parse_auth(<<"Bearer ", SToken/binary>>) ->
Token = str:strip(SToken),
diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl
index 57c1baab2..0af158562 100644
--- a/src/ejabberd_oauth.erl
+++ b/src/ejabberd_oauth.erl
@@ -39,7 +39,6 @@
authenticate_user/2,
authenticate_client/2,
verify_resowner_scope/3,
- verify_client_scope/3,
associate_access_code/3,
associate_access_token/3,
associate_refresh_token/3,
@@ -48,7 +47,7 @@
process/2,
opt_type/1]).
--export([oauth_issue_token/1, oauth_list_tokens/0, oauth_revoke_token/1, oauth_list_scopes/0]).
+-export([oauth_issue_token/3, oauth_list_tokens/0, oauth_revoke_token/1, oauth_list_scopes/0]).
-include("jlib.hrl").
@@ -57,6 +56,7 @@
-include("ejabberd_http.hrl").
-include("ejabberd_web_admin.hrl").
+-include("ejabberd_oauth.hrl").
-include("ejabberd_commands.hrl").
@@ -65,17 +65,12 @@
%% * Using the web form/api results in the token being generated in behalf of the user providing the user/pass
%% * Using the command line and oauth_issue_token command, the token is generated in behalf of ejabberd' sysadmin
%% (as it has access to ejabberd command line).
--record(oauth_token, {
- token = {<<"">>, <<"">>} :: {binary(), binary()},
- us = {<<"">>, <<"">>} :: {binary(), binary()} | server_admin,
- scope = [] :: [binary()],
- expire :: integer()
- }).
--define(EXPIRE, 3600).
+-define(EXPIRE, 31536000).
start() ->
- init_db(mnesia, ?MYNAME),
+ DBMod = get_db_backend(),
+ DBMod:init(),
Expire = expire(),
application:set_env(oauth2, backend, ejabberd_oauth),
application:set_env(oauth2, expiry_time, Expire),
@@ -90,57 +85,61 @@ start() ->
get_commands_spec() ->
[
#ejabberd_commands{name = oauth_issue_token, tags = [oauth],
- desc = "Issue an oauth token. Available scopes are the ones usable by ejabberd admins",
+ desc = "Issue an oauth token for the given jid",
module = ?MODULE, function = oauth_issue_token,
- args = [{scopes, string}],
+ args = [{jid, string},{ttl, integer}, {scopes, string}],
policy = restricted,
- args_example = ["connected_users_number;muc_online_rooms"],
+ args_example = ["user@server.com", "connected_users_number;muc_online_rooms"],
args_desc = ["List of scopes to allow, separated by ';'"],
result = {result, {tuple, [{token, string}, {scopes, string}, {expires_in, string}]}}
},
#ejabberd_commands{name = oauth_list_tokens, tags = [oauth],
- desc = "List oauth tokens, their scope, and how many seconds remain until expirity",
+ desc = "List oauth tokens, their user and scope, and how many seconds remain until expirity",
module = ?MODULE, function = oauth_list_tokens,
args = [],
policy = restricted,
- result = {tokens, {list, {token, {tuple, [{token, string}, {scope, string}, {expires_in, string}]}}}}
+ result = {tokens, {list, {token, {tuple, [{token, string}, {user, string}, {scope, string}, {expires_in, string}]}}}}
},
#ejabberd_commands{name = oauth_list_scopes, tags = [oauth],
- desc = "List scopes that can be granted to tokens generated through the command line",
+ desc = "List scopes that can be granted to tokens generated through the command line, together with the commands they allow",
module = ?MODULE, function = oauth_list_scopes,
args = [],
policy = restricted,
- result = {scopes, {list, {scope, string}}}
+ result = {scopes, {list, {scope, {tuple, [{scope, string}, {commands, string}]}}}}
},
#ejabberd_commands{name = oauth_revoke_token, tags = [oauth],
desc = "Revoke authorization for a token",
module = ?MODULE, function = oauth_revoke_token,
args = [{token, string}],
policy = restricted,
- result = {tokens, {list, {token, {tuple, [{token, string}, {scope, string}, {expires_in, string}]}}}},
+ result = {tokens, {list, {token, {tuple, [{token, string}, {user, string}, {scope, string}, {expires_in, string}]}}}},
result_desc = "List of remaining tokens"
}
].
-oauth_issue_token(ScopesString) ->
+oauth_issue_token(Jid, TTLSeconds, ScopesString) ->
Scopes = [list_to_binary(Scope) || Scope <- string:tokens(ScopesString, ";")],
- case oauth2:authorize_client_credentials(ejabberd_ctl, Scopes, none) of
- {ok, {_AppCtx, Authorization}} ->
- {ok, {_AppCtx2, Response}} = oauth2:issue_token(Authorization, none),
- {ok, AccessToken} = oauth2_response:access_token(Response),
- {ok, Expires} = oauth2_response:expires_in(Response),
- {ok, VerifiedScope} = oauth2_response:scope(Response),
- {AccessToken, VerifiedScope, integer_to_list(Expires) ++ " seconds"};
- {error, Error} ->
- {error, Error}
+ case jid:from_string(list_to_binary(Jid)) of
+ #jid{luser =Username, lserver = Server} ->
+ case oauth2:authorize_password({Username, Server}, Scopes, admin_generated) of
+ {ok, {_Ctx,Authorization}} ->
+ {ok, {_AppCtx2, Response}} = oauth2:issue_token(Authorization, [{expiry_time, TTLSeconds}]),
+ {ok, AccessToken} = oauth2_response:access_token(Response),
+ {ok, VerifiedScope} = oauth2_response:scope(Response),
+ {AccessToken, VerifiedScope, integer_to_list(TTLSeconds) ++ " seconds"};
+ {error, Error} ->
+ {error, Error}
+ end;
+ error ->
+ {error, "Invalid JID: " ++ Jid}
end.
oauth_list_tokens() ->
- Tokens = mnesia:dirty_match_object(#oauth_token{us = server_admin, _ = '_'}),
+ Tokens = mnesia:dirty_match_object(#oauth_token{_ = '_'}),
{MegaSecs, Secs, _MiniSecs} = os:timestamp(),
TS = 1000000 * MegaSecs + Secs,
- [{Token, Scope, integer_to_list(Expires - TS) ++ " seconds"} ||
- #oauth_token{token=Token, scope=Scope, expire=Expires} <- Tokens].
+ [{Token, jid:to_string(jid:make(U,S,<<>>)), Scope, integer_to_list(Expires - TS) ++ " seconds"} ||
+ #oauth_token{token=Token, scope=Scope, us= {U,S},expire=Expires} <- Tokens].
oauth_revoke_token(Token) ->
@@ -148,8 +147,7 @@ oauth_revoke_token(Token) ->
oauth_list_tokens().
oauth_list_scopes() ->
- get_cmd_scopes().
-
+ [ {Scope, string:join([atom_to_list(Cmd) || Cmd <- Cmds], ",")} || {Scope, Cmds} <- dict:to_list(get_cmd_scopes())].
@@ -170,15 +168,8 @@ handle_cast(_Msg, State) -> {noreply, State}.
handle_info(clean, State) ->
{MegaSecs, Secs, MiniSecs} = os:timestamp(),
TS = 1000000 * MegaSecs + Secs,
- F = fun() ->
- Ts = mnesia:select(
- oauth_token,
- [{#oauth_token{expire = '$1', _ = '_'},
- [{'<', '$1', TS}],
- ['$_']}]),
- lists:foreach(fun mnesia:delete_object/1, Ts)
- end,
- mnesia:async_dirty(F),
+ DBMod = get_db_backend(),
+ DBMod:clean(TS),
erlang:send_after(trunc(expire() * 1000 * (1 + MiniSecs / 1000000)),
self(), clean),
{noreply, State};
@@ -189,21 +180,11 @@ terminate(_Reason, _State) -> ok.
code_change(_OldVsn, State, _Extra) -> {ok, State}.
-init_db(mnesia, _Host) ->
- mnesia:create_table(oauth_token,
- [{disc_copies, [node()]},
- {attributes,
- record_info(fields, oauth_token)}]),
- mnesia:add_table_copy(oauth_token, node(), disc_copies);
-init_db(_, _) ->
- ok.
-
-
get_client_identity(Client, Ctx) -> {ok, {Ctx, {client, Client}}}.
verify_redirection_uri(_, _, Ctx) -> {ok, Ctx}.
-authenticate_user({User, Server}, {password, Password} = Ctx) ->
+authenticate_user({User, Server}, Ctx) ->
case jid:make(User, Server, <<"">>) of
#jid{} = JID ->
Access =
@@ -213,11 +194,16 @@ authenticate_user({User, Server}, {password, Password} = Ctx) ->
none),
case acl:match_rule(JID#jid.lserver, Access, JID) of
allow ->
- case ejabberd_auth:check_password(User, <<"">>, Server, Password) of
- true ->
- {ok, {Ctx, {user, User, Server}}};
- false ->
- {error, badpass}
+ case Ctx of
+ {password, Password} ->
+ case ejabberd_auth:check_password(User, <<"">>, Server, Password) of
+ true ->
+ {ok, {Ctx, {user, User, Server}}};
+ false ->
+ {error, badpass}
+ end;
+ admin_generated ->
+ {ok, {Ctx, {user, User, Server}}}
end;
deny ->
{error, badpass}
@@ -229,8 +215,8 @@ authenticate_user({User, Server}, {password, Password} = Ctx) ->
authenticate_client(Client, Ctx) -> {ok, {Ctx, {client, Client}}}.
verify_resowner_scope({user, _User, _Server}, Scope, Ctx) ->
- Cmds = ejabberd_commands:get_commands(),
- Cmds1 = [sasl_auth | Cmds],
+ Cmds = ejabberd_commands:get_exposed_commands(),
+ Cmds1 = ['ejabberd:user', 'ejabberd:admin', sasl_auth | Cmds],
RegisteredScope = [atom_to_binary(C, utf8) || C <- Cmds1],
case oauth2_priv_set:is_subset(oauth2_priv_set:new(Scope),
oauth2_priv_set:new(RegisteredScope)) of
@@ -244,27 +230,35 @@ verify_resowner_scope(_, _, _) ->
get_cmd_scopes() ->
- Cmds = lists:filter(fun(Cmd) -> case ejabberd_commands:get_command_policy(Cmd) of
- {ok, Policy} when Policy =/= restricted -> true;
- _ -> false
- end end,
- ejabberd_commands:get_commands()),
- [atom_to_binary(C, utf8) || C <- Cmds].
+ ScopeMap = lists:foldl(fun(Cmd, Accum) ->
+ case ejabberd_commands:get_command_policy_and_scope(Cmd) of
+ {ok, Policy, Scopes} when Policy =/= restricted ->
+ lists:foldl(fun(Scope, Accum2) ->
+ dict:append(Scope, Cmd, Accum2)
+ end, Accum, Scopes);
+ _ -> Accum
+ end end, dict:new(), ejabberd_commands:get_exposed_commands()),
+ ScopeMap.
%% This is callback for oauth tokens generated through the command line. Only open and admin commands are
%% made available.
-verify_client_scope({client, ejabberd_ctl}, Scope, Ctx) ->
- RegisteredScope = get_cmd_scopes(),
- case oauth2_priv_set:is_subset(oauth2_priv_set:new(Scope),
- oauth2_priv_set:new(RegisteredScope)) of
- true ->
- {ok, {Ctx, Scope}};
- false ->
- {error, badscope}
- end.
+%verify_client_scope({client, ejabberd_ctl}, Scope, Ctx) ->
+% RegisteredScope = dict:fetch_keys(get_cmd_scopes()),
+% case oauth2_priv_set:is_subset(oauth2_priv_set:new(Scope),
+% oauth2_priv_set:new(RegisteredScope)) of
+% true ->
+% {ok, {Ctx, Scope}};
+% false ->
+% {error, badscope}
+% end.
+
+-spec seconds_since_epoch(integer()) -> non_neg_integer().
+seconds_since_epoch(Diff) ->
+ {Mega, Secs, _} = os:timestamp(),
+ Mega * 1000000 + Secs + Diff.
associate_access_code(_AccessCode, _Context, AppContext) ->
@@ -272,58 +266,63 @@ associate_access_code(_AccessCode, _Context, AppContext) ->
{ok, AppContext}.
associate_access_token(AccessToken, Context, AppContext) ->
- %% Tokens generated using the API/WEB belongs to users and always include the user, server pair.
- %% Tokens generated form command line aren't tied to an user, and instead belongs to the ejabberd sysadmin
- US = case proplists:get_value(<<"resource_owner">>, Context, <<"">>) of
- {user, User, Server} -> {jid:nodeprep(User), jid:nodeprep(Server)};
- undefined -> server_admin
- end,
+ {user, User, Server} = proplists:get_value(<<"resource_owner">>, Context, <<"">>),
+ Expire = case proplists:get_value(expiry_time, AppContext, undefined) of
+ undefined ->
+ proplists:get_value(<<"expiry_time">>, Context, 0);
+ ExpiresIn ->
+ %% There is no clean way in oauth2 lib to actually override the TTL of the generated token.
+ %% It always pass the global configured value. Here we use the app context to pass the per-case
+ %% ttl if we want to override it.
+ seconds_since_epoch(ExpiresIn)
+ end,
+ {user, User, Server} = proplists:get_value(<<"resource_owner">>, Context, <<"">>),
Scope = proplists:get_value(<<"scope">>, Context, []),
- Expire = proplists:get_value(<<"expiry_time">>, Context, 0),
R = #oauth_token{
token = AccessToken,
- us = US,
+ us = {jid:nodeprep(User), jid:nodeprep(Server)},
scope = Scope,
expire = Expire
},
- mnesia:dirty_write(R),
+ DBMod = get_db_backend(),
+ DBMod:store(R),
{ok, AppContext}.
associate_refresh_token(_RefreshToken, _Context, AppContext) ->
%put(?REFRESH_TOKEN_TABLE, RefreshToken, Context),
{ok, AppContext}.
-
-check_token(User, Server, Scope, Token) ->
+check_token(User, Server, ScopeList, Token) ->
LUser = jid:nodeprep(User),
LServer = jid:nameprep(Server),
- case catch mnesia:dirty_read(oauth_token, Token) of
- [#oauth_token{us = {LUser, LServer},
- scope = TokenScope,
- expire = Expire}] ->
+ DBMod = get_db_backend(),
+ case DBMod:lookup(Token) of
+ #oauth_token{us = {LUser, LServer},
+ scope = TokenScope,
+ expire = Expire} ->
{MegaSecs, Secs, _} = os:timestamp(),
TS = 1000000 * MegaSecs + Secs,
- oauth2_priv_set:is_member(
- Scope, oauth2_priv_set:new(TokenScope)) andalso
- Expire > TS;
+ TokenScopeSet = oauth2_priv_set:new(TokenScope),
+ lists:any(fun(Scope) ->
+ oauth2_priv_set:is_member(Scope, TokenScopeSet) end,
+ ScopeList) andalso Expire > TS;
_ ->
false
end.
-check_token(Scope, Token) ->
- case catch mnesia:dirty_read(oauth_token, Token) of
- [#oauth_token{us = US,
- scope = TokenScope,
- expire = Expire}] ->
+check_token(ScopeList, Token) ->
+ DBMod = get_db_backend(),
+ case DBMod:lookup(Token) of
+ #oauth_token{us = US,
+ scope = TokenScope,
+ expire = Expire} ->
{MegaSecs, Secs, _} = os:timestamp(),
TS = 1000000 * MegaSecs + Secs,
- case oauth2_priv_set:is_member(
- Scope, oauth2_priv_set:new(TokenScope)) andalso
- Expire > TS of
- true -> case US of
- {LUser, LServer} -> {ok, user, {LUser, LServer}};
- server_admin -> {ok, server_admin}
- end;
+ TokenScopeSet = oauth2_priv_set:new(TokenScope),
+ case lists:any(fun(Scope) ->
+ oauth2_priv_set:is_member(Scope, TokenScopeSet) end,
+ ScopeList) andalso Expire > TS of
+ true -> {ok, user, US};
false -> false
end;
_ ->
@@ -358,12 +357,9 @@ process(_Handlers,
?XAE(<<"form">>,
[{<<"action">>, <<"authorization_token">>},
{<<"method">>, <<"post">>}],
- [?LABEL(<<"username">>, [?CT(<<"User">>), ?C(<<": ">>)]),
+ [?LABEL(<<"username">>, [?CT(<<"User (jid)">>), ?C(<<": ">>)]),
?INPUTID(<<"text">>, <<"username">>, <<"">>),
?BR,
- ?LABEL(<<"server">>, [?CT(<<"Server">>), ?C(<<": ">>)]),
- ?INPUTID(<<"text">>, <<"server">>, <<"">>),
- ?BR,
?LABEL(<<"password">>, [?CT(<<"Password">>), ?C(<<": ">>)]),
?INPUTID(<<"password">>, <<"password">>, <<"">>),
?INPUT(<<"hidden">>, <<"response_type">>, ResponseType),
@@ -372,6 +368,15 @@ process(_Handlers,
?INPUT(<<"hidden">>, <<"scope">>, Scope),
?INPUT(<<"hidden">>, <<"state">>, State),
?BR,
+ ?LABEL(<<"ttl">>, [?CT(<<"Token TTL">>), ?CT(<<": ">>)]),
+ ?XAE(<<"select">>, [{<<"name">>, <<"ttl">>}],
+ [
+ ?XAC(<<"option">>, [{<<"value">>, <<"3600">>}],<<"1 Hour">>),
+ ?XAC(<<"option">>, [{<<"value">>, <<"86400">>}],<<"1 Day">>),
+ ?XAC(<<"option">>, [{<<"value">>, <<"2592000">>}],<<"1 Month">>),
+ ?XAC(<<"option">>, [{<<"selected">>, <<"selected">>},{<<"value">>, <<"31536000">>}],<<"1 Year">>),
+ ?XAC(<<"option">>, [{<<"value">>, <<"315360000">>}],<<"10 Years">>)]),
+ ?BR,
?INPUTT(<<"submit">>, <<"">>, <<"Accept">>)
]),
Top =
@@ -415,11 +420,16 @@ process(_Handlers,
ClientId = proplists:get_value(<<"client_id">>, Q, <<"">>),
RedirectURI = proplists:get_value(<<"redirect_uri">>, Q, <<"">>),
SScope = proplists:get_value(<<"scope">>, Q, <<"">>),
- Username = proplists:get_value(<<"username">>, Q, <<"">>),
- Server = proplists:get_value(<<"server">>, Q, <<"">>),
+ StringJID = proplists:get_value(<<"username">>, Q, <<"">>),
+ #jid{user = Username, server = Server} = jid:from_string(StringJID),
Password = proplists:get_value(<<"password">>, Q, <<"">>),
State = proplists:get_value(<<"state">>, Q, <<"">>),
Scope = str:tokens(SScope, <<" ">>),
+ TTL = proplists:get_value(<<"ttl">>, Q, <<"">>),
+ ExpiresIn = case TTL of
+ <<>> -> undefined;
+ _ -> jlib:binary_to_integer(TTL)
+ end,
case oauth2:authorize_password({Username, Server},
ClientId,
RedirectURI,
@@ -427,10 +437,18 @@ process(_Handlers,
{password, Password}) of
{ok, {_AppContext, Authorization}} ->
{ok, {_AppContext2, Response}} =
- oauth2:issue_token(Authorization, none),
+ oauth2:issue_token(Authorization, [{expiry_time, ExpiresIn} || ExpiresIn /= undefined ]),
{ok, AccessToken} = oauth2_response:access_token(Response),
{ok, Type} = oauth2_response:token_type(Response),
- {ok, Expires} = oauth2_response:expires_in(Response),
+ %%Ugly: workardound to return the correct expirity time, given than oauth2 lib doesn't really have
+ %%per-case expirity time.
+ Expires = case ExpiresIn of
+ undefined ->
+ {ok, Ex} = oauth2_response:expires_in(Response),
+ Ex;
+ _ ->
+ ExpiresIn
+ end,
{ok, VerifiedScope} = oauth2_response:scope(Response),
%oauth2_wrq:redirected_access_token_response(ReqData,
% RedirectURI,
@@ -459,11 +477,82 @@ process(_Handlers,
}],
ejabberd_web:make_xhtml([?XC(<<"h1">>, <<"302 Found">>)])}
end;
+process(_Handlers,
+ #request{method = 'POST', q = Q, lang = _Lang,
+ path = [_, <<"token">>]}) ->
+ case proplists:get_value(<<"grant_type">>, Q, <<"">>) of
+ <<"password">> ->
+ SScope = proplists:get_value(<<"scope">>, Q, <<"">>),
+ StringJID = proplists:get_value(<<"username">>, Q, <<"">>),
+ #jid{user = Username, server = Server} = jid:from_string(StringJID),
+ Password = proplists:get_value(<<"password">>, Q, <<"">>),
+ Scope = str:tokens(SScope, <<" ">>),
+ TTL = proplists:get_value(<<"ttl">>, Q, <<"">>),
+ ExpiresIn = case TTL of
+ <<>> -> undefined;
+ _ -> jlib:binary_to_integer(TTL)
+ end,
+ case oauth2:authorize_password({Username, Server},
+ Scope,
+ {password, Password}) of
+ {ok, {_AppContext, Authorization}} ->
+ {ok, {_AppContext2, Response}} =
+ oauth2:issue_token(Authorization, [{expiry_time, ExpiresIn} || ExpiresIn /= undefined ]),
+ {ok, AccessToken} = oauth2_response:access_token(Response),
+ {ok, Type} = oauth2_response:token_type(Response),
+ %%Ugly: workardound to return the correct expirity time, given than oauth2 lib doesn't really have
+ %%per-case expirity time.
+ Expires = case ExpiresIn of
+ undefined ->
+ {ok, Ex} = oauth2_response:expires_in(Response),
+ Ex;
+ _ ->
+ ExpiresIn
+ end,
+ {ok, VerifiedScope} = oauth2_response:scope(Response),
+ json_response(200, {[
+ {<<"access_token">>, AccessToken},
+ {<<"token_type">>, Type},
+ {<<"scope">>, str:join(VerifiedScope, <<" ">>)},
+ {<<"expires_in">>, Expires}]});
+ {error, Error} when is_atom(Error) ->
+ json_error(400, <<"invalid_grant">>, Error)
+ end;
+ _OtherGrantType ->
+ json_error(400, <<"unsupported_grant_type">>, unsupported_grant_type)
+ end;
+
process(_Handlers, _Request) ->
ejabberd_web:error(not_found).
+-spec get_db_backend() -> module().
+
+get_db_backend() ->
+ DBType = ejabberd_config:get_option(
+ oauth_db_type,
+ fun(T) -> ejabberd_config:v_db(?MODULE, T) end,
+ mnesia),
+ list_to_atom("ejabberd_oauth_" ++ atom_to_list(DBType)).
+
+
+%% Headers as per RFC 6749
+json_response(Code, Body) ->
+ {Code, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>},
+ {<<"Cache-Control">>, <<"no-store">>},
+ {<<"Pragma">>, <<"no-cache">>}],
+ jiffy:encode(Body)}.
+%% OAauth error are defined in:
+%% https://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-5.2
+json_error(Code, Error, Reason) ->
+ Desc = json_error_desc(Reason),
+ Body = {[{<<"error">>, Error},
+ {<<"error_description">>, Desc}]},
+ json_response(Code, Body).
+json_error_desc(access_denied) -> <<"Access denied">>;
+json_error_desc(unsupported_grant_type) -> <<"Unsupported grant type">>;
+json_error_desc(invalid_scope) -> <<"Invalid scope">>.
web_head() ->
[?XA(<<"meta">>, [{<<"http-equiv">>, <<"X-UA-Compatible">>},
@@ -577,7 +666,7 @@ css() ->
text-decoration: underline;
}
- .container > .section {
+ .container > .section {
background: #424A55;
}
@@ -595,4 +684,6 @@ opt_type(oauth_expire) ->
fun(I) when is_integer(I), I >= 0 -> I end;
opt_type(oauth_access) ->
fun acl:access_rules_validator/1;
-opt_type(_) -> [oauth_expire, oauth_access].
+opt_type(oauth_db_type) ->
+ fun(T) -> ejabberd_config:v_db(?MODULE, T) end;
+opt_type(_) -> [oauth_expire, oauth_access, oauth_db_type].
diff --git a/src/ejabberd_oauth_mnesia.erl b/src/ejabberd_oauth_mnesia.erl
new file mode 100644
index 000000000..a23f443ed
--- /dev/null
+++ b/src/ejabberd_oauth_mnesia.erl
@@ -0,0 +1,65 @@
+%%%-------------------------------------------------------------------
+%%% File : ejabberd_oauth_mnesia.erl
+%%% Author : Alexey Shchepin <alexey@process-one.net>
+%%% Purpose : OAUTH2 mnesia backend
+%%% Created : 20 Jul 2016 by Alexey Shchepin <alexey@process-one.net>
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2002-2016 ProcessOne
+%%%
+%%% This program is free software; you can redistribute it and/or
+%%% modify it under the terms of the GNU General Public License as
+%%% published by the Free Software Foundation; either version 2 of the
+%%% License, or (at your option) any later version.
+%%%
+%%% This program is distributed in the hope that it will be useful,
+%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
+%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%%% General Public License for more details.
+%%%
+%%% You should have received a copy of the GNU General Public License
+%%% along with this program; if not, write to the Free Software
+%%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+%%% 02111-1307 USA
+%%%
+%%%-------------------------------------------------------------------
+
+-module(ejabberd_oauth_mnesia).
+
+-export([init/0,
+ store/1,
+ lookup/1,
+ clean/1]).
+
+-include("ejabberd_oauth.hrl").
+
+init() ->
+ mnesia:create_table(oauth_token,
+ [{disc_copies, [node()]},
+ {attributes,
+ record_info(fields, oauth_token)}]),
+ mnesia:add_table_copy(oauth_token, node(), disc_copies),
+ ok.
+
+store(R) ->
+ mnesia:dirty_write(R).
+
+lookup(Token) ->
+ case catch mnesia:dirty_read(oauth_token, Token) of
+ [R] ->
+ R;
+ _ ->
+ false
+ end.
+
+clean(TS) ->
+ F = fun() ->
+ Ts = mnesia:select(
+ oauth_token,
+ [{#oauth_token{expire = '$1', _ = '_'},
+ [{'<', '$1', TS}],
+ ['$_']}]),
+ lists:foreach(fun mnesia:delete_object/1, Ts)
+ end,
+ mnesia:async_dirty(F).
+
diff --git a/src/ejabberd_service.erl b/src/ejabberd_service.erl
index 465fb587a..9d72b17b4 100644
--- a/src/ejabberd_service.erl
+++ b/src/ejabberd_service.erl
@@ -224,8 +224,10 @@ wait_for_handshake({xmlstreamelement, El}, StateData) ->
fun (H) ->
ejabberd_router:register_route(H, ?MYNAME),
?INFO_MSG("Route registered for service ~p~n",
- [H])
- end, dict:fetch_keys(StateData#state.host_opts)),
+ [H]),
+ ejabberd_hooks:run(component_connected,
+ [H])
+ end, dict:fetch_keys(StateData#state.host_opts)),
{next_state, stream_established, StateData};
_ ->
send_text(StateData, ?INVALID_HANDSHAKE_ERR),
@@ -382,7 +384,9 @@ terminate(Reason, StateName, StateData) ->
case StateName of
stream_established ->
lists:foreach(fun (H) ->
- ejabberd_router:unregister_route(H)
+ ejabberd_router:unregister_route(H),
+ ejabberd_hooks:run(component_disconnected,
+ [StateData#state.host, Reason])
end,
dict:fetch_keys(StateData#state.host_opts));
_ -> ok
diff --git a/src/ejabberd_sm.erl b/src/ejabberd_sm.erl
index 8d94bc6aa..16e0f9114 100644
--- a/src/ejabberd_sm.erl
+++ b/src/ejabberd_sm.erl
@@ -270,25 +270,28 @@ get_session_pid(User, Server, Resource) ->
-spec set_offline_info(sid(), binary(), binary(), binary(), info()) -> ok.
-set_offline_info({Time, _Pid}, User, Server, Resource, Info) ->
- SID = {Time, undefined},
+set_offline_info(SID, User, Server, Resource, Info) ->
LUser = jid:nodeprep(User),
LServer = jid:nameprep(Server),
LResource = jid:resourceprep(Resource),
- set_session(SID, LUser, LServer, LResource, undefined, Info).
+ set_session(SID, LUser, LServer, LResource, undefined, [offline | Info]).
-spec get_offline_info(erlang:timestamp(), binary(), binary(),
binary()) -> none | info().
get_offline_info(Time, User, Server, Resource) ->
- SID = {Time, undefined},
LUser = jid:nodeprep(User),
LServer = jid:nameprep(Server),
LResource = jid:resourceprep(Resource),
Mod = get_sm_backend(LServer),
case Mod:get_sessions(LUser, LServer, LResource) of
- [#session{sid = SID, info = Info}] ->
- Info;
+ [#session{sid = {Time, _}, info = Info}] ->
+ case proplists:get_bool(offline, Info) of
+ true ->
+ Info;
+ false ->
+ none
+ end;
_ ->
none
end.
@@ -425,11 +428,12 @@ set_session(SID, User, Server, Resource, Priority, Info) ->
-spec online([#session{}]) -> [#session{}].
online(Sessions) ->
- lists:filter(fun(#session{sid = {_, undefined}}) ->
- false;
- (_) ->
- true
- end, Sessions).
+ lists:filter(fun is_online/1, Sessions).
+
+-spec is_online(#session{}) -> boolean().
+
+is_online(#session{info = Info}) ->
+ not proplists:get_bool(offline, Info).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
@@ -678,15 +682,17 @@ check_for_sessions_to_replace(User, Server, Resource) ->
check_max_sessions(LUser, LServer).
check_existing_resources(LUser, LServer, LResource) ->
- SIDs = get_resource_sessions(LUser, LServer, LResource),
- if SIDs == [] -> ok;
+ Mod = get_sm_backend(LServer),
+ Ss = Mod:get_sessions(LUser, LServer, LResource),
+ {OnlineSs, OfflineSs} = lists:partition(fun is_online/1, Ss),
+ lists:foreach(fun(#session{sid = S}) ->
+ Mod:delete_session(LUser, LServer, LResource, S)
+ end, OfflineSs),
+ if OnlineSs == [] -> ok;
true ->
+ SIDs = [SID || #session{sid = SID} <- OnlineSs],
MaxSID = lists:max(SIDs),
- lists:foreach(fun ({_, undefined} = S) ->
- Mod = get_sm_backend(LServer),
- Mod:delete_session(LUser, LServer, LResource,
- S);
- ({_, Pid} = S) when S /= MaxSID ->
+ lists:foreach(fun ({_, Pid} = S) when S /= MaxSID ->
Pid ! replaced;
(_) -> ok
end,
diff --git a/src/ejabberd_sql.erl b/src/ejabberd_sql.erl
index a480a1bd3..b19f16414 100644
--- a/src/ejabberd_sql.erl
+++ b/src/ejabberd_sql.erl
@@ -790,7 +790,7 @@ pgsql_connect(Server, Port, DB, Username, Password) ->
{port, Port},
{as_binary, true}]) of
{ok, Ref} ->
- pgsql:squery(Ref, [<<"alter database ">>, DB, <<" set ">>,
+ pgsql:squery(Ref, [<<"alter database \"">>, DB, <<"\" set ">>,
<<"standard_conforming_strings='off';">>]),
pgsql:squery(Ref, [<<"set standard_conforming_strings to 'off';">>]),
{ok, Ref};
diff --git a/src/ejabberd_web_admin.erl b/src/ejabberd_web_admin.erl
index 3281f6430..6583fb445 100644
--- a/src/ejabberd_web_admin.erl
+++ b/src/ejabberd_web_admin.erl
@@ -74,20 +74,27 @@ get_acl_rule([<<"vhosts">>], _) ->
%% The pages of a vhost are only accesible if the user is admin of that vhost:
get_acl_rule([<<"server">>, VHost | _RPath], Method)
when Method =:= 'GET' orelse Method =:= 'HEAD' ->
- {VHost, [configure, webadmin_view]};
+ AC = gen_mod:get_module_opt(VHost, ejabberd_web_admin,
+ access, fun(A) -> A end, configure),
+ ACR = gen_mod:get_module_opt(VHost, ejabberd_web_admin,
+ access_readonly, fun(A) -> A end, webadmin_view),
+ {VHost, [AC, ACR]};
get_acl_rule([<<"server">>, VHost | _RPath], 'POST') ->
- {VHost, [configure]};
+ AC = gen_mod:get_module_opt(VHost, ejabberd_web_admin,
+ access, fun(A) -> A end, configure),
+ {VHost, [AC]};
%% Default rule: only global admins can access any other random page
get_acl_rule(_RPath, Method)
when Method =:= 'GET' orelse Method =:= 'HEAD' ->
- {global, [configure, webadmin_view]};
-get_acl_rule(_RPath, 'POST') -> {global, [configure]}.
-
-is_acl_match(Host, Rules, Jid) ->
- lists:any(fun (Rule) ->
- allow == acl:match_rule(Host, Rule, Jid)
- end,
- Rules).
+ AC = gen_mod:get_module_opt(global, ejabberd_web_admin,
+ access, fun(A) -> A end, configure),
+ ACR = gen_mod:get_module_opt(global, ejabberd_web_admin,
+ access_readonly, fun(A) -> A end, webadmin_view),
+ {global, [AC, ACR]};
+get_acl_rule(_RPath, 'POST') ->
+ AC = gen_mod:get_module_opt(global, ejabberd_web_admin,
+ access, fun(A) -> A end, configure),
+ {global, [AC]}.
%%%==================================
%%%% Menu Items Access
@@ -138,7 +145,7 @@ is_allowed_path([<<"admin">> | Path], JID) ->
is_allowed_path(Path, JID);
is_allowed_path(Path, JID) ->
{HostOfRule, AccessRule} = get_acl_rule(Path, 'GET'),
- is_acl_match(HostOfRule, AccessRule, JID).
+ acl:any_rules_allowed(HostOfRule, AccessRule, JID).
%% @spec(Path) -> URL
%% where Path = [string()]
@@ -266,8 +273,8 @@ get_auth_account(HostOfRule, AccessRule, User, Server,
Pass) ->
case ejabberd_auth:check_password(User, <<"">>, Server, Pass) of
true ->
- case is_acl_match(HostOfRule, AccessRule,
- jid:make(User, Server, <<"">>))
+ case acl:any_rules_allowed(HostOfRule, AccessRule,
+ jid:make(User, Server, <<"">>))
of
false -> {unauthorized, <<"unprivileged-account">>};
true -> {ok, {User, Server}}
@@ -1333,7 +1340,7 @@ parse_access_rule(Text) ->
list_vhosts(Lang, JID) ->
Hosts = (?MYHOSTS),
HostsAllowed = lists:filter(fun (Host) ->
- is_acl_match(Host,
+ acl:any_rules_allowed(Host,
[configure, webadmin_view],
JID)
end,
@@ -2965,7 +2972,8 @@ make_menu_item(item, 3, URI, Name, Lang) ->
%%%==================================
-opt_type(access) -> fun (V) -> V end;
-opt_type(_) -> [access].
+opt_type(access) -> fun acl:access_rules_validator/1;
+opt_type(access_readonly) -> fun acl:access_rules_validator/1;
+opt_type(_) -> [access, access_readonly].
%%% vim: set foldmethod=marker foldmarker=%%%%,%%%=:
diff --git a/src/ejd2sql.erl b/src/ejd2sql.erl
index 2d19100a9..7bace05dd 100644
--- a/src/ejd2sql.erl
+++ b/src/ejd2sql.erl
@@ -186,7 +186,7 @@ delete(LServer, Table, ConvertFun) ->
mnesia:write_lock_table(Table),
{_N, SQLs} =
mnesia:foldl(
- fun(R, {N, SQLs} = Acc) ->
+ fun(R, Acc) ->
case ConvertFun(LServer, R) of
[] ->
Acc;
diff --git a/src/gen_mod.erl b/src/gen_mod.erl
index 1cc65ac21..c4306577c 100644
--- a/src/gen_mod.erl
+++ b/src/gen_mod.erl
@@ -53,6 +53,7 @@
-callback start(binary(), opts()) -> any().
-callback stop(binary()) -> any().
-callback mod_opt_type(atom()) -> fun((term()) -> term()) | [atom()].
+-callback depends(binary(), opts()) -> [{module(), hard | soft}].
-export_type([opts/0]).
-export_type([db_type/0]).
@@ -77,18 +78,56 @@ start_modules() ->
get_modules_options(Host) ->
ejabberd_config:get_option(
- {modules, Host},
- fun(Mods) ->
- lists:map(
+ {modules, Host},
+ fun(Mods) ->
+ lists:map(
fun({M, A}) when is_atom(M), is_list(A) ->
- {M, A}
+ {M, A}
end, Mods)
- end, []).
+ end, []).
+
+sort_modules(Host, ModOpts) ->
+ G = digraph:new([acyclic]),
+ lists:foreach(
+ fun({Mod, Opts}) ->
+ digraph:add_vertex(G, Mod, Opts),
+ Deps = try Mod:depends(Host, Opts) catch _:undef -> [] end,
+ lists:foreach(
+ fun({DepMod, Type}) ->
+ case lists:keyfind(DepMod, 1, ModOpts) of
+ false when Type == hard ->
+ ErrTxt = io_lib:format(
+ "failed to load module '~s' "
+ "because it depends on module '~s' "
+ "which is not found in the config",
+ [Mod, DepMod]),
+ ?ERROR_MSG(ErrTxt, []),
+ digraph:del_vertex(G, Mod),
+ maybe_halt_ejabberd(ErrTxt);
+ false when Type == soft ->
+ ?WARNING_MSG("module '~s' is recommended for "
+ "module '~s' but is not found in "
+ "the config",
+ [DepMod, Mod]);
+ {DepMod, DepOpts} ->
+ digraph:add_vertex(G, DepMod, DepOpts),
+ case digraph:add_edge(G, DepMod, Mod) of
+ {error, {bad_edge, Path}} ->
+ ?WARNING_MSG("cyclic dependency detected "
+ "between modules: ~p",
+ [Path]);
+ _ ->
+ ok
+ end
+ end
+ end, Deps)
+ end, ModOpts),
+ [digraph:vertex(G, V) || V <- digraph_utils:topsort(G)].
-spec start_modules(binary()) -> any().
start_modules(Host) ->
- Modules = get_modules_options(Host),
+ Modules = sort_modules(Host, get_modules_options(Host)),
lists:foreach(
fun({Module, Opts}) ->
start_module(Host, Module, Opts)
@@ -121,16 +160,20 @@ start_module(Host, Module, Opts0) ->
[Module, Host, Opts, Class, Reason,
erlang:get_stacktrace()]),
?CRITICAL_MSG(ErrorText, []),
- case is_app_running(ejabberd) of
- true ->
- erlang:raise(Class, Reason, erlang:get_stacktrace());
- false ->
- ?CRITICAL_MSG("ejabberd initialization was aborted "
- "because a module start failed.",
- []),
- timer:sleep(3000),
- erlang:halt(string:substr(lists:flatten(ErrorText), 1, 199))
- end
+ maybe_halt_ejabberd(ErrorText),
+ erlang:raise(Class, Reason, erlang:get_stacktrace())
+ end.
+
+maybe_halt_ejabberd(ErrorText) ->
+ case is_app_running(ejabberd) of
+ false ->
+ ?CRITICAL_MSG("ejabberd initialization was aborted "
+ "because a module start failed.",
+ []),
+ timer:sleep(3000),
+ erlang:halt(string:substr(lists:flatten(ErrorText), 1, 199));
+ true ->
+ ok
end.
is_app_running(AppName) ->
diff --git a/src/jid.erl b/src/jid.erl
index 0c3ac77c0..a730bd949 100644
--- a/src/jid.erl
+++ b/src/jid.erl
@@ -50,11 +50,35 @@
-spec start() -> ok.
start() ->
+ {ok, Owner} = ets_owner(),
SplitPattern = binary:compile_pattern([<<"@">>, <<"/">>]),
- catch ets:new(jlib, [named_table, protected, set, {keypos, 1}]),
+ %% Table is public to allow ETS insert to fix / update the table even if table already exist
+ %% with another owner.
+ catch ets:new(jlib, [named_table, public, set, {keypos, 1}, {heir, Owner, undefined}]),
ets:insert(jlib, {string_to_jid_pattern, SplitPattern}),
ok.
+ets_owner() ->
+ case whereis(jlib_ets) of
+ undefined ->
+ Pid = spawn(fun() -> ets_keepalive() end),
+ case catch register(jlib_ets, Pid) of
+ true ->
+ {ok, Pid};
+ Error -> Error
+ end;
+ Pid ->
+ {ok,Pid}
+ end.
+
+%% Process used to keep jlib ETS table alive in case the original owner dies.
+%% The table need to be public, otherwise subsequent inserts would fail.
+ets_keepalive() ->
+ receive
+ _ ->
+ ets_keepalive()
+ end.
+
-spec make(binary(), binary(), binary()) -> jid() | error.
make(User, Server, Resource) ->
diff --git a/src/jlib.erl b/src/jlib.erl
index 532a74610..4bc9b0055 100644
--- a/src/jlib.erl
+++ b/src/jlib.erl
@@ -577,33 +577,8 @@ add_delay_info(El, From, Time) ->
binary()) -> xmlel().
add_delay_info(El, From, Time, Desc) ->
- case fxml:get_subtag_with_xmlns(El, <<"delay">>, ?NS_DELAY) of
- false ->
- %% Add new tag
- DelayTag = create_delay_tag(Time, From, Desc),
- fxml:append_subtags(El, [DelayTag]);
- DelayTag ->
- %% Update existing tag
- NewDelayTag =
- case {fxml:get_tag_cdata(DelayTag), Desc} of
- {<<"">>, <<"">>} ->
- DelayTag;
- {OldDesc, <<"">>} ->
- DelayTag#xmlel{children = [{xmlcdata, OldDesc}]};
- {<<"">>, NewDesc} ->
- DelayTag#xmlel{children = [{xmlcdata, NewDesc}]};
- {OldDesc, NewDesc} ->
- case binary:match(OldDesc, NewDesc) of
- nomatch ->
- FinalDesc = <<OldDesc/binary, ", ", NewDesc/binary>>,
- DelayTag#xmlel{children = [{xmlcdata, FinalDesc}]};
- _ ->
- DelayTag#xmlel{children = [{xmlcdata, OldDesc}]}
- end
- end,
- NewEl = fxml:remove_subtags(El, <<"delay">>, {<<"xmlns">>, ?NS_DELAY}),
- fxml:append_subtags(NewEl, [NewDelayTag])
- end.
+ DelayTag = create_delay_tag(Time, From, Desc),
+ fxml:append_subtags(El, [DelayTag]).
-spec create_delay_tag(erlang:timestamp(), jid() | ljid() | binary(), binary())
-> xmlel() | error.
diff --git a/src/mod_adhoc.erl b/src/mod_adhoc.erl
index 9e0682f7d..e12e0de1e 100644
--- a/src/mod_adhoc.erl
+++ b/src/mod_adhoc.erl
@@ -35,7 +35,7 @@
process_sm_iq/3, get_local_commands/5,
get_local_identity/5, get_local_features/5,
get_sm_commands/5, get_sm_identity/5, get_sm_features/5,
- ping_item/4, ping_command/4, mod_opt_type/1]).
+ ping_item/4, ping_command/4, mod_opt_type/1, depends/2]).
-include("ejabberd.hrl").
-include("logger.hrl").
@@ -284,6 +284,9 @@ ping_command(_Acc, _From, _To,
end;
ping_command(Acc, _From, _To, _Request) -> Acc.
+depends(_Host, _Opts) ->
+ [].
+
mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1;
mod_opt_type(report_commands_node) ->
fun (B) when is_boolean(B) -> B end;
diff --git a/src/mod_admin_extra.erl b/src/mod_admin_extra.erl
index 562087d96..2ad1cc28e 100644
--- a/src/mod_admin_extra.erl
+++ b/src/mod_admin_extra.erl
@@ -47,7 +47,7 @@
srg_delete/2, srg_list/1, srg_get_info/2,
srg_get_members/2, srg_user_add/4, srg_user_del/4,
send_message/5, send_stanza/3, send_stanza_c2s/4, privacy_set/3,
- stats/1, stats/2, mod_opt_type/1, get_commands_spec/0]).
+ stats/1, stats/2, mod_opt_type/1, get_commands_spec/0, depends/2]).
-include("ejabberd.hrl").
@@ -66,6 +66,8 @@ start(_Host, _Opts) ->
stop(_Host) ->
ejabberd_commands:unregister_commands(get_commands_spec()).
+depends(_Host, _Opts) ->
+ [].
%%%
%%% Register commands
@@ -861,12 +863,15 @@ connected_users_vhost(Host) ->
%% Code copied from ejabberd_sm.erl and customized
dirty_get_sessions_list2() ->
- mnesia:dirty_select(
- session,
- [{#session{usr = '$1', sid = {'$2', '$3'}, priority = '$4', info = '$5',
- _ = '_'},
- [{is_pid, '$3'}],
- [['$1', {{'$2', '$3'}}, '$4', '$5']]}]).
+ Ss = mnesia:dirty_select(
+ session,
+ [{#session{usr = '$1', sid = '$2', priority = '$3', info = '$4',
+ _ = '_'},
+ [],
+ [['$1', '$2', '$3', '$4']]}]),
+ lists:filter(fun([_USR, _SID, _Priority, Info]) ->
+ not proplists:get_bool(offline, Info)
+ end, Ss).
%% Make string more print-friendly
stringize(String) ->
@@ -901,8 +906,8 @@ user_sessions_info(User, Host) ->
{'EXIT', _Reason} ->
[];
Ss ->
- lists:filter(fun(#session{sid = {_, Pid}}) ->
- is_pid(Pid)
+ lists:filter(fun(#session{info = Info}) ->
+ not proplists:get_bool(offline, Info)
end, Ss)
end,
lists:map(
diff --git a/src/mod_announce.erl b/src/mod_announce.erl
index fc57d6bd1..52ff2de92 100644
--- a/src/mod_announce.erl
+++ b/src/mod_announce.erl
@@ -33,7 +33,7 @@
-export([start/2, init/0, stop/1, export/1, import/1,
import/3, announce/3, send_motd/1, disco_identity/5,
- disco_features/5, disco_items/5,
+ disco_features/5, disco_items/5, depends/2,
send_announcement_to_all/3, announce_commands/4,
announce_items/4, mod_opt_type/1]).
@@ -74,6 +74,9 @@ start(Host, Opts) ->
register(gen_mod:get_module_proc(Host, ?PROCNAME),
proc_lib:spawn(?MODULE, init, [])).
+depends(_Host, _Opts) ->
+ [{mod_adhoc, hard}].
+
init() ->
loop().
diff --git a/src/mod_blocking.erl b/src/mod_blocking.erl
index af06e650d..818d53259 100644
--- a/src/mod_blocking.erl
+++ b/src/mod_blocking.erl
@@ -30,7 +30,7 @@
-protocol({xep, 191, '1.2'}).
-export([start/2, stop/1, process_iq/3,
- process_iq_set/4, process_iq_get/5, mod_opt_type/1]).
+ process_iq_set/4, process_iq_get/5, mod_opt_type/1, depends/2]).
-include("ejabberd.hrl").
-include("logger.hrl").
@@ -63,6 +63,9 @@ stop(Host) ->
gen_iq_handler:remove_iq_handler(ejabberd_sm, Host,
?NS_BLOCKING).
+depends(_Host, _Opts) ->
+ [{mod_privacy, hard}].
+
process_iq(_From, _To, IQ) ->
SubEl = IQ#iq.sub_el,
IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}.
diff --git a/src/mod_caps.erl b/src/mod_caps.erl
index 966a9baa6..3ed3149bb 100644
--- a/src/mod_caps.erl
+++ b/src/mod_caps.erl
@@ -41,7 +41,7 @@
import_start/2, import_stop/2]).
%% gen_mod callbacks
--export([start/2, start_link/2, stop/1]).
+-export([start/2, start_link/2, stop/1, depends/2]).
%% gen_server callbacks
-export([init/1, handle_info/2, handle_call/3,
@@ -306,6 +306,9 @@ c2s_broadcast_recipients(InAcc, Host, C2SState,
end;
c2s_broadcast_recipients(Acc, _, _, _, _, _) -> Acc.
+depends(_Host, _Opts) ->
+ [].
+
init([Host, Opts]) ->
Mod = gen_mod:db_mod(Host, Opts, ?MODULE),
Mod:init(Host, Opts),
diff --git a/src/mod_carboncopy.erl b/src/mod_carboncopy.erl
index bb20bd2f9..de8d8e1a7 100644
--- a/src/mod_carboncopy.erl
+++ b/src/mod_carboncopy.erl
@@ -37,7 +37,7 @@
-export([user_send_packet/4, user_receive_packet/5,
iq_handler2/3, iq_handler1/3, remove_connection/4,
- is_carbon_copy/1, mod_opt_type/1]).
+ is_carbon_copy/1, mod_opt_type/1, depends/2]).
-include("ejabberd.hrl").
-include("logger.hrl").
@@ -278,6 +278,9 @@ list(User, Server) ->
Mod = gen_mod:db_mod(Server, ?MODULE),
Mod:list(User, Server).
+depends(_Host, _Opts) ->
+ [].
+
mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1;
mod_opt_type(db_type) -> fun(T) -> ejabberd_config:v_db(?MODULE, T) end;
mod_opt_type(_) -> [db_type, iqdisc].
diff --git a/src/mod_client_state.erl b/src/mod_client_state.erl
index fe66f34e2..036175cce 100644
--- a/src/mod_client_state.erl
+++ b/src/mod_client_state.erl
@@ -31,7 +31,7 @@
-behavior(gen_mod).
%% gen_mod callbacks.
--export([start/2, stop/1, mod_opt_type/1]).
+-export([start/2, stop/1, mod_opt_type/1, depends/2]).
%% ejabberd_hooks callbacks.
-export([filter_presence/3, filter_chat_states/3, filter_pep/3, filter_other/3,
@@ -142,6 +142,11 @@ mod_opt_type(queue_pep) ->
fun(B) when is_boolean(B) -> B end;
mod_opt_type(_) -> [queue_presence, queue_chat_states, queue_pep].
+-spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}].
+
+depends(_Host, _Opts) ->
+ [].
+
%%--------------------------------------------------------------------
%% ejabberd_hooks callbacks.
%%--------------------------------------------------------------------
diff --git a/src/mod_configure.erl b/src/mod_configure.erl
index 9d4086559..d0e0166a4 100644
--- a/src/mod_configure.erl
+++ b/src/mod_configure.erl
@@ -35,7 +35,8 @@
get_local_features/5, get_local_items/5,
adhoc_local_items/4, adhoc_local_commands/4,
get_sm_identity/5, get_sm_features/5, get_sm_items/5,
- adhoc_sm_items/4, adhoc_sm_commands/4, mod_opt_type/1]).
+ adhoc_sm_items/4, adhoc_sm_commands/4, mod_opt_type/1,
+ depends/2]).
-include("ejabberd.hrl").
-include("logger.hrl").
@@ -95,6 +96,9 @@ stop(Host) ->
gen_iq_handler:remove_iq_handler(ejabberd_sm, Host,
?NS_COMMANDS).
+depends(_Host, _Opts) ->
+ [{mod_adhoc, hard}, {mod_last, soft}].
+
%%%-----------------------------------------------------------------------
-define(INFO_IDENTITY(Category, Type, Name, Lang),
@@ -1913,21 +1917,29 @@ set_form(From, Host, ?NS_ADMINL(<<"end-user-session">>),
Xmlelement = ?SERRT_POLICY_VIOLATION(Lang, <<"has been kicked">>),
case JID#jid.lresource of
<<>> ->
- SIDs = mnesia:dirty_select(session,
- [{#session{sid = {'$1', '$2'},
- usr = {LUser, LServer, '_'},
- _ = '_'},
- [{is_pid, '$2'}],
- [{{'$1', '$2'}}]}]),
- [Pid ! {kick, kicked_by_admin, Xmlelement} || {_, Pid} <- SIDs];
+ SIs = mnesia:dirty_select(session,
+ [{#session{usr = {LUser, LServer, '_'},
+ sid = '$1',
+ info = '$2',
+ _ = '_'},
+ [], [{{'$1', '$2'}}]}]),
+ Pids = [P || {{_, P}, Info} <- SIs,
+ not proplists:get_bool(offline, Info)],
+ lists:foreach(fun(Pid) ->
+ Pid ! {kick, kicked_by_admin, Xmlelement}
+ end, Pids);
R ->
- [{_, Pid}] = mnesia:dirty_select(session,
- [{#session{sid = {'$1', '$2'},
- usr = {LUser, LServer, R},
- _ = '_'},
- [{is_pid, '$2'}],
- [{{'$1', '$2'}}]}]),
- Pid ! {kick, kicked_by_admin, Xmlelement}
+ [{{_, Pid}, Info}] = mnesia:dirty_select(
+ session,
+ [{#session{usr = {LUser, LServer, R},
+ sid = '$1',
+ info = '$2',
+ _ = '_'},
+ [], [{{'$1', '$2'}}]}]),
+ case proplists:get_bool(offline, Info) of
+ true -> ok;
+ false -> Pid ! {kick, kicked_by_admin, Xmlelement}
+ end
end,
{result, []};
set_form(From, Host,
diff --git a/src/mod_configure2.erl b/src/mod_configure2.erl
index a8287b4d4..85b7740d0 100644
--- a/src/mod_configure2.erl
+++ b/src/mod_configure2.erl
@@ -32,7 +32,7 @@
-behaviour(gen_mod).
-export([start/2, stop/1, process_local_iq/3,
- mod_opt_type/1, opt_type/1]).
+ mod_opt_type/1, opt_type/1, depends/2]).
-include("ejabberd.hrl").
-include("logger.hrl").
@@ -201,6 +201,9 @@ process_get(#xmlel{name = <<"last">>, attrs = Attrs}, Lang) ->
%% {result, };
process_get(_, _) -> {error, ?ERR_BAD_REQUEST}.
+depends(_Host, _Opts) ->
+ [].
+
mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1;
mod_opt_type(_) -> [iqdisc].
diff --git a/src/mod_disco.erl b/src/mod_disco.erl
index 0d5abcb4b..2e7b80c18 100644
--- a/src/mod_disco.erl
+++ b/src/mod_disco.erl
@@ -39,7 +39,7 @@
get_sm_identity/5, get_sm_features/5, get_sm_items/5,
get_info/5, register_feature/2, unregister_feature/2,
register_extra_domain/2, unregister_extra_domain/2,
- transform_module_options/1, mod_opt_type/1]).
+ transform_module_options/1, mod_opt_type/1, depends/2]).
-include("ejabberd.hrl").
-include("logger.hrl").
@@ -534,6 +534,9 @@ values_to_xml(Values) ->
end,
Values).
+depends(_Host, _Opts) ->
+ [].
+
mod_opt_type(extra_domains) ->
fun (Hs) -> [iolist_to_binary(H) || H <- Hs] end;
mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1;
diff --git a/src/mod_echo.erl b/src/mod_echo.erl
index 7d9f81f82..ee904d798 100644
--- a/src/mod_echo.erl
+++ b/src/mod_echo.erl
@@ -37,7 +37,7 @@
-export([init/1, handle_call/3, handle_cast/2,
handle_info/2, terminate/2, code_change/3,
- mod_opt_type/1]).
+ mod_opt_type/1, depends/2]).
-include("ejabberd.hrl").
-include("logger.hrl").
@@ -200,5 +200,8 @@ do_client_version(enabled, From, To) ->
?INFO_MSG("Information of the client: ~s~s",
[ToS, Values_string2]).
+depends(_Host, _Opts) ->
+ [].
+
mod_opt_type(host) -> fun iolist_to_binary/1;
mod_opt_type(_) -> [host].
diff --git a/src/mod_fail2ban.erl b/src/mod_fail2ban.erl
index f0460e704..c57ac21b0 100644
--- a/src/mod_fail2ban.erl
+++ b/src/mod_fail2ban.erl
@@ -33,7 +33,7 @@
-export([init/1, handle_call/3, handle_cast/2,
handle_info/2, terminate/2, code_change/3,
- mod_opt_type/1]).
+ mod_opt_type/1, depends/2]).
-include_lib("stdlib/include/ms_transform.hrl").
-include("ejabberd.hrl").
@@ -120,6 +120,9 @@ stop(Host) ->
supervisor:terminate_child(ejabberd_sup, Proc),
supervisor:delete_child(ejabberd_sup, Proc).
+depends(_Host, _Opts) ->
+ [].
+
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl
index 595c121cd..ba3a14cf8 100644
--- a/src/mod_http_api.erl
+++ b/src/mod_http_api.erl
@@ -74,7 +74,7 @@
-behaviour(gen_mod).
--export([start/2, stop/1, process/2, mod_opt_type/1]).
+-export([start/2, stop/1, process/2, mod_opt_type/1, depends/2]).
-include("ejabberd.hrl").
-include("jlib.hrl").
@@ -123,6 +123,9 @@ start(_Host, _Opts) ->
stop(_Host) ->
ok.
+depends(_Host, _Opts) ->
+ [].
+
%% ----------
%% basic auth
%% ----------
@@ -130,13 +133,13 @@ stop(_Host) ->
check_permissions(Request, Command) ->
case catch binary_to_existing_atom(Command, utf8) of
Call when is_atom(Call) ->
- {ok, CommandPolicy} = ejabberd_commands:get_command_policy(Call),
- check_permissions2(Request, Call, CommandPolicy);
+ {ok, CommandPolicy, Scope} = ejabberd_commands:get_command_policy_and_scope(Call),
+ check_permissions2(Request, Call, CommandPolicy, Scope);
_ ->
- unauthorized_response()
+ json_error(404, 40, <<"Endpoint not found.">>)
end.
-check_permissions2(#request{auth = HTTPAuth, headers = Headers}, Call, _)
+check_permissions2(#request{auth = HTTPAuth, headers = Headers}, Call, _, ScopeList)
when HTTPAuth /= undefined ->
Admin =
case lists:keysearch(<<"X-Admin">>, 1, Headers) of
@@ -156,11 +159,9 @@ check_permissions2(#request{auth = HTTPAuth, headers = Headers}, Call, _)
false
end;
{oauth, Token, _} ->
- case oauth_check_token(Call, Token) of
+ case oauth_check_token(ScopeList, Token) of
{ok, user, {User, Server}} ->
{ok, {User, Server, {oauth, Token}, Admin}};
- {ok, server_admin} -> %% token whas generated using issue_token command line
- {ok, admin};
false ->
false
end;
@@ -171,9 +172,9 @@ check_permissions2(#request{auth = HTTPAuth, headers = Headers}, Call, _)
{ok, A} -> {allowed, Call, A};
_ -> unauthorized_response()
end;
-check_permissions2(_Request, Call, open) ->
+check_permissions2(_Request, Call, open, _Scope) ->
{allowed, Call, noauth};
-check_permissions2(#request{ip={IP, _Port}}, Call, _Policy) ->
+check_permissions2(#request{ip={IP, _Port}}, Call, _Policy, _Scope) ->
Access = gen_mod:get_module_opt(global, ?MODULE, admin_ip_access,
fun(V) -> V end,
none),
@@ -193,13 +194,11 @@ check_permissions2(#request{ip={IP, _Port}}, Call, _Policy) ->
_E ->
{allowed, Call, noauth}
end;
-check_permissions2(_Request, _Call, _Policy) ->
+check_permissions2(_Request, _Call, _Policy, _Scope) ->
unauthorized_response().
-oauth_check_token(Scope, Token) when is_atom(Scope) ->
- oauth_check_token(atom_to_binary(Scope, utf8), Token);
-oauth_check_token(Scope, Token) ->
- ejabberd_oauth:check_token(Scope, Token).
+oauth_check_token(ScopeList, Token) when is_list(ScopeList) ->
+ ejabberd_oauth:check_token(ScopeList, Token).
%% ------------------
%% command processing
@@ -221,8 +220,12 @@ process([Call], #request{method = 'POST', data = Data, ip = {IP, _} = IPPort} =
log(Call, Args, IPPort),
case check_permissions(Req, Call) of
{allowed, Cmd, Auth} ->
- {Code, Result} = handle(Cmd, Auth, Args, Version, IP),
- json_response(Code, jiffy:encode(Result));
+ case handle(Cmd, Auth, Args, Version, IP) of
+ {Code, Result} ->
+ json_response(Code, jiffy:encode(Result));
+ {HTMLCode, JSONErrorCode, Message} ->
+ json_error(HTMLCode, JSONErrorCode, Message)
+ end;
%% Warning: check_permission direcly formats 401 reply if not authorized
ErrorResponse ->
ErrorResponse
@@ -265,10 +268,10 @@ get_api_version(#request{path = Path}) ->
get_api_version(lists:reverse(Path));
get_api_version([<<"v", String/binary>> | Tail]) ->
case catch jlib:binary_to_integer(String) of
- N when is_integer(N) ->
- N;
- _ ->
- get_api_version(Tail)
+ N when is_integer(N) ->
+ N;
+ _ ->
+ get_api_version(Tail)
end;
get_api_version([_Head | Tail]) ->
get_api_version(Tail);
@@ -279,6 +282,8 @@ get_api_version([]) ->
%% command handlers
%% ----------------
+%% TODO Check accept types of request before decided format of reply.
+
% generic ejabberd command handler
handle(Call, Auth, Args, Version, IP) when is_atom(Call), is_list(Args) ->
case ejabberd_commands:get_command_format(Call, Auth, Version) of
@@ -310,8 +315,10 @@ handle(Call, Auth, Args, Version, IP) when is_atom(Call), is_list(Args) ->
{401, jlib:atom_to_binary(Why)};
throw:{not_allowed, Msg} ->
{401, iolist_to_binary(Msg)};
- throw:{error, account_unprivileged} ->
- {401, iolist_to_binary(<<"Unauthorized: Account Unpriviledged">>)};
+ throw:{error, account_unprivileged} ->
+ {403, 31, <<"Command need to be run with admin priviledge.">>};
+ throw:{error, access_rules_unauthorized} ->
+ {403, 32, <<"AccessRules: Account associated to token does not have the right to perform the operation.">>};
throw:{invalid_parameter, Msg} ->
{400, iolist_to_binary(Msg)};
throw:{error, Why} when is_atom(Why) ->
@@ -367,28 +374,33 @@ format_args(Args, ArgsFormat) ->
L when is_list(L) -> exit({additional_unused_args, L})
end.
-format_arg({array, Elements},
- {list, {ElementDefName, ElementDefFormat}})
- when is_list(Elements) ->
- lists:map(fun ({struct, [{ElementName, ElementValue}]}) when
- ElementDefName == ElementName ->
- format_arg(ElementValue, ElementDefFormat)
- end,
- Elements);
-format_arg({array, [{struct, Elements}]},
- {list, {ElementDefName, ElementDefFormat}})
+format_arg(Elements,
+ {list, {_ElementDefName, ElementDefFormat}})
when is_list(Elements) ->
- lists:map(fun ({ElementName, ElementValue}) ->
- true = ElementDefName == ElementName,
- format_arg(ElementValue, ElementDefFormat)
- end,
- Elements);
-format_arg({array, [{struct, Elements}]},
+ [format_arg(Element, ElementDefFormat)
+ || Element <- Elements];
+format_arg({[{Name, Value}]},
+ {tuple, [{_Tuple1N, Tuple1S}, {_Tuple2N, Tuple2S}]})
+ when Tuple1S == binary;
+ Tuple1S == string ->
+ {format_arg(Name, Tuple1S), format_arg(Value, Tuple2S)};
+format_arg({Elements},
{tuple, ElementsDef})
when is_list(Elements) ->
- FormattedList = format_args(Elements, ElementsDef),
- list_to_tuple(FormattedList);
-format_arg({array, Elements}, {list, ElementsDef})
+ F = lists:map(fun({TElName, TElDef}) ->
+ case lists:keyfind(atom_to_binary(TElName, latin1), 1, Elements) of
+ {_, Value} ->
+ format_arg(Value, TElDef);
+ _ when TElDef == binary; TElDef == string ->
+ <<"">>;
+ _ ->
+ ?ERROR_MSG("missing field ~p in tuple ~p", [TElName, Elements]),
+ throw({invalid_parameter,
+ io_lib:format("Missing field ~w in tuple ~w", [TElName, Elements])})
+ end
+ end, ElementsDef),
+ list_to_tuple(F);
+format_arg(Elements, {list, ElementsDef})
when is_list(Elements) and is_atom(ElementsDef) ->
[format_arg(Element, ElementsDef)
|| Element <- Elements];
@@ -402,7 +414,7 @@ format_arg(undefined, string) -> <<>>;
format_arg(Arg, Format) ->
?ERROR_MSG("don't know how to format Arg ~p for format ~p", [Arg, Format]),
throw({invalid_parameter,
- io_lib:format("Arg ~p is not in format ~p",
+ io_lib:format("Arg ~w is not in format ~w",
[Arg, Format])}).
process_unicode_codepoints(Str) ->
@@ -486,9 +498,7 @@ format_result(404, {_Name, _}) ->
"not_found".
unauthorized_response() ->
- unauthorized_response(<<"401 Unauthorized">>).
-unauthorized_response(Body) ->
- json_response(401, jiffy:encode(Body)).
+ json_error(401, 10, <<"Oauth Token is invalid or expired.">>).
badrequest_response() ->
badrequest_response(<<"400 Bad Request">>).
@@ -498,6 +508,15 @@ badrequest_response(Body) ->
json_response(Code, Body) when is_integer(Code) ->
{Code, ?HEADER(?CT_JSON), Body}.
+%% HTTPCode, JSONCode = integers
+%% message is binary
+json_error(HTTPCode, JSONCode, Message) ->
+ {HTTPCode, ?HEADER(?CT_JSON),
+ jiffy:encode({[{<<"status">>, <<"error">>},
+ {<<"code">>, JSONCode},
+ {<<"message">>, Message}]})
+ }.
+
log(Call, Args, {Addr, Port}) ->
AddrS = jlib:ip_to_list({Addr, Port}),
?INFO_MSG("API call ~s ~p from ~s:~p", [Call, Args, AddrS, Port]);
diff --git a/src/mod_http_bind.erl b/src/mod_http_bind.erl
index 1a07867e0..9a3a379f7 100644
--- a/src/mod_http_bind.erl
+++ b/src/mod_http_bind.erl
@@ -37,7 +37,7 @@
-behaviour(gen_mod).
--export([start/2, stop/1, process/2, mod_opt_type/1]).
+-export([start/2, stop/1, process/2, mod_opt_type/1, depends/2]).
-include("ejabberd.hrl").
-include("logger.hrl").
@@ -109,6 +109,8 @@ mod_opt_type(max_pause) ->
fun (I) when is_integer(I), I > 0 -> I end;
mod_opt_type(_) -> [max_inactivity, max_pause].
+depends(_Host, _Opts) ->
+ [].
%%%----------------------------------------------------------------------
%%% Help Web Page
diff --git a/src/mod_http_fileserver.erl b/src/mod_http_fileserver.erl
index 346dc41c8..37e02edd8 100644
--- a/src/mod_http_fileserver.erl
+++ b/src/mod_http_fileserver.erl
@@ -46,7 +46,7 @@
%% utility for other http modules
-export([content_type/3]).
--export([reopen_log/1, mod_opt_type/1]).
+-export([reopen_log/1, mod_opt_type/1, depends/2]).
-include("ejabberd.hrl").
-include("logger.hrl").
@@ -109,6 +109,9 @@ stop(Host) ->
supervisor:terminate_child(ejabberd_sup, Proc),
supervisor:delete_child(ejabberd_sup, Proc).
+depends(_Host, _Opts) ->
+ [].
+
%%====================================================================
%% API
%%====================================================================
diff --git a/src/mod_http_upload.erl b/src/mod_http_upload.erl
index a2aa75465..b166f2b66 100644
--- a/src/mod_http_upload.erl
+++ b/src/mod_http_upload.erl
@@ -68,6 +68,7 @@
-export([start_link/3,
start/2,
stop/1,
+ depends/2,
mod_opt_type/1]).
%% gen_server callbacks.
@@ -222,6 +223,11 @@ mod_opt_type(_) ->
dir_mode, docroot, put_url, get_url, service_url, custom_headers,
rm_on_unregister, thumbnail].
+-spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}].
+
+depends(_Host, _Opts) ->
+ [].
+
%%--------------------------------------------------------------------
%% gen_server callbacks.
%%--------------------------------------------------------------------
diff --git a/src/mod_http_upload_quota.erl b/src/mod_http_upload_quota.erl
index e08cd0b91..3051a4e87 100644
--- a/src/mod_http_upload_quota.erl
+++ b/src/mod_http_upload_quota.erl
@@ -39,6 +39,7 @@
-export([start_link/3,
start/2,
stop/1,
+ depends/2,
mod_opt_type/1]).
%% gen_server callbacks.
@@ -109,6 +110,11 @@ mod_opt_type(max_days) ->
mod_opt_type(_) ->
[access_soft_quota, access_hard_quota, max_days].
+-spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}].
+
+depends(_Host, _Opts) ->
+ [{mod_http_upload, hard}].
+
%%--------------------------------------------------------------------
%% gen_server callbacks.
%%--------------------------------------------------------------------
@@ -245,7 +251,7 @@ terminate(Reason, #state{server_host = ServerHost, timers = Timers}) ->
?DEBUG("Stopping upload quota process for ~s: ~p", [ServerHost, Reason]),
ejabberd_hooks:delete(http_upload_slot_request, ServerHost, ?MODULE,
handle_slot_request, 50),
- lists:foreach(fun(Timer) -> timer:cancel(Timer) end, Timers).
+ lists:foreach(fun timer:cancel/1, Timers).
-spec code_change({down, _} | _, state(), _) -> {ok, state()}.
@@ -293,7 +299,7 @@ enforce_quota(UserDir, SlotSize, _OldSize, MinSize, MaxSize) ->
{[Path | AccFiles], AccSize + Size, NewSize}
end, {[], 0, 0}, Files),
if OldSize + SlotSize > MaxSize ->
- lists:foreach(fun(File) -> del_file_and_dir(File) end, DelFiles),
+ lists:foreach(fun del_file_and_dir/1, DelFiles),
file:del_dir(UserDir), % In case it's empty, now.
NewSize + SlotSize;
true ->
@@ -308,7 +314,7 @@ delete_old_files(UserDir, CutOff) ->
[] ->
ok;
OldFiles ->
- lists:foreach(fun(File) -> del_file_and_dir(File) end, OldFiles),
+ lists:foreach(fun del_file_and_dir/1, OldFiles),
file:del_dir(UserDir) % In case it's empty, now.
end.
diff --git a/src/mod_ip_blacklist.erl b/src/mod_ip_blacklist.erl
index a6f9f619d..4f54ecd79 100644
--- a/src/mod_ip_blacklist.erl
+++ b/src/mod_ip_blacklist.erl
@@ -36,7 +36,7 @@
-export([update_bl_c2s/0]).
--export([is_ip_in_c2s_blacklist/3, mod_opt_type/1]).
+-export([is_ip_in_c2s_blacklist/3, mod_opt_type/1, depends/2]).
-include("ejabberd.hrl").
-include("logger.hrl").
@@ -65,6 +65,9 @@ preinit(Parent, State) ->
error:_ -> Parent ! {ok, Pid, true}
end.
+depends(_Host, _Opts) ->
+ [].
+
%% TODO:
stop(_Host) -> ok.
diff --git a/src/mod_irc.erl b/src/mod_irc.erl
index 4bcf69c3b..2206028b7 100644
--- a/src/mod_irc.erl
+++ b/src/mod_irc.erl
@@ -38,7 +38,7 @@
-export([init/1, handle_call/3, handle_cast/2,
handle_info/2, terminate/2, code_change/3,
- mod_opt_type/1]).
+ mod_opt_type/1, depends/2]).
-include("ejabberd.hrl").
-include("logger.hrl").
@@ -99,6 +99,9 @@ stop(Host) ->
gen_server:call(Proc, stop),
supervisor:delete_child(ejabberd_sup, Proc).
+depends(_Host, _Opts) ->
+ [].
+
%%====================================================================
%% gen_server callbacks
%%====================================================================
diff --git a/src/mod_last.erl b/src/mod_last.erl
index 92694cf13..ce9148841 100644
--- a/src/mod_last.erl
+++ b/src/mod_last.erl
@@ -37,7 +37,7 @@
process_sm_iq/3, on_presence_update/4, import/1,
import/3, store_last_info/4, get_last_info/2,
remove_user/2, transform_options/1, mod_opt_type/1,
- opt_type/1, register_user/2]).
+ opt_type/1, register_user/2, depends/2]).
-include("ejabberd.hrl").
-include("logger.hrl").
@@ -255,6 +255,9 @@ transform_options({node_start, {_, _, _} = Now}, Opts) ->
transform_options(Opt, Opts) ->
[Opt|Opts].
+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].
diff --git a/src/mod_mam.erl b/src/mod_mam.erl
index 18bffd909..7e1460695 100644
--- a/src/mod_mam.erl
+++ b/src/mod_mam.erl
@@ -31,13 +31,13 @@
-behaviour(gen_mod).
%% API
--export([start/2, stop/1]).
+-export([start/2, stop/1, depends/2]).
-export([user_send_packet/4, user_send_packet_strip_tag/4, user_receive_packet/5,
process_iq_v0_2/3, process_iq_v0_3/3, disco_sm_features/5,
remove_user/2, remove_room/3, mod_opt_type/1, muc_process_iq/4,
muc_filter_message/5, message_is_archived/5, delete_old_messages/2,
- get_commands_spec/0, msg_to_el/4]).
+ get_commands_spec/0, msg_to_el/4, get_room_config/4, set_room_option/4]).
-include("jlib.hrl").
-include("logger.hrl").
@@ -102,6 +102,12 @@ start(Host, Opts) ->
disco_sm_features, 50),
ejabberd_hooks:add(remove_user, Host, ?MODULE,
remove_user, 50),
+ ejabberd_hooks:add(remove_room, Host, ?MODULE,
+ remove_room, 50),
+ ejabberd_hooks:add(get_room_config, Host, ?MODULE,
+ get_room_config, 50),
+ ejabberd_hooks:add(set_room_option, Host, ?MODULE,
+ set_room_option, 50),
ejabberd_hooks:add(anonymous_purge_hook, Host, ?MODULE,
remove_user, 50),
case gen_mod:get_opt(assume_mam_usage, Opts,
@@ -149,6 +155,12 @@ stop(Host) ->
disco_sm_features, 50),
ejabberd_hooks:delete(remove_user, Host, ?MODULE,
remove_user, 50),
+ ejabberd_hooks:delete(remove_room, Host, ?MODULE,
+ remove_room, 50),
+ ejabberd_hooks:delete(get_room_config, Host, ?MODULE,
+ get_room_config, 50),
+ ejabberd_hooks:delete(set_room_option, Host, ?MODULE,
+ set_room_option, 50),
ejabberd_hooks:delete(anonymous_purge_hook, Host,
?MODULE, remove_user, 50),
case gen_mod:get_module_opt(Host, ?MODULE, assume_mam_usage,
@@ -165,6 +177,9 @@ stop(Host) ->
ejabberd_commands:unregister_commands(get_commands_spec()),
ok.
+depends(_Host, _Opts) ->
+ [].
+
remove_user(User, Server) ->
LUser = jid:nodeprep(User),
LServer = jid:nameprep(Server),
@@ -177,6 +192,41 @@ remove_room(LServer, Name, Host) ->
Mod = gen_mod:db_mod(LServer, ?MODULE),
Mod:remove_room(LServer, LName, LHost).
+get_room_config(X, RoomState, _From, Lang) ->
+ Config = RoomState#state.config,
+ Label = <<"Enable message archiving">>,
+ Var = <<"muc#roomconfig_mam">>,
+ Val = case Config#config.mam of
+ true -> <<"1">>;
+ _ -> <<"0">>
+ end,
+ XField = #xmlel{name = <<"field">>,
+ attrs =
+ [{<<"type">>, <<"boolean">>},
+ {<<"label">>, translate:translate(Lang, Label)},
+ {<<"var">>, Var}],
+ children =
+ [#xmlel{name = <<"value">>, attrs = [],
+ children = [{xmlcdata, Val}]}]},
+ X ++ [XField].
+
+set_room_option(_Acc, <<"muc#roomconfig_mam">> = Opt, Vals, Lang) ->
+ try
+ Val = case Vals of
+ [<<"0">>|_] -> false;
+ [<<"false">>|_] -> false;
+ [<<"1">>|_] -> true;
+ [<<"true">>|_] -> true
+ end,
+ {#config.mam, Val}
+ catch _:{case_clause, _} ->
+ Txt = <<"Value of '~s' should be boolean">>,
+ ErrTxt = iolist_to_binary(io_lib:format(Txt, [Opt])),
+ {error, ?ERRT_BAD_REQUEST(Lang, ErrTxt)}
+ end;
+set_room_option(Acc, _Opt, _Vals, _Lang) ->
+ Acc.
+
user_receive_packet(Pkt, C2SState, JID, Peer, To) ->
LUser = JID#jid.luser,
LServer = JID#jid.lserver,
@@ -982,6 +1032,8 @@ filter_by_max(_Msgs, _Junk) ->
limit_max(RSM, ?NS_MAM_TMP) ->
RSM; % XEP-0313 v0.2 doesn't require clients to support RSM.
+limit_max(none, _NS) ->
+ #rsm_in{max = ?DEF_PAGE_SIZE};
limit_max(#rsm_in{max = Max} = RSM, _NS) when not is_integer(Max) ->
RSM#rsm_in{max = ?DEF_PAGE_SIZE};
limit_max(#rsm_in{max = Max} = RSM, _NS) when Max > ?MAX_PAGE_SIZE ->
diff --git a/src/mod_mam_sql.erl b/src/mod_mam_sql.erl
index bbbe543f5..20ed8d4f1 100644
--- a/src/mod_mam_sql.erl
+++ b/src/mod_mam_sql.erl
@@ -295,25 +295,3 @@ make_sql_query(User, LServer, Start, End, With, RSM) ->
{QueryPage,
[<<"SELECT COUNT(*) FROM archive WHERE username='">>,
SUser, <<"'">>, WithClause, StartClause, EndClause, <<";">>]}.
-
-update(LServer, Table, Fields, Vals, Where) ->
- UPairs = lists:zipwith(fun (A, B) ->
- <<A/binary, "='", B/binary, "'">>
- end,
- Fields, Vals),
- case ejabberd_sql:sql_query(LServer,
- [<<"update ">>, Table, <<" set ">>,
- join(UPairs, <<", ">>), <<" where ">>, Where,
- <<";">>])
- of
- {updated, 1} -> {updated, 1};
- _ ->
- ejabberd_sql:sql_query(LServer,
- [<<"insert into ">>, Table, <<"(">>,
- join(Fields, <<", ">>), <<") values ('">>,
- join(Vals, <<"', '">>), <<"');">>])
- end.
-
-%% Almost a copy of string:join/2.
-join([], _Sep) -> [];
-join([H | T], Sep) -> [H, [[Sep, X] || X <- T]].
diff --git a/src/mod_metrics.erl b/src/mod_metrics.erl
index 40484e488..f1d487e0e 100644
--- a/src/mod_metrics.erl
+++ b/src/mod_metrics.erl
@@ -39,7 +39,8 @@
s2s_send_packet, s2s_receive_packet,
remove_user, register_user]).
--export([start/2, stop/1, send_metrics/4, opt_type/1, mod_opt_type/1]).
+-export([start/2, stop/1, send_metrics/4, opt_type/1, mod_opt_type/1,
+ depends/2]).
-export([offline_message_hook/3,
sm_register_connection_hook/3, sm_remove_connection_hook/3,
@@ -59,6 +60,9 @@ stop(Host) ->
[ejabberd_hooks:delete(Hook, Host, ?MODULE, Hook, 20)
|| Hook <- ?HOOKS].
+depends(_Host, _Opts) ->
+ [].
+
%%====================================================================
%% Hooks handlers
%%====================================================================
diff --git a/src/mod_mix.erl b/src/mod_mix.erl
index c0835b74e..b373ad13d 100644
--- a/src/mod_mix.erl
+++ b/src/mod_mix.erl
@@ -14,7 +14,7 @@
%% API
-export([start_link/2, start/2, stop/1, process_iq/3,
disco_items/5, disco_identity/5, disco_info/5,
- disco_features/5, mod_opt_type/1]).
+ disco_features/5, mod_opt_type/1, depends/2]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
@@ -343,6 +343,9 @@ is_not_subscribed({error, ErrEl}) ->
_ -> false
end.
+depends(_Host, _Opts) ->
+ [{mod_pubsub, hard}].
+
mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1;
mod_opt_type(host) -> fun iolist_to_binary/1;
mod_opt_type(_) -> [host, iqdisc].
diff --git a/src/mod_muc.erl b/src/mod_muc.erl
index b46585066..4c5b45ff5 100644
--- a/src/mod_muc.erl
+++ b/src/mod_muc.erl
@@ -53,7 +53,7 @@
-export([init/1, handle_call/3, handle_cast/2,
handle_info/2, terminate/2, code_change/3,
- mod_opt_type/1]).
+ mod_opt_type/1, depends/2]).
-include("ejabberd.hrl").
-include("logger.hrl").
@@ -105,6 +105,9 @@ stop(Host) ->
supervisor:delete_child(ejabberd_sup, Proc),
{wait, Rooms}.
+depends(_Host, _Opts) ->
+ [{mod_mam, soft}].
+
shutdown_rooms(Host) ->
MyHost = gen_mod:get_module_opt_host(Host, mod_muc,
<<"conference.@HOST@">>),
@@ -147,18 +150,10 @@ restore_room(ServerHost, Host, Name) ->
forget_room(ServerHost, Host, Name) ->
LServer = jid:nameprep(ServerHost),
- remove_room_mam(LServer, Host, Name),
+ ejabberd_hooks:run(remove_room, LServer, [LServer, Name, Host]),
Mod = gen_mod:db_mod(LServer, ?MODULE),
Mod:forget_room(LServer, Host, Name).
-remove_room_mam(LServer, Host, Name) ->
- case gen_mod:is_loaded(LServer, mod_mam) of
- true ->
- mod_mam:remove_room(LServer, Name, Host);
- false ->
- ok
- end.
-
process_iq_disco_items(Host, From, To,
#iq{lang = Lang} = IQ) ->
Rsm = jlib:rsm_decode(IQ),
@@ -230,6 +225,7 @@ init([Host, Opts]) ->
public -> Bool;
public_list -> Bool;
mam -> Bool;
+ allow_subscription -> Bool;
password -> fun iolist_to_binary/1;
title -> fun iolist_to_binary/1;
allow_private_messages_from_visitors ->
@@ -426,6 +422,18 @@ do_route1(Host, ServerHost, Access, HistorySize, RoomShaper,
iq_get_vcard(Lang)}]},
ejabberd_router:route(To, From,
jlib:iq_to_xml(Res));
+ #iq{type = get, xmlns = ?NS_MUCSUB,
+ sub_el = #xmlel{name = <<"subscriptions">>} = SubEl} = IQ ->
+ RoomJIDs = get_subscribed_rooms(ServerHost, Host, From),
+ Subs = lists:map(
+ fun(J) ->
+ #xmlel{name = <<"subscription">>,
+ attrs = [{<<"jid">>,
+ jid:to_string(J)}]}
+ end, RoomJIDs),
+ Res = IQ#iq{type = result,
+ sub_el = [SubEl#xmlel{children = Subs}]},
+ ejabberd_router:route(To, From, jlib:iq_to_xml(Res));
#iq{type = get, xmlns = ?NS_MUC_UNIQUE} = IQ ->
Res = IQ#iq{type = result,
sub_el =
@@ -598,6 +606,8 @@ iq_disco_info(ServerHost, Lang) ->
#xmlel{name = <<"feature">>,
attrs = [{<<"var">>, ?NS_RSM}], children = []},
#xmlel{name = <<"feature">>,
+ attrs = [{<<"var">>, ?NS_MUCSUB}], children = []},
+ #xmlel{name = <<"feature">>,
attrs = [{<<"var">>, ?NS_VCARD}], children = []}] ++
case gen_mod:is_loaded(ServerHost, mod_mam) of
true ->
@@ -693,6 +703,19 @@ get_vh_rooms(Host, #rsm_in{max=M, direction=Direction, id=I, index=Index})->
index = NewIndex}}
end.
+get_subscribed_rooms(ServerHost, Host, From) ->
+ Rooms = get_rooms(ServerHost, Host),
+ lists:flatmap(
+ fun(#muc_room{name_host = {Name, _}, opts = Opts}) ->
+ Subscribers = proplists:get_value(subscribers, Opts, []),
+ case lists:keymember(From, 1, Subscribers) of
+ true -> [jid:make(Name, Host, <<>>)];
+ false -> []
+ end;
+ (_) ->
+ []
+ end, Rooms).
+
%% @doc Return the position of desired room in the list of rooms.
%% The room must exist in the list. The count starts in 0.
%% @spec (Desired::muc_online_room(), Rooms::[muc_online_room()]) -> integer()
diff --git a/src/mod_muc_admin.erl b/src/mod_muc_admin.erl
index 5fbda4f28..0b5e79f60 100644
--- a/src/mod_muc_admin.erl
+++ b/src/mod_muc_admin.erl
@@ -11,7 +11,7 @@
-behaviour(gen_mod).
--export([start/2, stop/1, muc_online_rooms/1,
+-export([start/2, stop/1, depends/2, muc_online_rooms/1,
muc_unregister_nick/1, create_room/3, destroy_room/2,
create_rooms_file/1, destroy_rooms_file/1,
rooms_unused_list/2, rooms_unused_destroy/2,
@@ -49,6 +49,9 @@ stop(Host) ->
ejabberd_hooks:delete(webadmin_page_main, ?MODULE, web_page_main, 50),
ejabberd_hooks:delete(webadmin_page_host, Host, ?MODULE, web_page_host, 50).
+depends(_Host, _Opts) ->
+ [{mod_muc, hard}].
+
%%%
%%% Register commands
%%%
diff --git a/src/mod_muc_log.erl b/src/mod_muc_log.erl
index 167a96e37..ec4711b43 100644
--- a/src/mod_muc_log.erl
+++ b/src/mod_muc_log.erl
@@ -41,7 +41,7 @@
-export([init/1, handle_call/3, handle_cast/2,
handle_info/2, terminate/2, code_change/3,
- mod_opt_type/1, opt_type/1]).
+ mod_opt_type/1, opt_type/1, depends/2]).
-include("ejabberd.hrl").
-include("logger.hrl").
@@ -109,6 +109,9 @@ transform_module_options(Opts) ->
Opt
end, Opts).
+depends(_Host, _Opts) ->
+ [{mod_muc, hard}].
+
%%====================================================================
%% gen_server callbacks
%%====================================================================
diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl
index eabff103d..773953c4a 100644
--- a/src/mod_muc_room.erl
+++ b/src/mod_muc_room.erl
@@ -113,13 +113,7 @@ init([Host, ServerHost, Access, Room, HistorySize,
just_created = true,
room_shaper = Shaper}),
State1 = set_opts(DefRoomOpts, State),
- if (State1#state.config)#config.persistent ->
- mod_muc:store_room(State1#state.server_host,
- State1#state.host,
- State1#state.room,
- make_opts(State1));
- true -> ok
- end,
+ store_room(State1),
?INFO_MSG("Created MUC room ~s@~s by ~s",
[Room, Host, jid:to_string(Creator)]),
add_to_log(room_existence, created, State1),
@@ -268,16 +262,7 @@ normal_state({route, From, <<"">>,
StateData),
send_affiliation(IJID, member,
StateData),
- case
- (NSD#state.config)#config.persistent
- of
- true ->
- mod_muc:store_room(NSD#state.server_host,
- NSD#state.host,
- NSD#state.room,
- make_opts(NSD));
- _ -> ok
- end,
+ store_room(NSD),
{next_state, normal_state, NSD};
_ -> {next_state, normal_state, StateData}
end;
@@ -427,6 +412,7 @@ normal_state({route, From, <<"">>,
or (XMLNS == (?NS_DISCO_INFO))
or (XMLNS == (?NS_DISCO_ITEMS))
or (XMLNS == (?NS_VCARD))
+ or (XMLNS == (?NS_MUCSUB))
or (XMLNS == (?NS_CAPTCHA)) ->
Res1 = case XMLNS of
?NS_MUC_ADMIN ->
@@ -444,6 +430,8 @@ normal_state({route, From, <<"">>,
process_iq_disco_items(From, Type, Lang, StateData);
?NS_VCARD ->
process_iq_vcard(From, Type, Lang, SubEl, StateData);
+ ?NS_MUCSUB ->
+ process_iq_mucsub(From, Packet, IQ, StateData);
?NS_CAPTCHA ->
process_iq_captcha(From, Type, Lang, SubEl, StateData)
end,
@@ -458,12 +446,22 @@ normal_state({route, From, <<"">>,
XMLNS}],
children = Res}]},
SD};
+ {ignore, SD} -> {ignore, SD};
+ {error, Error, ResStateData} ->
+ {IQ#iq{type = error,
+ sub_el = [SubEl, Error]},
+ ResStateData};
{error, Error} ->
{IQ#iq{type = error,
sub_el = [SubEl, Error]},
StateData}
end,
- ejabberd_router:route(StateData#state.jid, From, jlib:iq_to_xml(IQRes)),
+ if IQRes /= ignore ->
+ ejabberd_router:route(
+ StateData#state.jid, From, jlib:iq_to_xml(IQRes));
+ true ->
+ ok
+ end,
case NewStateData of
stop -> {stop, normal, StateData};
_ -> {next_state, normal_state, NewStateData}
@@ -678,11 +676,12 @@ handle_event({service_message, Msg}, _StateName,
children =
[#xmlel{name = <<"body">>, attrs = [],
children = [{xmlcdata, Msg}]}]},
- send_multiple(
+ send_wrapped_multiple(
StateData#state.jid,
- StateData#state.server_host,
StateData#state.users,
- MessagePkt),
+ MessagePkt,
+ ?NS_MUCSUB_NODES_MESSAGES,
+ StateData),
NSD = add_message_to_history(<<"">>,
StateData#state.jid, MessagePkt, StateData),
{next_state, normal_state, NSD};
@@ -866,9 +865,11 @@ terminate(Reason, _StateName, StateData) ->
Nick = Info#user.nick,
case Reason of
shutdown ->
- ejabberd_router:route(jid:replace_resource(StateData#state.jid,
- Nick),
- Info#user.jid, Packet);
+ send_wrapped(jid:replace_resource(StateData#state.jid,
+ Nick),
+ Info#user.jid, Packet,
+ ?NS_MUCSUB_NODES_PARTICIPANTS,
+ StateData);
_ -> ok
end,
tab_remove_online_user(LJID, StateData)
@@ -894,14 +895,13 @@ process_groupchat_message(From,
is_user_allowed_message_nonparticipant(From, StateData)
of
true ->
- {FromNick, Role} = get_participant_data(From,
- StateData),
- if (Role == moderator) or (Role == participant) or
+ {FromNick, Role, IsSubscriber} = get_participant_data(From, StateData),
+ if (Role == moderator) or (Role == participant) or IsSubscriber or
((StateData#state.config)#config.moderated == false) ->
- {NewStateData1, IsAllowed} = case check_subject(Packet)
- of
+ Subject = check_subject(Packet),
+ {NewStateData1, IsAllowed} = case Subject of
false -> {StateData, true};
- Subject ->
+ _ ->
case
can_change_subject(Role,
StateData)
@@ -914,16 +914,7 @@ process_groupchat_message(From,
subject_author
=
FromNick},
- case
- (NSD#state.config)#config.persistent
- of
- true ->
- mod_muc:store_room(NSD#state.server_host,
- NSD#state.host,
- NSD#state.room,
- make_opts(NSD));
- _ -> ok
- end,
+ store_room(NSD),
{NSD, true};
_ -> {StateData, false}
end
@@ -942,11 +933,13 @@ process_groupchat_message(From,
{next_state, normal_state, StateData};
NewPacket1 ->
NewPacket = fxml:remove_subtags(NewPacket1, <<"nick">>, {<<"xmlns">>, ?NS_NICK}),
- send_multiple(jid:replace_resource(StateData#state.jid,
- FromNick),
- StateData#state.server_host,
- StateData#state.users,
- NewPacket),
+ Node = if Subject == false -> ?NS_MUCSUB_NODES_MESSAGES;
+ true -> ?NS_MUCSUB_NODES_SUBJECT
+ end,
+ send_wrapped_multiple(
+ jid:replace_resource(StateData#state.jid, FromNick),
+ StateData#state.users,
+ NewPacket, Node, NewStateData1),
NewStateData2 = case has_body_or_subject(NewPacket) of
true ->
add_message_to_history(FromNick, From,
@@ -1013,9 +1006,9 @@ get_participant_data(From, StateData) ->
case (?DICT):find(jid:tolower(From),
StateData#state.users)
of
- {ok, #user{nick = FromNick, role = Role}} ->
- {FromNick, Role};
- error -> {<<"">>, moderator}
+ {ok, #user{nick = FromNick, role = Role, is_subscriber = IsSubscriber}} ->
+ {FromNick, Role, IsSubscriber};
+ error -> {<<"">>, moderator, false}
end.
process_presence(From, Nick,
@@ -1023,6 +1016,7 @@ process_presence(From, Nick,
StateData) ->
Type0 = fxml:get_attr_s(<<"type">>, Attrs0),
IsOnline = is_user_online(From, StateData),
+ IsSubscriber = is_subscriber(From, StateData),
if Type0 == <<"">>;
IsOnline and ((Type0 == <<"unavailable">>) or (Type0 == <<"error">>)) ->
case ejabberd_hooks:run_fold(muc_filter_presence,
@@ -1059,7 +1053,7 @@ process_presence(From, Nick,
Status_el ->
fxml:get_tag_cdata(Status_el)
end,
- remove_online_user(From, NewState, Reason);
+ remove_online_user(From, NewState, IsSubscriber, Reason);
<<"error">> ->
ErrorText = <<"It is not allowed to send error messages to the"
" room. The participant (~s) has sent an error "
@@ -1115,22 +1109,28 @@ process_presence(From, Nick,
Nick),
From, Err),
StateData;
- _ -> change_nick(From, Nick, StateData)
+ _ ->
+ case is_initial_presence(From, StateData) of
+ true ->
+ subscriber_becomes_available(
+ From, Nick, Packet, StateData);
+ false ->
+ change_nick(From, Nick, StateData)
+ end
end;
_NotNickChange ->
- Stanza = case
- {(StateData#state.config)#config.allow_visitor_status,
- is_visitor(From, StateData)}
- of
- {false, true} ->
- strip_status(Packet);
- _Allowed -> Packet
- end,
- NewState = add_user_presence(From, Stanza,
- StateData),
- send_new_presence(
- From, NewState, StateData),
- NewState
+ case is_initial_presence(From, StateData) of
+ true ->
+ subscriber_becomes_available(
+ From, Nick, Packet, StateData);
+ false ->
+ Stanza = maybe_strip_status_from_presence(
+ From, Packet, StateData),
+ NewState = add_user_presence(From, Stanza,
+ StateData),
+ send_new_presence(From, NewState, StateData),
+ NewState
+ end
end
end
end,
@@ -1140,6 +1140,25 @@ process_presence(From, Nick,
{next_state, normal_state, StateData}
end.
+maybe_strip_status_from_presence(From, Packet, StateData) ->
+ case {(StateData#state.config)#config.allow_visitor_status,
+ is_visitor(From, StateData)} of
+ {false, true} ->
+ strip_status(Packet);
+ _Allowed -> Packet
+ end.
+
+subscriber_becomes_available(From, Nick, Packet, StateData) ->
+ Stanza = maybe_strip_status_from_presence(From, Packet, StateData),
+ State1 = add_user_presence(From, Stanza, StateData),
+ Aff = get_affiliation(From, State1),
+ Role = get_default_role(Aff, State1),
+ State2 = set_role(From, Role, State1),
+ State3 = set_nick(From, Nick, State2),
+ send_existing_presences(From, State3),
+ send_initial_presence(From, State3, StateData),
+ State3.
+
close_room_if_temporary_and_empty(StateData1) ->
case not (StateData1#state.config)#config.persistent
andalso (?DICT):to_list(StateData1#state.users) == []
@@ -1157,6 +1176,15 @@ is_user_online(JID, StateData) ->
LJID = jid:tolower(JID),
(?DICT):is_key(LJID, StateData#state.users).
+is_subscriber(JID, StateData) ->
+ LJID = jid:tolower(JID),
+ case (?DICT):find(LJID, StateData#state.users) of
+ {ok, #user{is_subscriber = IsSubscriber}} ->
+ IsSubscriber;
+ _ ->
+ false
+ end.
+
%% Check if the user is occupant of the room, or at least is an admin or owner.
is_occupant_or_admin(JID, StateData) ->
FAffiliation = get_affiliation(JID, StateData),
@@ -1324,6 +1352,7 @@ make_reason(Packet, From, StateData, Reason1) ->
iolist_to_binary(io_lib:format(Reason1, [FromNick, Condition])).
expulse_participant(Packet, From, StateData, Reason1) ->
+ IsSubscriber = is_subscriber(From, StateData),
Reason2 = make_reason(Packet, From, StateData, Reason1),
NewState = add_user_presence_un(From,
#xmlel{name = <<"presence">>,
@@ -1338,7 +1367,7 @@ expulse_participant(Packet, From, StateData, Reason1) ->
Reason2}]}]},
StateData),
send_new_presence(From, NewState, StateData),
- remove_online_user(From, NewState).
+ remove_online_user(From, NewState, IsSubscriber).
set_affiliation(JID, Affiliation, StateData) ->
set_affiliation(JID, Affiliation, StateData, <<"">>).
@@ -1439,15 +1468,16 @@ set_role(JID, Role, StateData) ->
StateData#state.nicks},
LJIDs);
_ ->
- {lists:foldl(fun (J, Us) ->
- {ok, User} = (?DICT):find(J,
- Us),
- (?DICT):store(J,
- User#user{role =
- Role},
- Us)
- end,
- StateData#state.users, LJIDs),
+ {lists:foldl(
+ fun (J, Us) ->
+ {ok, User} = (?DICT):find(J, Us),
+ if User#user.last_presence == undefined ->
+ Us;
+ true ->
+ (?DICT):store(J, User#user{role = Role}, Us)
+ end
+ end,
+ StateData#state.users, LJIDs),
StateData#state.nicks}
end,
StateData#state{users = Users, nicks = Nicks}.
@@ -1617,27 +1647,66 @@ prepare_room_queue(StateData) ->
{empty, _} -> StateData
end.
-add_online_user(JID, Nick, Role, StateData) ->
+update_online_user(JID, #user{nick = Nick, subscriptions = Nodes,
+ is_subscriber = IsSubscriber} = User, StateData) ->
LJID = jid:tolower(JID),
- Users = (?DICT):store(LJID,
- #user{jid = JID, nick = Nick, role = Role},
- StateData#state.users),
- add_to_log(join, Nick, StateData),
+ Nicks1 = case (?DICT):find(LJID, StateData#state.users) of
+ {ok, #user{nick = OldNick}} ->
+ case lists:delete(
+ LJID, ?DICT:fetch(OldNick, StateData#state.nicks)) of
+ [] ->
+ ?DICT:erase(OldNick, StateData#state.nicks);
+ LJIDs ->
+ ?DICT:store(OldNick, LJIDs, StateData#state.nicks)
+ end;
+ error ->
+ StateData#state.nicks
+ end,
Nicks = (?DICT):update(Nick,
- fun (Entry) ->
- case lists:member(LJID, Entry) of
- true -> Entry;
- false -> [LJID | Entry]
- end
- end,
- [LJID], StateData#state.nicks),
+ fun (LJIDs) -> [LJID|LJIDs -- [LJID]] end,
+ [LJID], Nicks1),
+ Users = (?DICT):update(LJID,
+ fun(U) ->
+ U#user{nick = Nick,
+ subscriptions = Nodes,
+ is_subscriber = IsSubscriber}
+ end, User, StateData#state.users),
+ NewStateData = StateData#state{users = Users, nicks = Nicks},
+ case {?DICT:find(LJID, StateData#state.users),
+ ?DICT:find(LJID, NewStateData#state.users)} of
+ {{ok, #user{nick = Old}}, {ok, #user{nick = New}}} when Old /= New ->
+ send_nick_changing(JID, Old, NewStateData, true, true);
+ _ ->
+ ok
+ end,
+ NewStateData.
+
+add_online_user(JID, Nick, Role, IsSubscriber, Nodes, StateData) ->
tab_add_online_user(JID, StateData),
- StateData#state{users = Users, nicks = Nicks}.
+ User = #user{jid = JID, nick = Nick, role = Role,
+ is_subscriber = IsSubscriber, subscriptions = Nodes},
+ StateData1 = update_online_user(JID, User, StateData),
+ if IsSubscriber ->
+ store_room(StateData1);
+ true ->
+ ok
+ end,
+ StateData1.
-remove_online_user(JID, StateData) ->
- remove_online_user(JID, StateData, <<"">>).
+remove_online_user(JID, StateData, IsSubscriber) ->
+ remove_online_user(JID, StateData, IsSubscriber, <<"">>).
-remove_online_user(JID, StateData, Reason) ->
+remove_online_user(JID, StateData, _IsSubscriber = true, _Reason) ->
+ LJID = jid:tolower(JID),
+ Users = case (?DICT):find(LJID, StateData#state.users) of
+ {ok, U} ->
+ (?DICT):store(LJID, U#user{last_presence = undefined},
+ StateData#state.users);
+ error ->
+ StateData#state.users
+ end,
+ StateData#state{users = Users};
+remove_online_user(JID, StateData, _IsSubscriber, Reason) ->
LJID = jid:tolower(JID),
{ok, #user{nick = Nick}} = (?DICT):find(LJID,
StateData#state.users),
@@ -1742,10 +1811,12 @@ find_jid_by_nick(Nick, StateData) ->
error -> false
end.
-higher_presence(Pres1, Pres2) ->
+higher_presence(Pres1, Pres2) when Pres1 /= undefined, Pres2 /= undefined ->
Pri1 = get_priority_from_presence(Pres1),
Pri2 = get_priority_from_presence(Pres2),
- Pri1 > Pri2.
+ Pri1 > Pri2;
+higher_presence(Pres1, Pres2) ->
+ Pres1 > Pres2.
get_priority_from_presence(PresencePacket) ->
case fxml:get_subtag(PresencePacket, <<"priority">>) of
@@ -1784,9 +1855,10 @@ nick_collision(User, Nick, StateData) ->
/= jid:remove_resource(jid:tolower(User))).
add_new_user(From, Nick,
- #xmlel{attrs = Attrs, children = Els} = Packet,
+ #xmlel{name = Name, attrs = Attrs, children = Els} = Packet,
StateData) ->
Lang = fxml:get_attr_s(<<"xml:lang">>, Attrs),
+ UserRoomJID = jid:replace_resource(StateData#state.jid, Nick),
MaxUsers = get_max_users(StateData),
MaxAdminUsers = MaxUsers +
get_max_users_admin_threshold(StateData),
@@ -1802,6 +1874,7 @@ add_new_user(From, Nick,
fun(I) when is_integer(I), I>0 -> I end,
10),
Collision = nick_collision(From, Nick, StateData),
+ IsSubscribeRequest = Name /= <<"presence">>,
case {(ServiceAffiliation == owner orelse
((Affiliation == admin orelse Affiliation == owner)
andalso NUsers < MaxAdminUsers)
@@ -1814,91 +1887,116 @@ add_new_user(From, Nick,
of
{false, _, _, _} when NUsers >= MaxUsers orelse NUsers >= MaxAdminUsers ->
Txt = <<"Too many users in this conference">>,
- Err = jlib:make_error_reply(Packet,
- ?ERRT_RESOURCE_CONSTRAINT(Lang, Txt)),
- ejabberd_router:route % TODO: s/Nick/""/
- (jid:replace_resource(StateData#state.jid, Nick),
- From, Err),
- StateData;
+ Err = ?ERRT_RESOURCE_CONSTRAINT(Lang, Txt),
+ ErrPacket = jlib:make_error_reply(Packet, Err),
+ if not IsSubscribeRequest ->
+ ejabberd_router:route(UserRoomJID, From, ErrPacket),
+ StateData;
+ true ->
+ {error, Err, StateData}
+ end;
{false, _, _, _} when NConferences >= MaxConferences ->
Txt = <<"You have joined too many conferences">>,
- Err = jlib:make_error_reply(Packet,
- ?ERRT_RESOURCE_CONSTRAINT(Lang, Txt)),
- ejabberd_router:route % TODO: s/Nick/""/
- (jid:replace_resource(StateData#state.jid, Nick),
- From, Err),
- StateData;
+ Err = ?ERRT_RESOURCE_CONSTRAINT(Lang, Txt),
+ ErrPacket = jlib:make_error_reply(Packet, Err),
+ if not IsSubscribeRequest ->
+ ejabberd_router:route(UserRoomJID, From, ErrPacket),
+ StateData;
+ true ->
+ {error, Err, StateData}
+ end;
{false, _, _, _} ->
- Err = jlib:make_error_reply(Packet,
- ?ERR_SERVICE_UNAVAILABLE),
- ejabberd_router:route % TODO: s/Nick/""/
- (jid:replace_resource(StateData#state.jid, Nick),
- From, Err),
- StateData;
+ Err = ?ERR_SERVICE_UNAVAILABLE,
+ ErrPacket = jlib:make_error_reply(Packet, Err),
+ if not IsSubscribeRequest ->
+ ejabberd_router:route(UserRoomJID, From, ErrPacket),
+ StateData;
+ true ->
+ {error, Err, StateData}
+ end;
{_, _, _, none} ->
- Err = jlib:make_error_reply(Packet,
- case Affiliation of
- outcast ->
- ErrText =
- <<"You have been banned from this room">>,
- ?ERRT_FORBIDDEN(Lang, ErrText);
- _ ->
- ErrText =
- <<"Membership is required to enter this room">>,
- ?ERRT_REGISTRATION_REQUIRED(Lang,
- ErrText)
- end),
- ejabberd_router:route % TODO: s/Nick/""/
- (jid:replace_resource(StateData#state.jid, Nick),
- From, Err),
- StateData;
+ Err = case Affiliation of
+ outcast ->
+ ErrText = <<"You have been banned from this room">>,
+ ?ERRT_FORBIDDEN(Lang, ErrText);
+ _ ->
+ ErrText = <<"Membership is required to enter this room">>,
+ ?ERRT_REGISTRATION_REQUIRED(Lang, ErrText)
+ end,
+ ErrPacket = jlib:make_error_reply(Packet, Err),
+ if not IsSubscribeRequest ->
+ ejabberd_router:route(UserRoomJID, From, ErrPacket),
+ StateData;
+ true ->
+ {error, Err, StateData}
+ end;
{_, true, _, _} ->
ErrText = <<"That nickname is already in use by another occupant">>,
- Err = jlib:make_error_reply(Packet,
- ?ERRT_CONFLICT(Lang, ErrText)),
- ejabberd_router:route(jid:replace_resource(StateData#state.jid,
- Nick),
- From, Err),
- StateData;
+ Err = ?ERRT_CONFLICT(Lang, ErrText),
+ ErrPacket = jlib:make_error_reply(Packet, Err),
+ if not IsSubscribeRequest ->
+ ejabberd_router:route(UserRoomJID, From, ErrPacket),
+ StateData;
+ true ->
+ {error, Err, StateData}
+ end;
{_, _, false, _} ->
ErrText = <<"That nickname is registered by another person">>,
- Err = jlib:make_error_reply(Packet,
- ?ERRT_CONFLICT(Lang, ErrText)),
- ejabberd_router:route(jid:replace_resource(StateData#state.jid,
- Nick),
- From, Err),
- StateData;
+ Err = ?ERRT_CONFLICT(Lang, ErrText),
+ ErrPacket = jlib:make_error_reply(Packet, Err),
+ if not IsSubscribeRequest ->
+ ejabberd_router:route(UserRoomJID, From, ErrPacket),
+ StateData;
+ true ->
+ {error, Err, StateData}
+ end;
{_, _, _, Role} ->
case check_password(ServiceAffiliation, Affiliation,
Els, From, StateData)
of
true ->
- NewState = add_user_presence(From, Packet,
- add_online_user(From, Nick, Role,
- StateData)),
- send_existing_presences(From, NewState),
- send_initial_presence(From, NewState, StateData),
- Shift = count_stanza_shift(Nick, Els, NewState),
- case send_history(From, Shift, NewState) of
- true -> ok;
- _ -> send_subject(From, StateData)
- end,
- case NewState#state.just_created of
- true -> NewState#state{just_created = false};
- false ->
- Robots = (?DICT):erase(From, StateData#state.robots),
- NewState#state{robots = Robots}
- end;
+ Nodes = get_subscription_nodes(Packet),
+ NewStateData =
+ if not IsSubscribeRequest ->
+ NewState = add_user_presence(
+ From, Packet,
+ add_online_user(From, Nick, Role,
+ IsSubscribeRequest,
+ Nodes, StateData)),
+ send_existing_presences(From, NewState),
+ send_initial_presence(From, NewState, StateData),
+ Shift = count_stanza_shift(Nick, Els, NewState),
+ case send_history(From, Shift, NewState) of
+ true -> ok;
+ _ -> send_subject(From, StateData)
+ end,
+ NewState;
+ true ->
+ add_online_user(From, Nick, none,
+ IsSubscribeRequest,
+ Nodes, StateData)
+ end,
+ ResultState =
+ case NewStateData#state.just_created of
+ true ->
+ NewStateData#state{just_created = false};
+ false ->
+ Robots = (?DICT):erase(From, StateData#state.robots),
+ NewStateData#state{robots = Robots}
+ end,
+ if not IsSubscribeRequest -> ResultState;
+ true -> {result, subscription_nodes_to_events(Nodes), ResultState}
+ end;
nopass ->
ErrText = <<"A password is required to enter this room">>,
- Err = jlib:make_error_reply(Packet,
- ?ERRT_NOT_AUTHORIZED(Lang,
- ErrText)),
- ejabberd_router:route % TODO: s/Nick/""/
- (jid:replace_resource(StateData#state.jid,
- Nick),
- From, Err),
- StateData;
+ Err = ?ERRT_NOT_AUTHORIZED(Lang, ErrText),
+ ErrPacket = jlib:make_error_reply(Packet, Err),
+ if not IsSubscribeRequest ->
+ ejabberd_router:route(UserRoomJID, From, ErrPacket),
+ StateData;
+ true ->
+ {error, Err, StateData}
+ end;
captcha_required ->
SID = fxml:get_attr_s(<<"id">>, Attrs),
RoomJID = StateData#state.jid,
@@ -1906,7 +2004,7 @@ add_new_user(From, Nick,
Limiter = {From#jid.luser, From#jid.lserver},
case ejabberd_captcha:create_captcha(SID, RoomJID, To,
Lang, Limiter, From)
- of
+ of
{ok, ID, CaptchaEls} ->
MsgPkt = #xmlel{name = <<"message">>,
attrs = [{<<"id">>, ID}],
@@ -1914,38 +2012,43 @@ add_new_user(From, Nick,
Robots = (?DICT):store(From, {Nick, Packet},
StateData#state.robots),
ejabberd_router:route(RoomJID, From, MsgPkt),
- StateData#state{robots = Robots};
+ NewState = StateData#state{robots = Robots},
+ if not IsSubscribeRequest ->
+ NewState;
+ true ->
+ {ignore, NewState}
+ end;
{error, limit} ->
ErrText = <<"Too many CAPTCHA requests">>,
- Err = jlib:make_error_reply(Packet,
- ?ERRT_RESOURCE_CONSTRAINT(Lang,
- ErrText)),
- ejabberd_router:route % TODO: s/Nick/""/
- (jid:replace_resource(StateData#state.jid,
- Nick),
- From, Err),
- StateData;
+ Err = ?ERRT_RESOURCE_CONSTRAINT(Lang, ErrText),
+ ErrPacket = jlib:make_error_reply(Packet, Err),
+ if not IsSubscribeRequest ->
+ ejabberd_router:route(UserRoomJID, From, ErrPacket),
+ StateData;
+ true ->
+ {error, Err, StateData}
+ end;
_ ->
ErrText = <<"Unable to generate a CAPTCHA">>,
- Err = jlib:make_error_reply(Packet,
- ?ERRT_INTERNAL_SERVER_ERROR(Lang,
- ErrText)),
- ejabberd_router:route % TODO: s/Nick/""/
- (jid:replace_resource(StateData#state.jid,
- Nick),
- From, Err),
- StateData
+ Err = ?ERRT_INTERNAL_SERVER_ERROR(Lang, ErrText),
+ ErrPacket = jlib:make_error_reply(Packet, Err),
+ if not IsSubscribeRequest ->
+ ejabberd_router:route(UserRoomJID, From, ErrPacket),
+ StateData;
+ true ->
+ {error, Err, StateData}
+ end
end;
_ ->
ErrText = <<"Incorrect password">>,
- Err = jlib:make_error_reply(Packet,
- ?ERRT_NOT_AUTHORIZED(Lang,
- ErrText)),
- ejabberd_router:route % TODO: s/Nick/""/
- (jid:replace_resource(StateData#state.jid,
- Nick),
- From, Err),
- StateData
+ Err = ?ERRT_NOT_AUTHORIZED(Lang, ErrText),
+ ErrPacket = jlib:make_error_reply(Packet, Err),
+ if not IsSubscribeRequest ->
+ ejabberd_router:route(UserRoomJID, From, ErrPacket),
+ StateData;
+ true ->
+ {error, Err, StateData}
+ end
end
end.
@@ -2111,6 +2214,15 @@ presence_broadcast_allowed(JID, StateData) ->
Role = get_role(JID, StateData),
lists:member(Role, (StateData#state.config)#config.presence_broadcast).
+is_initial_presence(From, StateData) ->
+ LJID = jid:tolower(From),
+ case (?DICT):find(LJID, StateData#state.users) of
+ {ok, #user{last_presence = Pres}} when Pres /= undefined ->
+ false;
+ _ ->
+ true
+ end.
+
send_initial_presence(NJID, StateData, OldStateData) ->
send_new_presence1(NJID, <<"">>, true, StateData, OldStateData).
@@ -2159,6 +2271,24 @@ send_new_presence(NJID, Reason, IsInitialPresence, StateData, OldStateData) ->
OldStateData)
end.
+is_ra_changed(_, _IsInitialPresence = true, _, _) ->
+ false;
+is_ra_changed(LJID, _IsInitialPresence = false, NewStateData, OldStateData) ->
+ JID = case LJID of
+ #jid{} -> LJID;
+ _ -> jid:make(LJID)
+ end,
+ NewRole = get_role(LJID, NewStateData),
+ NewAff = get_affiliation(JID, NewStateData),
+ OldRole = get_role(LJID, OldStateData),
+ OldAff = get_affiliation(JID, OldStateData),
+ if (NewRole == none) and (NewAff == OldAff) ->
+ %% A user is leaving the room;
+ false;
+ true ->
+ (NewRole /= OldRole) or (NewAff /= OldAff)
+ end.
+
send_new_presence1(NJID, Reason, IsInitialPresence, StateData, OldStateData) ->
LNJID = jid:tolower(NJID),
#user{nick = Nick} = (?DICT):fetch(LNJID, StateData#state.users),
@@ -2188,56 +2318,72 @@ send_new_presence1(NJID, Reason, IsInitialPresence, StateData, OldStateData) ->
false ->
(?DICT):to_list(StateData#state.users)
end,
- lists:foreach(fun ({LUJID, Info}) ->
- {Role, Presence} =
- if
- LNJID == LUJID -> {Role0, Presence0};
- true -> {Role1, Presence1}
- end,
- SRole = role_to_list(Role),
- ItemAttrs = case Info#user.role == moderator orelse
- (StateData#state.config)#config.anonymous
- == false
- of
- true ->
- [{<<"jid">>,
- jid:to_string(RealJID)},
- {<<"affiliation">>, SAffiliation},
- {<<"role">>, SRole}];
- _ ->
- [{<<"affiliation">>, SAffiliation},
- {<<"role">>, SRole}]
- end,
- ItemEls = case Reason of
- <<"">> -> [];
- _ ->
- [#xmlel{name = <<"reason">>,
- attrs = [],
- children =
- [{xmlcdata, Reason}]}]
- end,
- StatusEls = status_els(IsInitialPresence, NJID, Info,
- StateData),
- Packet = fxml:append_subtags(Presence,
- [#xmlel{name = <<"x">>,
- attrs =
- [{<<"xmlns">>,
- ?NS_MUC_USER}],
- children =
- [#xmlel{name =
- <<"item">>,
- attrs
- =
- ItemAttrs,
- children
- =
- ItemEls}
- | StatusEls]}]),
- ejabberd_router:route(jid:replace_resource(StateData#state.jid,
- Nick),
- Info#user.jid, Packet)
- end,
- UserList).
+ lists:foreach(
+ fun({LUJID, Info}) ->
+ {Role, Presence} = if LNJID == LUJID -> {Role0, Presence0};
+ true -> {Role1, Presence1}
+ end,
+ SRole = role_to_list(Role),
+ ItemAttrs = case Info#user.role == moderator orelse
+ (StateData#state.config)#config.anonymous
+ == false
+ of
+ true ->
+ [{<<"jid">>,
+ jid:to_string(RealJID)},
+ {<<"affiliation">>, SAffiliation},
+ {<<"role">>, SRole}];
+ _ ->
+ [{<<"affiliation">>, SAffiliation},
+ {<<"role">>, SRole}]
+ end,
+ ItemEls = case Reason of
+ <<"">> -> [];
+ _ ->
+ [#xmlel{name = <<"reason">>,
+ attrs = [],
+ children =
+ [{xmlcdata, Reason}]}]
+ end,
+ StatusEls = status_els(IsInitialPresence, NJID, Info,
+ StateData),
+ Pres = if Presence == undefined -> #xmlel{name = <<"presence">>};
+ true -> Presence
+ end,
+ Packet = fxml:append_subtags(Pres,
+ [#xmlel{name = <<"x">>,
+ attrs =
+ [{<<"xmlns">>,
+ ?NS_MUC_USER}],
+ children =
+ [#xmlel{name =
+ <<"item">>,
+ attrs
+ =
+ ItemAttrs,
+ children
+ =
+ ItemEls}
+ | StatusEls]}]),
+ Node1 = case is_ra_changed(NJID, IsInitialPresence, StateData, OldStateData) of
+ true -> ?NS_MUCSUB_NODES_AFFILIATIONS;
+ false -> ?NS_MUCSUB_NODES_PRESENCE
+ end,
+ send_wrapped(jid:replace_resource(StateData#state.jid, Nick),
+ Info#user.jid, Packet, Node1, StateData),
+ Type = fxml:get_tag_attr_s(<<"type">>, Packet),
+ IsSubscriber = Info#user.is_subscriber,
+ IsOccupant = Info#user.last_presence /= undefined,
+ if (IsSubscriber and not IsOccupant) and
+ (IsInitialPresence or (Type == <<"unavailable">>)) ->
+ Node2 = ?NS_MUCSUB_NODES_PARTICIPANTS,
+ send_wrapped(jid:replace_resource(StateData#state.jid, Nick),
+ Info#user.jid, Packet, Node2, StateData);
+ true ->
+ ok
+ end
+ end,
+ UserList).
send_existing_presences(ToJID, StateData) ->
case is_room_overcrowded(StateData) of
@@ -2249,90 +2395,94 @@ send_existing_presences1(ToJID, StateData) ->
LToJID = jid:tolower(ToJID),
{ok, #user{jid = RealToJID, role = Role}} =
(?DICT):find(LToJID, StateData#state.users),
- lists:foreach(fun ({FromNick, _Users}) ->
- LJID = find_jid_by_nick(FromNick, StateData),
- #user{jid = FromJID, role = FromRole,
- last_presence = Presence} =
- (?DICT):fetch(jid:tolower(LJID),
- StateData#state.users),
- PresenceBroadcast =
- lists:member(
- FromRole, (StateData#state.config)#config.presence_broadcast),
- case {RealToJID, PresenceBroadcast} of
- {FromJID, _} -> ok;
- {_, false} -> ok;
- _ ->
- FromAffiliation = get_affiliation(LJID,
- StateData),
- ItemAttrs = case Role == moderator orelse
- (StateData#state.config)#config.anonymous
- == false
- of
- true ->
- [{<<"jid">>,
- jid:to_string(FromJID)},
- {<<"affiliation">>,
- affiliation_to_list(FromAffiliation)},
- {<<"role">>,
- role_to_list(FromRole)}];
- _ ->
- [{<<"affiliation">>,
- affiliation_to_list(FromAffiliation)},
- {<<"role">>,
- role_to_list(FromRole)}]
- end,
- Packet = fxml:append_subtags(Presence,
- [#xmlel{name =
- <<"x">>,
- attrs =
- [{<<"xmlns">>,
- ?NS_MUC_USER}],
- children =
- [#xmlel{name
- =
- <<"item">>,
- attrs
- =
- ItemAttrs,
- children
- =
- []}]}]),
- ejabberd_router:route(jid:replace_resource(StateData#state.jid,
- FromNick),
- RealToJID, Packet)
- end
- end,
- (?DICT):to_list(StateData#state.nicks)).
+ lists:foreach(
+ fun({FromNick, _Users}) ->
+ LJID = find_jid_by_nick(FromNick, StateData),
+ #user{jid = FromJID, role = FromRole,
+ last_presence = Presence} =
+ (?DICT):fetch(jid:tolower(LJID),
+ StateData#state.users),
+ PresenceBroadcast =
+ lists:member(
+ FromRole, (StateData#state.config)#config.presence_broadcast),
+ case {RealToJID, PresenceBroadcast} of
+ {FromJID, _} -> ok;
+ {_, false} -> ok;
+ _ ->
+ FromAffiliation = get_affiliation(LJID, StateData),
+ ItemAttrs = case Role == moderator orelse
+ (StateData#state.config)#config.anonymous
+ == false
+ of
+ true ->
+ [{<<"jid">>,
+ jid:to_string(FromJID)},
+ {<<"affiliation">>,
+ affiliation_to_list(FromAffiliation)},
+ {<<"role">>,
+ role_to_list(FromRole)}];
+ _ ->
+ [{<<"affiliation">>,
+ affiliation_to_list(FromAffiliation)},
+ {<<"role">>,
+ role_to_list(FromRole)}]
+ end,
+ Packet = fxml:append_subtags(
+ Presence,
+ [#xmlel{name =
+ <<"x">>,
+ attrs =
+ [{<<"xmlns">>,
+ ?NS_MUC_USER}],
+ children =
+ [#xmlel{name
+ =
+ <<"item">>,
+ attrs
+ =
+ ItemAttrs,
+ children
+ =
+ []}]}]),
+ send_wrapped(jid:replace_resource(StateData#state.jid, FromNick),
+ RealToJID, Packet, ?NS_MUCSUB_NODES_PRESENCE, StateData)
+ end
+ end,
+ (?DICT):to_list(StateData#state.nicks)).
-change_nick(JID, Nick, StateData) ->
+set_nick(JID, Nick, State) ->
LJID = jid:tolower(JID),
- {ok, #user{nick = OldNick}} = (?DICT):find(LJID,
- StateData#state.users),
+ {ok, #user{nick = OldNick}} = (?DICT):find(LJID, State#state.users),
Users = (?DICT):update(LJID,
fun (#user{} = User) -> User#user{nick = Nick} end,
- StateData#state.users),
- OldNickUsers = (?DICT):fetch(OldNick,
- StateData#state.nicks),
- NewNickUsers = case (?DICT):find(Nick,
- StateData#state.nicks)
- of
- {ok, U} -> U;
- error -> []
+ State#state.users),
+ OldNickUsers = (?DICT):fetch(OldNick, State#state.nicks),
+ NewNickUsers = case (?DICT):find(Nick, State#state.nicks) of
+ {ok, U} -> U;
+ error -> []
end,
- SendOldUnavailable = length(OldNickUsers) == 1,
- SendNewAvailable = SendOldUnavailable orelse
- NewNickUsers == [],
Nicks = case OldNickUsers of
- [LJID] ->
- (?DICT):store(Nick, [LJID | NewNickUsers],
- (?DICT):erase(OldNick, StateData#state.nicks));
- [_ | _] ->
- (?DICT):store(Nick, [LJID | NewNickUsers],
- (?DICT):store(OldNick, OldNickUsers -- [LJID],
- StateData#state.nicks))
+ [LJID] ->
+ (?DICT):store(Nick, [LJID | NewNickUsers -- [LJID]],
+ (?DICT):erase(OldNick, State#state.nicks));
+ [_ | _] ->
+ (?DICT):store(Nick, [LJID | NewNickUsers -- [LJID]],
+ (?DICT):store(OldNick, OldNickUsers -- [LJID],
+ State#state.nicks))
end,
- NewStateData = StateData#state{users = Users,
- nicks = Nicks},
+ State#state{users = Users, nicks = Nicks}.
+
+change_nick(JID, Nick, StateData) ->
+ LJID = jid:tolower(JID),
+ {ok, #user{nick = OldNick}} = (?DICT):find(LJID, StateData#state.users),
+ OldNickUsers = (?DICT):fetch(OldNick, StateData#state.nicks),
+ NewNickUsers = case (?DICT):find(Nick, StateData#state.nicks) of
+ {ok, U} -> U;
+ error -> []
+ end,
+ SendOldUnavailable = length(OldNickUsers) == 1,
+ SendNewAvailable = SendOldUnavailable orelse NewNickUsers == [],
+ NewStateData = set_nick(JID, Nick, StateData),
case presence_broadcast_allowed(JID, NewStateData) of
true ->
send_nick_changing(JID, OldNick, NewStateData,
@@ -2352,7 +2502,7 @@ send_nick_changing(JID, OldNick, StateData,
Affiliation = get_affiliation(JID, StateData),
SAffiliation = affiliation_to_list(Affiliation),
SRole = role_to_list(Role),
- lists:foreach(fun ({_LJID, Info}) ->
+ lists:foreach(fun ({_LJID, Info}) when Presence /= undefined ->
ItemAttrs1 = case Info#user.role == moderator orelse
(StateData#state.config)#config.anonymous
== false
@@ -2428,17 +2578,23 @@ send_nick_changing(JID, OldNick, StateData,
=
[]}|Status110]}]),
if SendOldUnavailable ->
- ejabberd_router:route(jid:replace_resource(StateData#state.jid,
- OldNick),
- Info#user.jid, Packet1);
+ send_wrapped(jid:replace_resource(StateData#state.jid,
+ OldNick),
+ Info#user.jid, Packet1,
+ ?NS_MUCSUB_NODES_PRESENCE,
+ StateData);
true -> ok
end,
if SendNewAvailable ->
- ejabberd_router:route(jid:replace_resource(StateData#state.jid,
- Nick),
- Info#user.jid, Packet2);
+ send_wrapped(jid:replace_resource(StateData#state.jid,
+ Nick),
+ Info#user.jid, Packet2,
+ ?NS_MUCSUB_NODES_PRESENCE,
+ StateData);
true -> ok
- end
+ end;
+ (_) ->
+ ok
end,
(?DICT):to_list(StateData#state.users)).
@@ -2731,13 +2887,7 @@ process_admin_items_set(UJID, Items, Lang, StateData) ->
jid:to_string(StateData#state.jid), Res]),
NSD = lists:foldl(process_item_change(UJID),
StateData, lists:flatten(Res)),
- case (NSD#state.config)#config.persistent of
- true ->
- mod_muc:store_room(NSD#state.server_host,
- NSD#state.host, NSD#state.room,
- make_opts(NSD));
- _ -> ok
- end,
+ store_room(NSD),
{result, [], NSD};
Err -> Err
end.
@@ -3194,9 +3344,18 @@ send_kickban_presence1(MJID, UJID, Reason, Code, Affiliation,
Code}],
children =
[]}]}]},
- ejabberd_router:route(jid:replace_resource(StateData#state.jid,
- Nick),
- Info#user.jid, Packet)
+ RoomJIDNick = jid:replace_resource(
+ StateData#state.jid, Nick),
+ send_wrapped(RoomJIDNick, Info#user.jid, Packet,
+ ?NS_MUCSUB_NODES_AFFILIATIONS, StateData),
+ IsSubscriber = Info#user.is_subscriber,
+ IsOccupant = Info#user.last_presence /= undefined,
+ if (IsSubscriber and not IsOccupant) ->
+ send_wrapped(RoomJIDNick, Info#user.jid, Packet,
+ ?NS_MUCSUB_NODES_PARTICIPANTS, StateData);
+ true ->
+ ok
+ end
end,
(?DICT):to_list(StateData#state.users)).
@@ -3686,6 +3845,9 @@ get_config(Lang, StateData, From) ->
?BOOLXFIELD(<<"Allow visitors to send voice requests">>,
<<"muc#roomconfig_allowvoicerequests">>,
(Config#config.allow_voice_requests)),
+ ?BOOLXFIELD(<<"Allow subscription">>,
+ <<"muc#roomconfig_allow_subscription">>,
+ (Config#config.allow_subscription)),
?STRINGXFIELD(<<"Minimum interval between voice requests "
"(in seconds)">>,
<<"muc#roomconfig_voicerequestmininterval">>,
@@ -3698,14 +3860,6 @@ get_config(Lang, StateData, From) ->
(Config#config.captcha_protected))];
false -> []
end ++
- case gen_mod:is_loaded(StateData#state.server_host, mod_mam) of
- true ->
- [?BOOLXFIELD(<<"Enable message archiving">>,
- <<"muc#roomconfig_mam">>,
- (Config#config.mam))];
- false -> []
- end
- ++
[?JIDMULTIXFIELD(<<"Exclude Jabber IDs from CAPTCHA challenge">>,
<<"muc#roomconfig_captcha_whitelist">>,
((?SETS):to_list(Config#config.captcha_whitelist)))]
@@ -3720,6 +3874,10 @@ get_config(Lang, StateData, From) ->
(Config#config.logging))];
_ -> []
end,
+ X = ejabberd_hooks:run_fold(get_room_config,
+ StateData#state.server_host,
+ Res,
+ [StateData, From, Lang]),
{result,
[#xmlel{name = <<"instructions">>, attrs = [],
children =
@@ -3730,7 +3888,7 @@ get_config(Lang, StateData, From) ->
#xmlel{name = <<"x">>,
attrs =
[{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"form">>}],
- children = Res}],
+ children = X}],
StateData}.
set_config(XEl, StateData, Lang) ->
@@ -3738,7 +3896,8 @@ set_config(XEl, StateData, Lang) ->
case XData of
invalid -> {error, ?ERRT_BAD_REQUEST(Lang, <<"Incorrect data form">>)};
_ ->
- case set_xoption(XData, StateData#state.config) of
+ case set_xoption(XData, StateData#state.config,
+ StateData#state.server_host, Lang) of
#config{} = Config ->
Res = change_config(Config, StateData),
{result, _, NSD} = Res,
@@ -3760,30 +3919,30 @@ set_config(XEl, StateData, Lang) ->
-define(SET_BOOL_XOPT(Opt, Val),
case Val of
<<"0">> ->
- set_xoption(Opts, Config#config{Opt = false});
+ set_xoption(Opts, Config#config{Opt = false}, ServerHost, Lang);
<<"false">> ->
- set_xoption(Opts, Config#config{Opt = false});
- <<"1">> -> set_xoption(Opts, Config#config{Opt = true});
+ set_xoption(Opts, Config#config{Opt = false}, ServerHost, Lang);
+ <<"1">> -> set_xoption(Opts, Config#config{Opt = true}, ServerHost, Lang);
<<"true">> ->
- set_xoption(Opts, Config#config{Opt = true});
+ set_xoption(Opts, Config#config{Opt = true}, ServerHost, Lang);
_ ->
Txt = <<"Value of '~s' should be boolean">>,
ErrTxt = iolist_to_binary(io_lib:format(Txt, [Opt])),
- {error, ?ERRT_BAD_REQUEST(?MYLANG, ErrTxt)}
+ {error, ?ERRT_BAD_REQUEST(Lang, ErrTxt)}
end).
-define(SET_NAT_XOPT(Opt, Val),
case catch jlib:binary_to_integer(Val) of
I when is_integer(I), I > 0 ->
- set_xoption(Opts, Config#config{Opt = I});
+ set_xoption(Opts, Config#config{Opt = I}, ServerHost, Lang);
_ ->
Txt = <<"Value of '~s' should be integer">>,
ErrTxt = iolist_to_binary(io_lib:format(Txt, [Opt])),
- {error, ?ERRT_BAD_REQUEST(?MYLANG, ErrTxt)}
+ {error, ?ERRT_BAD_REQUEST(Lang, ErrTxt)}
end).
-define(SET_STRING_XOPT(Opt, Val),
- set_xoption(Opts, Config#config{Opt = Val})).
+ set_xoption(Opts, Config#config{Opt = Val}, ServerHost, Lang)).
-define(SET_JIDMULTI_XOPT(Opt, Vals),
begin
@@ -3795,33 +3954,33 @@ set_config(XEl, StateData, Lang) ->
(_, Set1) -> Set1
end,
(?SETS):empty(), Vals),
- set_xoption(Opts, Config#config{Opt = Set})
+ set_xoption(Opts, Config#config{Opt = Set}, ServerHost, Lang)
end).
-set_xoption([], Config) -> Config;
+set_xoption([], Config, _ServerHost, _Lang) -> Config;
set_xoption([{<<"muc#roomconfig_roomname">>, [Val]}
| Opts],
- Config) ->
+ Config, ServerHost, Lang) ->
?SET_STRING_XOPT(title, Val);
set_xoption([{<<"muc#roomconfig_roomdesc">>, [Val]}
| Opts],
- Config) ->
+ Config, ServerHost, Lang) ->
?SET_STRING_XOPT(description, Val);
set_xoption([{<<"muc#roomconfig_changesubject">>, [Val]}
| Opts],
- Config) ->
+ Config, ServerHost, Lang) ->
?SET_BOOL_XOPT(allow_change_subj, Val);
set_xoption([{<<"allow_query_users">>, [Val]} | Opts],
- Config) ->
+ Config, ServerHost, Lang) ->
?SET_BOOL_XOPT(allow_query_users, Val);
set_xoption([{<<"allow_private_messages">>, [Val]}
| Opts],
- Config) ->
+ Config, ServerHost, Lang) ->
?SET_BOOL_XOPT(allow_private_messages, Val);
set_xoption([{<<"allow_private_messages_from_visitors">>,
[Val]}
| Opts],
- Config) ->
+ Config, ServerHost, Lang) ->
case Val of
<<"anyone">> ->
?SET_STRING_XOPT(allow_private_messages_from_visitors,
@@ -3835,62 +3994,66 @@ set_xoption([{<<"allow_private_messages_from_visitors">>,
_ ->
Txt = <<"Value of 'allow_private_messages_from_visitors' "
"should be anyone|moderators|nobody">>,
- {error, ?ERRT_BAD_REQUEST(?MYLANG, Txt)}
+ {error, ?ERRT_BAD_REQUEST(Lang, Txt)}
end;
set_xoption([{<<"muc#roomconfig_allowvisitorstatus">>,
[Val]}
| Opts],
- Config) ->
+ Config, ServerHost, Lang) ->
?SET_BOOL_XOPT(allow_visitor_status, Val);
set_xoption([{<<"muc#roomconfig_allowvisitornickchange">>,
[Val]}
| Opts],
- Config) ->
+ Config, ServerHost, Lang) ->
?SET_BOOL_XOPT(allow_visitor_nickchange, Val);
set_xoption([{<<"muc#roomconfig_publicroom">>, [Val]}
| Opts],
- Config) ->
+ Config, ServerHost, Lang) ->
?SET_BOOL_XOPT(public, Val);
set_xoption([{<<"public_list">>, [Val]} | Opts],
- Config) ->
+ Config, ServerHost, Lang) ->
?SET_BOOL_XOPT(public_list, Val);
set_xoption([{<<"muc#roomconfig_persistentroom">>,
[Val]}
| Opts],
- Config) ->
+ Config, ServerHost, Lang) ->
?SET_BOOL_XOPT(persistent, Val);
set_xoption([{<<"muc#roomconfig_moderatedroom">>, [Val]}
| Opts],
- Config) ->
+ Config, ServerHost, Lang) ->
?SET_BOOL_XOPT(moderated, Val);
set_xoption([{<<"members_by_default">>, [Val]} | Opts],
- Config) ->
+ Config, ServerHost, Lang) ->
?SET_BOOL_XOPT(members_by_default, Val);
set_xoption([{<<"muc#roomconfig_membersonly">>, [Val]}
| Opts],
- Config) ->
+ Config, ServerHost, Lang) ->
?SET_BOOL_XOPT(members_only, Val);
set_xoption([{<<"captcha_protected">>, [Val]} | Opts],
- Config) ->
+ Config, ServerHost, Lang) ->
?SET_BOOL_XOPT(captcha_protected, Val);
set_xoption([{<<"muc#roomconfig_allowinvites">>, [Val]}
| Opts],
- Config) ->
+ Config, ServerHost, Lang) ->
?SET_BOOL_XOPT(allow_user_invites, Val);
+set_xoption([{<<"muc#roomconfig_allow_subscription">>, [Val]}
+ | Opts],
+ Config, ServerHost, Lang) ->
+ ?SET_BOOL_XOPT(allow_subscription, Val);
set_xoption([{<<"muc#roomconfig_passwordprotectedroom">>,
[Val]}
| Opts],
- Config) ->
+ Config, ServerHost, Lang) ->
?SET_BOOL_XOPT(password_protected, Val);
set_xoption([{<<"muc#roomconfig_roomsecret">>, [Val]}
| Opts],
- Config) ->
+ Config, ServerHost, Lang) ->
?SET_STRING_XOPT(password, Val);
set_xoption([{<<"anonymous">>, [Val]} | Opts],
- Config) ->
+ Config, ServerHost, Lang) ->
?SET_BOOL_XOPT(anonymous, Val);
set_xoption([{<<"muc#roomconfig_presencebroadcast">>, Vals} | Opts],
- Config) ->
+ Config, ServerHost, Lang) ->
Roles =
lists:foldl(
fun(_S, error) -> error;
@@ -3906,27 +4069,28 @@ set_xoption([{<<"muc#roomconfig_presencebroadcast">>, Vals} | Opts],
error ->
Txt = <<"Value of 'muc#roomconfig_presencebroadcast' should "
"be moderator|participant|visitor">>,
- {error, ?ERRT_BAD_REQUEST(?MYLANG, Txt)};
+ {error, ?ERRT_BAD_REQUEST(Lang, Txt)};
{M, P, V} ->
Res =
if M -> [moderator]; true -> [] end ++
if P -> [participant]; true -> [] end ++
if V -> [visitor]; true -> [] end,
- set_xoption(Opts, Config#config{presence_broadcast = Res})
+ set_xoption(Opts, Config#config{presence_broadcast = Res},
+ ServerHost, Lang)
end;
set_xoption([{<<"muc#roomconfig_allowvoicerequests">>,
[Val]}
| Opts],
- Config) ->
+ Config, ServerHost, Lang) ->
?SET_BOOL_XOPT(allow_voice_requests, Val);
set_xoption([{<<"muc#roomconfig_voicerequestmininterval">>,
[Val]}
| Opts],
- Config) ->
+ Config, ServerHost, Lang) ->
?SET_NAT_XOPT(voice_request_min_interval, Val);
set_xoption([{<<"muc#roomconfig_whois">>, [Val]}
| Opts],
- Config) ->
+ Config, ServerHost, Lang) ->
case Val of
<<"moderators">> ->
?SET_BOOL_XOPT(anonymous,
@@ -3937,37 +4101,44 @@ set_xoption([{<<"muc#roomconfig_whois">>, [Val]}
_ ->
Txt = <<"Value of 'muc#roomconfig_whois' should be "
"moderators|anyone">>,
- {error, ?ERRT_BAD_REQUEST(?MYLANG, Txt)}
+ {error, ?ERRT_BAD_REQUEST(Lang, Txt)}
end;
set_xoption([{<<"muc#roomconfig_maxusers">>, [Val]}
| Opts],
- Config) ->
+ Config, ServerHost, Lang) ->
case Val of
<<"none">> -> ?SET_STRING_XOPT(max_users, none);
_ -> ?SET_NAT_XOPT(max_users, Val)
end;
set_xoption([{<<"muc#roomconfig_enablelogging">>, [Val]}
| Opts],
- Config) ->
+ Config, ServerHost, Lang) ->
?SET_BOOL_XOPT(logging, Val);
-set_xoption([{<<"muc#roomconfig_mam">>, [Val]}|Opts], Config) ->
- ?SET_BOOL_XOPT(mam, Val);
set_xoption([{<<"muc#roomconfig_captcha_whitelist">>,
Vals}
| Opts],
- Config) ->
+ Config, ServerHost, Lang) ->
JIDs = [jid:from_string(Val) || Val <- Vals],
?SET_JIDMULTI_XOPT(captcha_whitelist, JIDs);
-set_xoption([{<<"FORM_TYPE">>, _} | Opts], Config) ->
- set_xoption(Opts, Config);
-set_xoption([{Opt, _Vals} | _Opts], _Config) ->
+set_xoption([{<<"FORM_TYPE">>, _} | Opts], Config, ServerHost, Lang) ->
+ set_xoption(Opts, Config, ServerHost, Lang);
+set_xoption([{Opt, Vals} | Opts], Config, ServerHost, Lang) ->
Txt = <<"Unknown option '~s'">>,
ErrTxt = iolist_to_binary(io_lib:format(Txt, [Opt])),
- {error, ?ERRT_BAD_REQUEST(?MYLANG, ErrTxt)}.
+ Err = {error, ?ERRT_BAD_REQUEST(Lang, ErrTxt)},
+ case ejabberd_hooks:run_fold(set_room_option,
+ ServerHost,
+ Err,
+ [Opt, Vals, Lang]) of
+ {error, Reason} ->
+ {error, Reason};
+ {Pos, Val} ->
+ set_xoption(Opts, setelement(Pos, Config, Val), ServerHost, Lang)
+ end.
change_config(Config, StateData) ->
send_config_change_info(Config, StateData),
- NSD = StateData#state{config = Config},
+ NSD = remove_subscriptions(StateData#state{config = Config}),
case {(StateData#state.config)#config.persistent,
Config#config.persistent}
of
@@ -4015,10 +4186,11 @@ send_config_change_info(New, #state{config = Old} = StateData) ->
children = [#xmlel{name = <<"x">>,
attrs = [{<<"xmlns">>, ?NS_MUC_USER}],
children = StatusEls}]},
- send_multiple(StateData#state.jid,
- StateData#state.server_host,
- StateData#state.users,
- Message).
+ send_wrapped_multiple(StateData#state.jid,
+ StateData#state.users,
+ Message,
+ ?NS_MUCSUB_NODES_CONFIG,
+ StateData).
remove_nonmembers(StateData) ->
lists:foldl(fun ({_LJID, #user{jid = JID}}, SD) ->
@@ -4148,6 +4320,18 @@ set_opts([{Opt, Val} | Opts], StateData) ->
StateData#state{config =
(StateData#state.config)#config{vcard =
Val}};
+ allow_subscription ->
+ StateData#state{config =
+ (StateData#state.config)#config{allow_subscription = Val}};
+ subscribers ->
+ lists:foldl(
+ fun({JID, Nick, Nodes}, State) ->
+ User = #user{jid = JID, nick = Nick,
+ subscriptions = Nodes,
+ is_subscriber = true,
+ role = none},
+ update_online_user(JID, User, State)
+ end, StateData, Val);
affiliations ->
StateData#state{affiliations = (?DICT):from_list(Val)};
subject -> StateData#state{subject = Val};
@@ -4161,6 +4345,13 @@ set_opts([{Opt, Val} | Opts], StateData) ->
make_opts(StateData) ->
Config = StateData#state.config,
+ Subscribers = (?DICT):fold(
+ fun(_LJID, #user{is_subscriber = true} = User, Acc) ->
+ [{User#user.jid, User#user.nick,
+ User#user.subscriptions}|Acc];
+ (_, _, Acc) ->
+ Acc
+ end, [], StateData#state.users),
[?MAKE_CONFIG_OPT(title), ?MAKE_CONFIG_OPT(description),
?MAKE_CONFIG_OPT(allow_change_subj),
?MAKE_CONFIG_OPT(allow_query_users),
@@ -4187,7 +4378,8 @@ make_opts(StateData) ->
{affiliations,
(?DICT):to_list(StateData#state.affiliations)},
{subject, StateData#state.subject},
- {subject_author, StateData#state.subject_author}].
+ {subject_author, StateData#state.subject_author},
+ {subscribers, Subscribers}].
destroy_room(DEl, StateData) ->
lists:foreach(fun ({_LJID, Info}) ->
@@ -4210,9 +4402,10 @@ destroy_room(DEl, StateData) ->
children =
[]},
DEl]}]},
- ejabberd_router:route(jid:replace_resource(StateData#state.jid,
- Nick),
- Info#user.jid, Packet)
+ send_wrapped(jid:replace_resource(StateData#state.jid,
+ Nick),
+ Info#user.jid, Packet,
+ ?NS_MUCSUB_NODES_CONFIG, StateData)
end,
(?DICT):to_list(StateData#state.users)),
case (StateData#state.config)#config.persistent of
@@ -4264,6 +4457,10 @@ process_iq_disco_info(_From, get, Lang, StateData) ->
<<"muc_moderated">>, <<"muc_unmoderated">>),
?CONFIG_OPT_TO_FEATURE((Config#config.password_protected),
<<"muc_passwordprotected">>, <<"muc_unsecured">>)]
+ ++ case Config#config.allow_subscription of
+ true -> [?FEATURE(?NS_MUCSUB)];
+ false -> []
+ end
++ case {gen_mod:is_loaded(StateData#state.server_host, mod_mam),
Config#config.mam} of
{true, true} ->
@@ -4361,6 +4558,118 @@ process_iq_vcard(From, set, Lang, SubEl, StateData) ->
{error, ?ERRT_FORBIDDEN(Lang, ErrText)}
end.
+process_iq_mucsub(From, Packet,
+ #iq{type = set, lang = Lang,
+ sub_el = #xmlel{name = <<"subscribe">>} = SubEl},
+ #state{config = Config} = StateData) ->
+ case fxml:get_tag_attr_s(<<"nick">>, SubEl) of
+ <<"">> ->
+ Err = ?ERRT_BAD_REQUEST(Lang, <<"Missing 'nick' attribute">>),
+ {error, Err};
+ Nick when Config#config.allow_subscription ->
+ LJID = jid:tolower(From),
+ case (?DICT):find(LJID, StateData#state.users) of
+ {ok, #user{role = Role, nick = Nick1}} when Nick1 /= Nick ->
+ Nodes = get_subscription_nodes(Packet),
+ case {nick_collision(From, Nick, StateData),
+ mod_muc:can_use_nick(StateData#state.server_host,
+ StateData#state.host,
+ From, Nick)} of
+ {true, _} ->
+ ErrText = <<"That nickname is already in use by another occupant">>,
+ {error, ?ERRT_CONFLICT(Lang, ErrText)};
+ {_, false} ->
+ ErrText = <<"That nickname is registered by another person">>,
+ {error, ?ERRT_CONFLICT(Lang, ErrText)};
+ _ ->
+ NewStateData = add_online_user(
+ From, Nick, Role, true, Nodes, StateData),
+ {result, subscription_nodes_to_events(Nodes), NewStateData}
+ end;
+ {ok, #user{role = Role}} ->
+ Nodes = get_subscription_nodes(Packet),
+ NewStateData = add_online_user(
+ From, Nick, Role, true, Nodes, StateData),
+ {result, subscription_nodes_to_events(Nodes), NewStateData};
+ error ->
+ add_new_user(From, Nick, Packet, StateData)
+ end;
+ _ ->
+ Err = ?ERRT_NOT_ALLOWED(Lang, <<"Subscriptions are not allowed">>),
+ {error, Err}
+ end;
+process_iq_mucsub(From, _Packet,
+ #iq{type = set,
+ sub_el = #xmlel{name = <<"unsubscribe">>}},
+ StateData) ->
+ LJID = jid:tolower(From),
+ case ?DICT:find(LJID, StateData#state.users) of
+ {ok, #user{is_subscriber = true} = User} ->
+ NewStateData = remove_subscription(From, User, StateData),
+ store_room(NewStateData),
+ {result, [], NewStateData};
+ _ ->
+ {result, [], StateData}
+ end;
+process_iq_mucsub(_From, _Packet, #iq{type = set, lang = Lang}, _StateData) ->
+ Txt = <<"Unrecognized subscription command">>,
+ {error, ?ERRT_BAD_REQUEST(Lang, Txt)};
+process_iq_mucsub(_From, _Packet, #iq{type = get, lang = Lang}, _StateData) ->
+ Txt = <<"Value 'get' of 'type' attribute is not allowed">>,
+ {error, ?ERRT_BAD_REQUEST(Lang, Txt)}.
+
+remove_subscription(JID, #user{is_subscriber = true} = User, StateData) ->
+ case User#user.last_presence of
+ undefined ->
+ remove_online_user(JID, StateData, false);
+ _ ->
+ LJID = jid:tolower(JID),
+ Users = ?DICT:store(LJID, User#user{is_subscriber = false},
+ StateData#state.users),
+ StateData#state{users = Users}
+ end;
+remove_subscription(_JID, #user{}, StateData) ->
+ StateData.
+
+remove_subscriptions(StateData) ->
+ if not (StateData#state.config)#config.allow_subscription ->
+ dict:fold(
+ fun(_LJID, User, State) ->
+ remove_subscription(User#user.jid, User, State)
+ end, StateData, StateData#state.users);
+ true ->
+ StateData
+ end.
+
+get_subscription_nodes(#xmlel{name = <<"iq">>} = Packet) ->
+ case fxml:get_subtag_with_xmlns(Packet, <<"subscribe">>, ?NS_MUCSUB) of
+ #xmlel{children = Els} ->
+ lists:flatmap(
+ fun(#xmlel{name = <<"event">>, attrs = Attrs}) ->
+ Node = fxml:get_attr_s(<<"node">>, Attrs),
+ case lists:member(Node, [?NS_MUCSUB_NODES_PRESENCE,
+ ?NS_MUCSUB_NODES_MESSAGES,
+ ?NS_MUCSUB_NODES_AFFILIATIONS,
+ ?NS_MUCSUB_NODES_SUBJECT,
+ ?NS_MUCSUB_NODES_CONFIG,
+ ?NS_MUCSUB_NODES_PARTICIPANTS]) of
+ true ->
+ [Node];
+ false ->
+ []
+ end;
+ (_) ->
+ []
+ end, Els);
+ false ->
+ []
+ end;
+get_subscription_nodes(_) ->
+ [].
+
+subscription_nodes_to_events(Nodes) ->
+ [#xmlel{name = <<"event">>, attrs = [{<<"node">>, Node}]} || Node <- Nodes].
+
get_title(StateData) ->
case (StateData#state.config)#config.title of
<<"">> -> StateData#state.room;
@@ -4484,9 +4793,9 @@ send_voice_request(From, StateData) ->
Moderators = search_role(moderator, StateData),
FromNick = find_nick_by_jid(From, StateData),
lists:foreach(fun ({_, User}) ->
- ejabberd_router:route(StateData#state.jid, User#user.jid,
- prepare_request_form(From, FromNick,
- <<"">>))
+ ejabberd_router:route(
+ StateData#state.jid, User#user.jid,
+ prepare_request_form(From, FromNick, <<"">>))
end,
Moderators).
@@ -4777,6 +5086,46 @@ tab_count_user(JID) ->
element_size(El) ->
byte_size(fxml:element_to_binary(El)).
+store_room(StateData) ->
+ if (StateData#state.config)#config.persistent ->
+ mod_muc:store_room(StateData#state.server_host,
+ StateData#state.host, StateData#state.room,
+ make_opts(StateData));
+ true ->
+ ok
+ end.
+
+send_wrapped(From, To, Packet, Node, State) ->
+ LTo = jid:tolower(To),
+ case ?DICT:find(LTo, State#state.users) of
+ {ok, #user{is_subscriber = true,
+ subscriptions = Nodes,
+ last_presence = undefined}} ->
+ case lists:member(Node, Nodes) of
+ true ->
+ NewPacket = wrap(From, To, Packet, Node),
+ ejabberd_router:route(State#state.jid, To, NewPacket);
+ false ->
+ ok
+ end;
+ _ ->
+ ejabberd_router:route(From, To, Packet)
+ end.
+
+wrap(From, To, Packet, Node) ->
+ Pkt1 = jlib:replace_from_to(From, To, Packet),
+ Pkt2 = #xmlel{attrs = Attrs} = jlib:remove_attr(<<"xmlns">>, Pkt1),
+ Pkt3 = Pkt2#xmlel{attrs = [{<<"xmlns">>, <<"jabber:client">>}|Attrs]},
+ Item = #xmlel{name = <<"item">>,
+ attrs = [{<<"id">>, randoms:get_string()}],
+ children = [Pkt3]},
+ Items = #xmlel{name = <<"items">>, attrs = [{<<"node">>, Node}],
+ children = [Item]},
+ Event = #xmlel{name = <<"event">>,
+ attrs = [{<<"xmlns">>, ?NS_PUBSUB_EVENT}],
+ children = [Items]},
+ #xmlel{name = <<"message">>, children = [Event]}.
+
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Multicast
@@ -4784,6 +5133,12 @@ send_multiple(From, Server, Users, Packet) ->
JIDs = [ User#user.jid || {_, User} <- ?DICT:to_list(Users)],
ejabberd_router_multicast:route_multicast(From, Server, JIDs, Packet).
+send_wrapped_multiple(From, Users, Packet, Node, State) ->
+ lists:foreach(
+ fun({_, #user{jid = To}}) ->
+ send_wrapped(From, To, Packet, Node, State)
+ end, ?DICT:to_list(Users)).
+
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Detect messange stanzas that don't have meaninful content
diff --git a/src/mod_multicast.erl b/src/mod_multicast.erl
index cbe2a4e50..df385c28c 100644
--- a/src/mod_multicast.erl
+++ b/src/mod_multicast.erl
@@ -40,7 +40,7 @@
-export([init/1, handle_info/2, handle_call/3,
handle_cast/2, terminate/2, code_change/3]).
--export([purge_loop/1, mod_opt_type/1]).
+-export([purge_loop/1, mod_opt_type/1, depends/2]).
-include("ejabberd.hrl").
-include("logger.hrl").
@@ -1219,6 +1219,9 @@ stj(String) -> jid:from_string(String).
jts(String) -> jid:to_string(String).
+depends(_Host, _Opts) ->
+ [].
+
mod_opt_type(access) ->
fun acl:access_rules_validator/1;
mod_opt_type(host) -> fun iolist_to_binary/1;
diff --git a/src/mod_offline.erl b/src/mod_offline.erl
index a794a0371..799605c69 100644
--- a/src/mod_offline.erl
+++ b/src/mod_offline.erl
@@ -66,7 +66,7 @@
-export([init/1, handle_call/3, handle_cast/2,
handle_info/2, terminate/2, code_change/3,
- mod_opt_type/1]).
+ mod_opt_type/1, depends/2]).
-deprecated({get_queue_length,2}).
@@ -125,6 +125,8 @@ stop(Host) ->
supervisor:delete_child(ejabberd_sup, Proc),
ok.
+depends(_Host, _Opts) ->
+ [].
%%====================================================================
%% gen_server callbacks
@@ -791,7 +793,7 @@ get_messages_subset(User, Host, MsgsAll) ->
fun(A) when is_atom(A) -> A end,
max_user_offline_messages),
MaxOfflineMsgs = case get_max_user_messages(Access,
- {User, Host}, Host)
+ User, Host)
of
Number when is_integer(Number) -> Number;
_ -> 100
diff --git a/src/mod_ping.erl b/src/mod_ping.erl
index f1c175a91..d1b3f9322 100644
--- a/src/mod_ping.erl
+++ b/src/mod_ping.erl
@@ -55,7 +55,7 @@
handle_cast/2, handle_info/2, code_change/3]).
-export([iq_ping/3, user_online/3, user_offline/3,
- user_send/4, mod_opt_type/1]).
+ user_send/4, mod_opt_type/1, depends/2]).
-record(state,
{host = <<"">>,
@@ -253,6 +253,9 @@ cancel_timer(TRef) ->
_ -> ok
end.
+depends(_Host, _Opts) ->
+ [].
+
mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1;
mod_opt_type(ping_interval) ->
fun (I) when is_integer(I), I > 0 -> I end;
diff --git a/src/mod_pres_counter.erl b/src/mod_pres_counter.erl
index 1118b7bbc..e6f2cfbab 100644
--- a/src/mod_pres_counter.erl
+++ b/src/mod_pres_counter.erl
@@ -28,7 +28,7 @@
-behavior(gen_mod).
-export([start/2, stop/1, check_packet/6,
- mod_opt_type/1]).
+ mod_opt_type/1, depends/2]).
-include("ejabberd.hrl").
-include("logger.hrl").
@@ -48,6 +48,9 @@ stop(Host) ->
?MODULE, check_packet, 25),
ok.
+depends(_Host, _Opts) ->
+ [].
+
check_packet(_, _User, Server, _PrivacyList,
{From, To, #xmlel{name = Name, attrs = Attrs}}, Dir) ->
case Name of
diff --git a/src/mod_privacy.erl b/src/mod_privacy.erl
index ad13c27cd..18ff78371 100644
--- a/src/mod_privacy.erl
+++ b/src/mod_privacy.erl
@@ -36,7 +36,7 @@
check_packet/6, remove_user/2,
is_list_needdb/1, updated_list/3,
item_to_xml/1, get_user_lists/2, import/3,
- set_privacy_list/1, mod_opt_type/1]).
+ set_privacy_list/1, mod_opt_type/1, depends/2]).
-include("ejabberd.hrl").
-include("logger.hrl").
@@ -593,6 +593,9 @@ import(LServer, DBType, Data) ->
Mod = gen_mod:db_mod(DBType, ?MODULE),
Mod:import(LServer, Data).
+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].
diff --git a/src/mod_private.erl b/src/mod_private.erl
index 38e42ca4c..f0e4632f6 100644
--- a/src/mod_private.erl
+++ b/src/mod_private.erl
@@ -33,7 +33,7 @@
-export([start/2, stop/1, process_sm_iq/3, import/3,
remove_user/2, get_data/2, export/1, import/1,
- mod_opt_type/1, set_data/3]).
+ mod_opt_type/1, set_data/3, depends/2]).
-include("ejabberd.hrl").
-include("logger.hrl").
@@ -173,6 +173,9 @@ import(LServer, DBType, PD) ->
Mod = gen_mod:db_mod(DBType, ?MODULE),
Mod:import(LServer, PD).
+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].
diff --git a/src/mod_proxy65.erl b/src/mod_proxy65.erl
index 2737de7ad..beea35725 100644
--- a/src/mod_proxy65.erl
+++ b/src/mod_proxy65.erl
@@ -39,7 +39,7 @@
%% supervisor callbacks.
-export([init/1]).
--export([start_link/2, mod_opt_type/1]).
+-export([start_link/2, mod_opt_type/1, depends/2]).
-define(PROCNAME, ejabberd_mod_proxy65).
@@ -84,6 +84,9 @@ init([Host, Opts]) ->
{{one_for_one, 10, 1},
[StreamManager, StreamSupervisor, Service]}}.
+depends(_Host, _Opts) ->
+ [].
+
mod_opt_type(auth_type) ->
fun (plain) -> plain;
(anonymous) -> anonymous
diff --git a/src/mod_pubsub.erl b/src/mod_pubsub.erl
index 53378c355..81afc9a06 100644
--- a/src/mod_pubsub.erl
+++ b/src/mod_pubsub.erl
@@ -77,7 +77,7 @@
%% API and gen_server callbacks
-export([start_link/2, start/2, stop/1, init/1,
handle_call/3, handle_cast/2, handle_info/2,
- terminate/2, code_change/3]).
+ terminate/2, code_change/3, depends/2]).
-export([send_loop/1, mod_opt_type/1]).
@@ -347,6 +347,18 @@ init_send_loop(ServerHost) ->
end,
{Pid, State}.
+depends(ServerHost, Opts) ->
+ Host = gen_mod:get_opt_host(ServerHost, Opts, <<"pubsub.@HOST@">>),
+ Plugins = gen_mod:get_opt(plugins, Opts,
+ fun(A) when is_list(A) -> A end, [?STDNODE]),
+ lists:flatmap(
+ fun(Name) ->
+ Plugin = plugin(ServerHost, Name),
+ try apply(Plugin, depends, [Host, ServerHost, Opts])
+ catch _:undef -> []
+ end
+ end, Plugins).
+
%% @doc Call the init/1 function for each plugin declared in the config file.
%% The default plugin module is implicit.
%% <p>The Erlang code for the plugin is located in a module called
@@ -406,7 +418,8 @@ send_loop(State) ->
{_, Node} = NodeRec#pubsub_node.nodeid,
Nidx = NodeRec#pubsub_node.id,
Options = NodeRec#pubsub_node.options,
- send_items(Host, Node, Nidx, PType, Options, SubJID, last)
+ [send_items(Host, Node, Nidx, PType, Options, SubJID, last)
+ || NodeRec#pubsub_node.type == PType]
end,
lists:usort(Subs))
end,
diff --git a/src/mod_register.erl b/src/mod_register.erl
index 43499d2e5..45cd78fef 100644
--- a/src/mod_register.erl
+++ b/src/mod_register.erl
@@ -37,7 +37,7 @@
unauthenticated_iq_register/4, try_register/5,
process_iq/3, send_registration_notifications/3,
transform_options/1, transform_module_options/1,
- mod_opt_type/1, opt_type/1]).
+ mod_opt_type/1, opt_type/1, depends/2]).
-include("ejabberd.hrl").
-include("logger.hrl").
@@ -72,6 +72,9 @@ stop(Host) ->
gen_iq_handler:remove_iq_handler(ejabberd_sm, Host,
?NS_REGISTER).
+depends(_Host, _Opts) ->
+ [].
+
stream_feature_register(Acc, Host) ->
AF = gen_mod:get_module_opt(Host, ?MODULE, access_from,
fun(A) -> A end,
diff --git a/src/mod_register_web.erl b/src/mod_register_web.erl
index 2b7e5f532..76de1677f 100644
--- a/src/mod_register_web.erl
+++ b/src/mod_register_web.erl
@@ -55,7 +55,7 @@
-behaviour(gen_mod).
--export([start/2, stop/1, process/2, mod_opt_type/1]).
+-export([start/2, stop/1, process/2, mod_opt_type/1, depends/2]).
-include("ejabberd.hrl").
-include("logger.hrl").
@@ -76,6 +76,9 @@ start(_Host, _Opts) ->
stop(_Host) -> ok.
+depends(_Host, _Opts) ->
+ [{mod_register, hard}].
+
%%%----------------------------------------------------------------------
%%% HTTP handlers
%%%----------------------------------------------------------------------
diff --git a/src/mod_roster.erl b/src/mod_roster.erl
index f79061560..a75041bc7 100644
--- a/src/mod_roster.erl
+++ b/src/mod_roster.erl
@@ -49,7 +49,7 @@
get_jid_info/4, item_to_xml/1, webadmin_page/3,
webadmin_user/4, get_versioning_feature/2,
roster_versioning_enabled/1, roster_version/2,
- mod_opt_type/1, set_roster/1]).
+ mod_opt_type/1, set_roster/1, depends/2]).
-include("ejabberd.hrl").
-include("logger.hrl").
@@ -136,6 +136,9 @@ stop(Host) ->
gen_iq_handler:remove_iq_handler(ejabberd_sm, Host,
?NS_ROSTER).
+depends(_Host, _Opts) ->
+ [].
+
process_iq(From, To, IQ) when ((From#jid.luser == <<"">>) andalso (From#jid.resource == <<"">>)) ->
process_iq_manager(From, To, IQ);
diff --git a/src/mod_roster_sql.erl b/src/mod_roster_sql.erl
index 899978091..61f59a990 100644
--- a/src/mod_roster_sql.erl
+++ b/src/mod_roster_sql.erl
@@ -244,31 +244,6 @@ raw_to_record(LServer,
askmessage = SAskMessage}
end.
-record_to_string(#roster{us = {User, _Server},
- jid = JID, name = Name, subscription = Subscription,
- ask = Ask, askmessage = AskMessage}) ->
- Username = ejabberd_sql:escape(User),
- SJID =
- ejabberd_sql:escape(jid:to_string(jid:tolower(JID))),
- Nick = ejabberd_sql:escape(Name),
- SSubscription = case Subscription of
- both -> <<"B">>;
- to -> <<"T">>;
- from -> <<"F">>;
- none -> <<"N">>
- end,
- SAsk = case Ask of
- subscribe -> <<"S">>;
- unsubscribe -> <<"U">>;
- both -> <<"B">>;
- out -> <<"O">>;
- in -> <<"I">>;
- none -> <<"N">>
- end,
- SAskMessage = ejabberd_sql:escape(AskMessage),
- [Username, SJID, Nick, SSubscription, SAsk, SAskMessage,
- <<"N">>, <<"">>, <<"item">>].
-
record_to_row(
#roster{us = {LUser, _LServer},
jid = JID, name = Name, subscription = Subscription,
diff --git a/src/mod_service_log.erl b/src/mod_service_log.erl
index 3e1da3a4e..ae264bbc9 100644
--- a/src/mod_service_log.erl
+++ b/src/mod_service_log.erl
@@ -30,7 +30,7 @@
-behaviour(gen_mod).
-export([start/2, stop/1, log_user_send/4,
- log_user_receive/5, mod_opt_type/1]).
+ log_user_receive/5, mod_opt_type/1, depends/2]).
-include("ejabberd.hrl").
-include("logger.hrl").
@@ -51,6 +51,9 @@ stop(Host) ->
?MODULE, log_user_receive, 50),
ok.
+depends(_Host, _Opts) ->
+ [].
+
log_user_send(Packet, _C2SState, From, To) ->
log_packet(From, To, Packet, From#jid.lserver),
Packet.
diff --git a/src/mod_shared_roster.erl b/src/mod_shared_roster.erl
index 76a619c9b..b472e1aab 100644
--- a/src/mod_shared_roster.erl
+++ b/src/mod_shared_roster.erl
@@ -39,7 +39,7 @@
delete_group/2, get_group_opts/2, set_group_opts/3,
get_group_users/2, get_group_explicit_users/2,
is_user_in_group/3, add_user_to_group/3, opts_to_binary/1,
- remove_user_from_group/3, mod_opt_type/1]).
+ remove_user_from_group/3, mod_opt_type/1, depends/2]).
-include("ejabberd.hrl").
-include("logger.hrl").
@@ -132,6 +132,9 @@ stop(Host) ->
%%ejabberd_hooks:delete(remove_user, Host,
%% ?MODULE, remove_user, 50),
+depends(_Host, _Opts) ->
+ [].
+
get_user_roster(Items, US) ->
{U, S} = US,
DisplayedGroups = get_user_displayed_groups(US),
diff --git a/src/mod_shared_roster_ldap.erl b/src/mod_shared_roster_ldap.erl
index 34588b90c..22f50d302 100644
--- a/src/mod_shared_roster_ldap.erl
+++ b/src/mod_shared_roster_ldap.erl
@@ -41,7 +41,7 @@
-export([get_user_roster/2, get_subscription_lists/3,
get_jid_info/4, process_item/2, in_subscription/6,
- out_subscription/4, mod_opt_type/1, opt_type/1]).
+ out_subscription/4, mod_opt_type/1, opt_type/1, depends/2]).
-include("ejabberd.hrl").
-include("logger.hrl").
@@ -105,6 +105,9 @@ stop(Host) ->
supervisor:terminate_child(ejabberd_sup, Proc),
supervisor:delete_child(ejabberd_sup, Proc).
+depends(_Host, _Opts) ->
+ [{mod_roster, hard}].
+
%%--------------------------------------------------------------------
%% Hooks
%%--------------------------------------------------------------------
diff --git a/src/mod_sic.erl b/src/mod_sic.erl
index b4eae0daf..49b65a0ee 100644
--- a/src/mod_sic.erl
+++ b/src/mod_sic.erl
@@ -32,7 +32,7 @@
-behaviour(gen_mod).
-export([start/2, stop/1, process_local_iq/3,
- process_sm_iq/3, mod_opt_type/1]).
+ process_sm_iq/3, mod_opt_type/1, depends/2]).
-include("ejabberd.hrl").
-include("logger.hrl").
@@ -55,6 +55,9 @@ stop(Host) ->
gen_iq_handler:remove_iq_handler(ejabberd_sm, Host,
?NS_SIC).
+depends(_Host, _Opts) ->
+ [].
+
process_local_iq(#jid{user = User, server = Server,
resource = Resource},
_To, #iq{type = get, sub_el = _SubEl} = IQ) ->
diff --git a/src/mod_sip.erl b/src/mod_sip.erl
index 6ffe56331..816100f47 100644
--- a/src/mod_sip.erl
+++ b/src/mod_sip.erl
@@ -34,7 +34,7 @@
-export([data_in/2, data_out/2, message_in/2,
message_out/2, request/2, request/3, response/2,
- locate/1, mod_opt_type/1]).
+ locate/1, mod_opt_type/1, depends/2]).
-include("ejabberd.hrl").
-include("logger.hrl").
@@ -62,6 +62,9 @@ start(_Host, _Opts) ->
stop(_Host) ->
ok.
+depends(_Host, _Opts) ->
+ [].
+
data_in(Data, #sip_socket{type = Transport,
addr = {MyIP, MyPort},
peer = {PeerIP, PeerPort}}) ->
diff --git a/src/mod_stats.erl b/src/mod_stats.erl
index c14cf8d15..99059839a 100644
--- a/src/mod_stats.erl
+++ b/src/mod_stats.erl
@@ -32,7 +32,7 @@
-behaviour(gen_mod).
-export([start/2, stop/1, process_local_iq/3,
- mod_opt_type/1]).
+ mod_opt_type/1, depends/2]).
-include("ejabberd.hrl").
-include("logger.hrl").
@@ -48,6 +48,9 @@ stop(Host) ->
gen_iq_handler:remove_iq_handler(ejabberd_local, Host,
?NS_STATS).
+depends(_Host, _Opts) ->
+ [].
+
process_local_iq(_From, To,
#iq{id = _ID, type = Type, xmlns = XMLNS,
sub_el = SubEl, lang = Lang} =
diff --git a/src/mod_time.erl b/src/mod_time.erl
index 740b654a7..90296f3d8 100644
--- a/src/mod_time.erl
+++ b/src/mod_time.erl
@@ -33,7 +33,7 @@
-behaviour(gen_mod).
-export([start/2, stop/1, process_local_iq/3,
- mod_opt_type/1]).
+ mod_opt_type/1, depends/2]).
-include("ejabberd.hrl").
-include("logger.hrl").
@@ -86,5 +86,8 @@ process_local_iq(_From, _To,
sign(N) when N < 0 -> <<"-">>;
sign(_) -> <<"+">>.
+depends(_Host, _Opts) ->
+ [].
+
mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1;
mod_opt_type(_) -> [iqdisc].
diff --git a/src/mod_vcard.erl b/src/mod_vcard.erl
index 5e042528b..aca9d7462 100644
--- a/src/mod_vcard.erl
+++ b/src/mod_vcard.erl
@@ -34,7 +34,7 @@
-export([start/2, init/3, stop/1, get_sm_features/5,
process_local_iq/3, process_sm_iq/3, string2lower/1,
- remove_user/2, export/1, import/1, import/3,
+ remove_user/2, export/1, import/1, import/3, depends/2,
mod_opt_type/1, set_vcard/3, make_vcard_search/4]).
-include("ejabberd.hrl").
@@ -594,6 +594,9 @@ import(LServer, DBType, VCard) ->
Mod = gen_mod:db_mod(DBType, ?MODULE),
Mod:import(LServer, VCard).
+depends(_Host, _Opts) ->
+ [].
+
mod_opt_type(allow_return_all) ->
fun (B) when is_boolean(B) -> B end;
mod_opt_type(db_type) -> fun(T) -> ejabberd_config:v_db(?MODULE, T) end;
diff --git a/src/mod_vcard_ldap.erl b/src/mod_vcard_ldap.erl
index 46f81af09..a0ad305a9 100644
--- a/src/mod_vcard_ldap.erl
+++ b/src/mod_vcard_ldap.erl
@@ -40,7 +40,7 @@
-export([start/2, start_link/2, stop/1,
get_sm_features/5, process_local_iq/3, process_sm_iq/3,
remove_user/1, route/4, transform_module_options/1,
- mod_opt_type/1, opt_type/1]).
+ mod_opt_type/1, opt_type/1, depends/2]).
-include("ejabberd.hrl").
-include("logger.hrl").
@@ -138,6 +138,9 @@ stop(Host) ->
supervisor:terminate_child(ejabberd_sup, Proc),
supervisor:delete_child(ejabberd_sup, Proc).
+depends(_Host, _Opts) ->
+ [].
+
terminate(_Reason, State) ->
Host = State#state.serverhost,
gen_iq_handler:remove_iq_handler(ejabberd_local, Host,
diff --git a/src/mod_vcard_xupdate.erl b/src/mod_vcard_xupdate.erl
index 041b0b64c..f2101df91 100644
--- a/src/mod_vcard_xupdate.erl
+++ b/src/mod_vcard_xupdate.erl
@@ -13,7 +13,7 @@
-export([start/2, stop/1]).
-export([update_presence/3, vcard_set/3, export/1,
- import/1, import/3, mod_opt_type/1]).
+ import/1, import/3, mod_opt_type/1, depends/2]).
-include("ejabberd.hrl").
-include("logger.hrl").
@@ -46,6 +46,9 @@ stop(Host) ->
vcard_set, 100),
ok.
+depends(_Host, _Opts) ->
+ [].
+
%%====================================================================
%% Hooks
%%====================================================================
diff --git a/src/mod_version.erl b/src/mod_version.erl
index 7f7759f2d..8a035763f 100644
--- a/src/mod_version.erl
+++ b/src/mod_version.erl
@@ -32,7 +32,7 @@
-behaviour(gen_mod).
-export([start/2, stop/1, process_local_iq/3,
- mod_opt_type/1]).
+ mod_opt_type/1, depends/2]).
-include("ejabberd.hrl").
-include("logger.hrl").
@@ -93,6 +93,9 @@ get_os() ->
#xmlel{name = <<"os">>, attrs = [],
children = [{xmlcdata, OS}]}.
+depends(_Host, _Opts) ->
+ [].
+
mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1;
mod_opt_type(show_os) ->
fun (B) when is_boolean(B) -> B end;
diff --git a/src/node_flat_sql.erl b/src/node_flat_sql.erl
index 2030b12c7..fa4af4d57 100644
--- a/src/node_flat_sql.erl
+++ b/src/node_flat_sql.erl
@@ -365,21 +365,22 @@ get_entity_subscriptions(Host, Owner) ->
H = encode_host(Host),
SJ = encode_jid(SubKey),
GJ = encode_jid(GenKey),
- GJLike = <<(encode_jid_like(GenKey))/binary, "%">>,
+ GJLike = <<(encode_jid_like(GenKey))/binary, "/%">>,
Query =
case SubKey of
GenKey ->
?SQL("select @(node)s, @(type)s, @(i.nodeid)d,"
" @(jid)s, @(subscriptions)s "
"from pubsub_state i, pubsub_node n "
- "where i.nodeid = n.nodeid and jid like %(GJLike)s"
- " escape '^' and host=%(H)s");
+ "where i.nodeid = n.nodeid and "
+ "(jid=%(GJ)s or jid like %(GJLike)s escape '^')"
+ " and host=%(H)s");
_ ->
?SQL("select @(node)s, @(type)s, @(i.nodeid)d,"
" @(jid)s, @(subscriptions)s "
"from pubsub_state i, pubsub_node n "
- "where i.nodeid = n.nodeid and jid in"
- " (%(SJ)s, %(GJ)s) and host=%(H)s")
+ "where i.nodeid = n.nodeid and"
+ " jid in (%(SJ)s, %(GJ)s) and host=%(H)s")
end,
Reply = case catch ejabberd_sql:sql_query_t(Query) of
{selected, RItems} ->
@@ -415,7 +416,7 @@ get_entity_subscriptions_for_send_last(Host, Owner) ->
H = encode_host(Host),
SJ = encode_jid(SubKey),
GJ = encode_jid(GenKey),
- GJLike = <<(encode_jid_like(GenKey))/binary, "%">>,
+ GJLike = <<(encode_jid_like(GenKey))/binary, "/%">>,
Query =
case SubKey of
GenKey ->
@@ -423,8 +424,9 @@ get_entity_subscriptions_for_send_last(Host, Owner) ->
" @(jid)s, @(subscriptions)s "
"from pubsub_state i, pubsub_node n, pubsub_node_option o "
"where i.nodeid = n.nodeid and n.nodeid = o.nodeid and name='send_last_published_item' "
- "and val='on_sub_and_presence' and jid like %(GJLike)s"
- " escape '^' and host=%(H)s");
+ "and val='on_sub_and_presence' and "
+ "(jid=%(GJ)s or jid like %(GJLike)s escape '^')"
+ " and host=%(H)s");
_ ->
?SQL("select @(node)s, @(type)s, @(i.nodeid)d,"
" @(jid)s, @(subscriptions)s "
@@ -912,11 +914,13 @@ first_in_list(Pred, [H | T]) ->
end.
itemids(Nidx, {_U, _S, _R} = JID) ->
- SJID = <<(ejabberd_sql:escape(encode_jid_like(JID)))/binary, "%">>,
+ SJID = encode_jid(JID),
+ SJIDLike = <<(ejabberd_sql:escape(encode_jid_like(JID)))/binary, "/%">>,
case catch
ejabberd_sql:sql_query_t(
?SQL("select @(itemid)s from pubsub_item where "
- "nodeid=%(Nidx)d and publisher like %(SJID)s escape '^' "
+ "nodeid=%(Nidx)d and (publisher=%(SJID)s"
+ " or publisher like %(SJIDLike)s escape '^') "
"order by modification desc"))
of
{selected, RItems} ->
@@ -1033,13 +1037,6 @@ encode_subscriptions(Subscriptions) ->
%%% record getter/setter
-state_to_raw(Nidx, State) ->
- {JID, _} = State#pubsub_state.stateid,
- J = ejabberd_sql:escape(encode_jid(JID)),
- A = encode_affiliation(State#pubsub_state.affiliation),
- S = encode_subscriptions(State#pubsub_state.subscriptions),
- [<<"'">>, Nidx, <<"', '">>, J, <<"', '">>, A, <<"', '">>, S, <<"'">>].
-
raw_to_item(Nidx, [ItemId, SJID, Creation, Modification, XML]) ->
raw_to_item(Nidx, {ItemId, SJID, Creation, Modification, XML});
raw_to_item(Nidx, {ItemId, SJID, Creation, Modification, XML}) ->
diff --git a/src/node_pep.erl b/src/node_pep.erl
index 504e39fa3..1677ed4bd 100644
--- a/src/node_pep.erl
+++ b/src/node_pep.erl
@@ -45,11 +45,13 @@
get_pending_nodes/2, get_states/1, get_state/2,
set_state/1, get_items/7, get_items/3, get_item/7,
get_item/2, set_item/1, get_item_name/3, node_to_path/1,
- path_to_node/1]).
+ path_to_node/1, depends/3]).
+
+depends(_Host, _ServerHost, _Opts) ->
+ [{mod_caps, hard}].
init(Host, ServerHost, Opts) ->
node_flat:init(Host, ServerHost, Opts),
- complain_if_modcaps_disabled(ServerHost),
ok.
terminate(Host, ServerHost) ->
@@ -245,21 +247,3 @@ node_to_path(Node) ->
path_to_node(Path) ->
node_flat:path_to_node(Path).
-
-%%%
-%%% Internal
-%%%
-
-%% @doc Check mod_caps is enabled, otherwise show warning.
-%% The PEP plugin for mod_pubsub requires mod_caps to be enabled in the host.
-%% Check that the mod_caps module is enabled in that Jabber Host
-%% If not, show a warning message in the ejabberd log file.
-complain_if_modcaps_disabled(ServerHost) ->
- case gen_mod:is_loaded(ServerHost, mod_caps) of
- false ->
- ?WARNING_MSG("The PEP plugin is enabled in mod_pubsub "
- "of host ~p. This plugin requires mod_caps "
- "but it does not seems enabled, please check config.",
- [ServerHost]);
- true -> ok
- end.
diff --git a/src/node_pep_sql.erl b/src/node_pep_sql.erl
index 1df173fd7..ec7795475 100644
--- a/src/node_pep_sql.erl
+++ b/src/node_pep_sql.erl
@@ -45,12 +45,14 @@
get_pending_nodes/2, get_states/1, get_state/2,
set_state/1, get_items/7, get_items/3, get_item/7,
get_item/2, set_item/1, get_item_name/3, node_to_path/1,
- path_to_node/1,
+ path_to_node/1, depends/3,
get_entity_subscriptions_for_send_last/2, get_last_items/3]).
+depends(_Host, _ServerHost, _Opts) ->
+ [{mod_caps, hard}].
+
init(Host, ServerHost, Opts) ->
node_flat_sql:init(Host, ServerHost, Opts),
- complain_if_modcaps_disabled(ServerHost),
ok.
terminate(Host, ServerHost) ->
@@ -237,21 +239,3 @@ node_to_path(Node) ->
path_to_node(Path) ->
node_flat_sql:path_to_node(Path).
-
-%%%
-%%% Internal
-%%%
-
-%% @doc Check mod_caps is enabled, otherwise show warning.
-%% The PEP plugin for mod_pubsub requires mod_caps to be enabled in the host.
-%% Check that the mod_caps module is enabled in that Jabber Host
-%% If not, show a warning message in the ejabberd log file.
-complain_if_modcaps_disabled(ServerHost) ->
- case gen_mod:is_loaded(ServerHost, mod_caps) of
- false ->
- ?WARNING_MSG("The PEP plugin is enabled in mod_pubsub "
- "of host ~p. This plugin requires mod_caps "
- "to be enabled, but it isn't.",
- [ServerHost]);
- true -> ok
- end.
diff --git a/src/nodetree_tree_sql.erl b/src/nodetree_tree_sql.erl
index 1ad4046cd..edfdbc1d5 100644
--- a/src/nodetree_tree_sql.erl
+++ b/src/nodetree_tree_sql.erl
@@ -191,18 +191,25 @@ get_subnodes_tree(Host, Node, _From) ->
get_subnodes_tree(Host, Node).
get_subnodes_tree(Host, Node) ->
- H = node_flat_sql:encode_host(Host),
- N = <<(ejabberd_sql:escape_like_arg_circumflex(Node))/binary, "%">>,
- case catch
- ejabberd_sql:sql_query_t(
- ?SQL("select @(node)s, @(parent)s, @(type)s, @(nodeid)d from "
- "pubsub_node where host=%(H)s"
- " and node like %(N)s escape '^'"))
- of
- {selected, RItems} ->
- [raw_to_node(Host, Item) || Item <- RItems];
- _ ->
- []
+ case get_node(Host, Node) of
+ {error, _} ->
+ [];
+ Rec ->
+ H = node_flat_sql:encode_host(Host),
+ N = <<(ejabberd_sql:escape_like_arg_circumflex(Node))/binary, "/%">>,
+ Sub = case catch
+ ejabberd_sql:sql_query_t(
+ ?SQL("select @(node)s, @(parent)s, @(type)s, @(nodeid)d from "
+ "pubsub_node where host=%(H)s"
+ " and node like %(N)s escape '^'"
+ " and \"type\"='hometree'"))
+ of
+ {selected, RItems} ->
+ [raw_to_node(Host, Item) || Item <- RItems];
+ _ ->
+ []
+ end,
+ [Rec|Sub]
end.
create_node(Host, Node, Type, Owner, Options, Parents) ->
@@ -252,11 +259,12 @@ create_node(Host, Node, Type, Owner, Options, Parents) ->
delete_node(Host, Node) ->
H = node_flat_sql:encode_host(Host),
- N = <<(ejabberd_sql:escape_like_arg_circumflex(Node))/binary, "%">>,
+ N = <<(ejabberd_sql:escape_like_arg_circumflex(Node))/binary, "/%">>,
Removed = get_subnodes_tree(Host, Node),
catch ejabberd_sql:sql_query_t(
?SQL("delete from pubsub_node where host=%(H)s"
- " and node like %(N)s escape '^'")),
+ " and (node=%(Node)s"
+ " or (\"type\"='hometree' and node like %(N)s escape '^'))")),
Removed.
%% helpers
diff --git a/test/acl_test.exs b/test/acl_test.exs
index 551c74ae0..4bd8e6989 100644
--- a/test/acl_test.exs
+++ b/test/acl_test.exs
@@ -26,6 +26,7 @@ defmodule ACLTest do
setup_all do
:ok = :mnesia.start
:ok = :jid.start
+ :stringprep.start
:ok = :ejabberd_config.start(["domain1", "domain2"], [])
:ok = :acl.start
end
diff --git a/test/ejabberd_commands_mock_test.exs b/test/ejabberd_commands_mock_test.exs
index 487cf6a4b..439a3c1d3 100644
--- a/test/ejabberd_commands_mock_test.exs
+++ b/test/ejabberd_commands_mock_test.exs
@@ -18,9 +18,13 @@
#
# ----------------------------------------------------------------------
+## TODO Fix next test error: add admin user ACL
+
defmodule EjabberdCommandsMockTest do
use ExUnit.Case, async: false
+ require EjabberdOauthMock
+
@author "jsautret@process-one.net"
# mocked callback module
@@ -44,8 +48,11 @@ defmodule EjabberdCommandsMockTest do
_ -> :ok
end
:mnesia.start
+ :ok = :jid.start
+ :ok = :ejabberd_config.start(["domain1", "domain2"], [])
+ :ok = :acl.start
EjabberdOauthMock.init
- :ok
+ on_exit fn -> :meck.unload end
end
setup do
@@ -180,7 +187,7 @@ defmodule EjabberdCommandsMockTest do
test "API command with user policy" do
- mock_commands_config
+ mock_commands_config [:user, :admin]
# Register a command test(user, domain) -> {:versionN, user, domain}
# with policy=user and versions 1 & 3
@@ -313,9 +320,8 @@ defmodule EjabberdCommandsMockTest do
end
-
test "API command with admin policy" do
- mock_commands_config
+ mock_commands_config [:admin]
# Register a command test(user, domain) -> {user, domain}
# with policy=admin
@@ -393,13 +399,47 @@ defmodule EjabberdCommandsMockTest do
assert :meck.validate @module
end
+ test "Commands can perform extra check on access" do
+ mock_commands_config [:admin, :open]
+
+ command_name = :test
+ function = :test_command
+ command = ejabberd_commands(name: command_name,
+ args: [{:user, :binary}, {:host, :binary}],
+ access: [:basic_rule_1],
+ module: @module,
+ function: function,
+ policy: :open)
+ :meck.expect(@module, function,
+ fn(user, domain) when is_binary(user) and is_binary(domain) ->
+ {user, domain}
+ end)
+ assert :ok == :ejabberd_commands.register_commands [command]
+
+# :acl.add(:global, :basic_acl_1, {:user, @user, @host})
+# :acl.add_access(:global, :basic_rule_1, [{:allow, [{:acl, :basic_acl_1}]}])
+
+ assert {@user, @domain} ==
+ :ejabberd_commands.execute_command(:undefined,
+ {@user, @domain,
+ @userpass, false},
+ command_name,
+ [@user, @domain])
+ assert {@user, @domain} ==
+ :ejabberd_commands.execute_command(:undefined,
+ {@admin, @domain,
+ @adminpass, false},
+ command_name,
+ [@user, @domain])
+
+ end
##########################################################
# Utils
# Mock a config where only @admin user is allowed to call commands
# as admin
- def mock_commands_config do
+ def mock_commands_config(commands \\ []) do
EjabberdAuthMock.init
EjabberdAuthMock.create_user @user, @domain, @userpass
EjabberdAuthMock.create_user @admin, @domain, @adminpass
@@ -408,10 +448,12 @@ defmodule EjabberdCommandsMockTest do
:meck.expect(:ejabberd_config, :get_option,
fn(:commands_admin_access, _, _) -> :commands_admin_access
(:oauth_access, _, _) -> :all
+ (:commands, _, _) -> [{:add_commands, commands}]
(_, _, default) -> default
end)
:meck.expect(:ejabberd_config, :get_myhosts,
fn() -> [@domain] end)
+
:meck.new :acl
:meck.expect(:acl, :access_matches,
fn(:commands_admin_access, info, _scope) ->
diff --git a/test/ejabberd_commands_test.exs b/test/ejabberd_commands_test.exs
index 31d108214..10b656140 100644
--- a/test/ejabberd_commands_test.exs
+++ b/test/ejabberd_commands_test.exs
@@ -28,7 +28,11 @@ defmodule EjabberdCommandsTest do
setup_all do
:mnesia.start
+ :stringprep.start
+ :ok = :ejabberd_config.start(["localhost"], [])
+
:ejabberd_commands.init
+ :ok
end
test "Check that we can register a command" do
@@ -37,6 +41,14 @@ defmodule EjabberdCommandsTest do
assert Enum.member?(commands, {:test_user, [], "Test user"})
end
+ test "get_exposed_commands/0 returns registered commands" do
+ commands = [open_test_command]
+ :ok = :ejabberd_commands.register_commands(commands)
+ :ok = :ejabberd_commands.expose_commands(commands)
+ exposed_commands = :ejabberd_commands.get_exposed_commands
+ assert Enum.member?(exposed_commands, :test_open)
+ end
+
test "Check that admin commands are rejected with noauth credentials" do
:ok = :ejabberd_commands.register_commands([admin_test_command])
@@ -70,6 +82,16 @@ defmodule EjabberdCommandsTest do
]}}}})
end
+ defp open_test_command do
+ ejabberd_commands(name: :test_open, tags: [:test],
+ desc: "Test open",
+ policy: :open,
+ module: __MODULE__,
+ function: :test_open,
+ args: [],
+ result: {:res, :rescode})
+ end
+
defp admin_test_command do
ejabberd_commands(name: :test_admin, tags: [:roster],
desc: "Test admin",
diff --git a/test/ejabberd_cyrsasl_test.exs b/test/ejabberd_cyrsasl_test.exs
index 0dc64ee44..d9b949294 100644
--- a/test/ejabberd_cyrsasl_test.exs
+++ b/test/ejabberd_cyrsasl_test.exs
@@ -71,8 +71,8 @@ defmodule EjabberdCyrsaslTest do
response = "username=\"#{user}\",realm=\"#{domain}\",nonce=\"#{nonce}\",cnonce=\"#{cnonce}\"," <>
"nc=\"#{nc}\",qop=auth,digest-uri=\"#{digest_uri}\",response=\"#{response_hash}\"," <>
"charset=utf-8,algorithm=md5-sess"
- assert {:continue, calc_str, state3} = :cyrsasl.server_step(state1, response)
- assert {:ok, list} = :cyrsasl.server_step(state3, "")
+ assert {:continue, _calc_str, state3} = :cyrsasl.server_step(state1, response)
+ assert {:ok, _list} = :cyrsasl.server_step(state3, "")
end
defp calc_digest_sha(user, domain, pass, nc, nonce, cnonce) do
@@ -94,7 +94,7 @@ defmodule EjabberdCyrsaslTest do
defp setup_anonymous_mocks() do
:meck.unload
mock(:ejabberd_auth_anonymous, :is_sasl_anonymous_enabled,
- fn (host) ->
+ fn (_host) ->
true
end)
mock(:ejabberd_auth, :is_user_exists,
@@ -119,7 +119,7 @@ defmodule EjabberdCyrsaslTest do
end
end
- defp check_password(user, authzid, pass) do
+ defp check_password(_user, authzid, pass) do
case get_password(authzid) do
{^pass, mod} ->
{true, mod}
@@ -128,7 +128,7 @@ defmodule EjabberdCyrsaslTest do
end
end
- defp check_password_digest(user, authzid, pass, digest, digest_gen) do
+ defp check_password_digest(_user, authzid, _pass, digest, digest_gen) do
case get_password(authzid) do
{spass, mod} ->
v = digest_gen.(spass)
diff --git a/test/ejabberd_oauth_mock.exs b/test/ejabberd_oauth_mock.exs
index 81cfdc038..e6a34f65e 100644
--- a/test/ejabberd_oauth_mock.exs
+++ b/test/ejabberd_oauth_mock.exs
@@ -40,7 +40,7 @@ defmodule EjabberdOauthMock do
{:user, user, domain}},
{"scope", [to_string command]},
{"expiry_time", expire}],
- :undefined)
+ [])
token
end
diff --git a/test/mod_admin_extra_test.exs b/test/mod_admin_extra_test.exs
index 761b07b7c..03422264f 100644
--- a/test/mod_admin_extra_test.exs
+++ b/test/mod_admin_extra_test.exs
@@ -22,6 +22,9 @@ defmodule EjabberdModAdminExtraTest do
use ExUnit.Case, async: false
require EjabberdAuthMock
+ require EjabberdSmMock
+ require ModLastMock
+ require ModRosterMock
@author "jsautret@process-one.net"
diff --git a/test/mod_http_api_mock_test.exs b/test/mod_http_api_mock_test.exs
index 47b1fe94a..9cba35365 100644
--- a/test/mod_http_api_mock_test.exs
+++ b/test/mod_http_api_mock_test.exs
@@ -58,6 +58,7 @@ defmodule ModHttpApiMockTest do
setup do
:meck.unload
:meck.new :ejabberd_commands
+ :meck.new(:acl, [:passthrough]) # Need to fake acl to allow oauth
EjabberdAuthMock.init
:ok
end
@@ -70,9 +71,9 @@ defmodule ModHttpApiMockTest do
fn (@acommand, {@user, @domain, @userpass, false}, @version) ->
{[], {:res, :rescode}}
end)
- :meck.expect(:ejabberd_commands, :get_command_policy,
- fn (@acommand) -> {:ok, :user} end)
- :meck.expect(:ejabberd_commands, :get_commands,
+ :meck.expect(:ejabberd_commands, :get_command_policy_and_scope,
+ fn (@acommand) -> {:ok, :user, [:erlang.atom_to_binary(@acommand,:utf8)]} end)
+ :meck.expect(:ejabberd_commands, :get_exposed_commands,
fn () -> [@acommand] end)
:meck.expect(:ejabberd_commands, :execute_command,
fn (:undefined, {@user, @domain, @userpass, false}, @acommand, [], @version, _) ->
@@ -123,9 +124,9 @@ defmodule ModHttpApiMockTest do
fn (@acommand, {@user, @domain, {:oauth, _token}, false}, @version) ->
{[], {:res, :rescode}}
end)
- :meck.expect(:ejabberd_commands, :get_command_policy,
- fn (@acommand) -> {:ok, :user} end)
- :meck.expect(:ejabberd_commands, :get_commands,
+ :meck.expect(:ejabberd_commands, :get_command_policy_and_scope,
+ fn (@acommand) -> {:ok, :user, [:erlang.atom_to_binary(@acommand,:utf8), "ejabberd:user"]} end)
+ :meck.expect(:ejabberd_commands, :get_exposed_commands,
fn () -> [@acommand] end)
:meck.expect(:ejabberd_commands, :execute_command,
fn (:undefined, {@user, @domain, {:oauth, _token}, false},
@@ -134,7 +135,7 @@ defmodule ModHttpApiMockTest do
end)
- # Correct OAuth call
+ # Correct OAuth call using specific scope
token = EjabberdOauthMock.get_token @user, @domain, @command
req = request(method: :GET,
path: ["api", @command],
@@ -147,6 +148,19 @@ defmodule ModHttpApiMockTest do
assert 200 == elem(result, 0) # HTTP code
assert "0" == elem(result, 2) # command result
+ # Correct OAuth call using specific ejabberd:user scope
+ token = EjabberdOauthMock.get_token @user, @domain, "ejabberd:user"
+ req = request(method: :GET,
+ path: ["api", @command],
+ q: [nokey: ""],
+ # OAuth
+ auth: {:oauth, token, []},
+ ip: {{127,0,0,1},60000},
+ host: @domain)
+ result = :mod_http_api.process([@command], req)
+ assert 200 == elem(result, 0) # HTTP code
+ assert "0" == elem(result, 2) # command result
+
# Wrong OAuth token
req = request(method: :GET,
path: ["api", @command],
@@ -184,8 +198,8 @@ defmodule ModHttpApiMockTest do
result = :mod_http_api.process([@command], req)
assert 401 == elem(result, 0) # HTTP code
- # Check that the command was executed only once
- assert 1 ==
+ # Check that the command was executed twice
+ assert 2 ==
:meck.num_calls(:ejabberd_commands, :execute_command, :_)
assert :meck.validate :ejabberd_auth
@@ -193,5 +207,69 @@ defmodule ModHttpApiMockTest do
#assert :ok = :meck.history(:ejabberd_commands)
end
+ test "Request oauth token, resource owner password credentials" do
+ EjabberdAuthMock.create_user @user, @domain, @userpass
+ :application.set_env(:oauth2, :backend, :ejabberd_oauth)
+ :application.start(:oauth2)
+
+ # Mock a simple command() -> :ok
+ :meck.expect(:ejabberd_commands, :get_command_format,
+ fn (@acommand, {@user, @domain, {:oauth, _token}, false}, @version) ->
+ {[], {:res, :rescode}}
+ end)
+ :meck.expect(:ejabberd_commands, :get_command_policy_and_scope,
+ fn (@acommand) -> {:ok, :user, [:erlang.atom_to_binary(@acommand,:utf8), "ejabberd:user"]} end)
+ :meck.expect(:ejabberd_commands, :get_exposed_commands,
+ fn () -> [@acommand] end)
+ :meck.expect(:ejabberd_commands, :execute_command,
+ fn (:undefined, {@user, @domain, {:oauth, _token}, false},
+ @acommand, [], @version, _) ->
+ :ok
+ end)
+
+ #Mock acl to allow oauth authorizations
+ :meck.expect(:acl, :match_rule, fn(_Server, _Access, _Jid) -> :allow end)
+
+
+ # Correct password
+ req = request(method: :POST,
+ path: ["oauth", "token"],
+ q: [{"grant_type", "password"}, {"scope", @command}, {"username", @user<>"@"<>@domain}, {"ttl", "4000"}, {"password", @userpass}],
+ ip: {{127,0,0,1},60000},
+ host: @domain)
+ result = :ejabberd_oauth.process([], req)
+ assert 200 = elem(result, 0) #http code
+ {kv} = :jiffy.decode(elem(result,2))
+ assert {_, "bearer"} = List.keyfind(kv, "token_type", 0)
+ assert {_, @command} = List.keyfind(kv, "scope", 0)
+ assert {_, 4000} = List.keyfind(kv, "expires_in", 0)
+ {"access_token", _token} = List.keyfind(kv, "access_token", 0)
+
+ #missing grant_type
+ req = request(method: :POST,
+ path: ["oauth", "token"],
+ q: [{"scope", @command}, {"username", @user<>"@"<>@domain}, {"password", @userpass}],
+ ip: {{127,0,0,1},60000},
+ host: @domain)
+ result = :ejabberd_oauth.process([], req)
+ assert 400 = elem(result, 0) #http code
+ {kv} = :jiffy.decode(elem(result,2))
+ assert {_, "unsupported_grant_type"} = List.keyfind(kv, "error", 0)
+
+
+ # incorrect user/pass
+ req = request(method: :POST,
+ path: ["oauth", "token"],
+ q: [{"grant_type", "password"}, {"scope", @command}, {"username", @user<>"@"<>@domain}, {"password", @userpass<>"aa"}],
+ ip: {{127,0,0,1},60000},
+ host: @domain)
+ result = :ejabberd_oauth.process([], req)
+ assert 400 = elem(result, 0) #http code
+ {kv} = :jiffy.decode(elem(result,2))
+ assert {_, "invalid_grant"} = List.keyfind(kv, "error", 0)
+
+ assert :meck.validate :ejabberd_auth
+ assert :meck.validate :ejabberd_commands
+ end
end
diff --git a/test/mod_http_api_test.exs b/test/mod_http_api_test.exs
index 99b8d9b28..e2ae3d784 100644
--- a/test/mod_http_api_test.exs
+++ b/test/mod_http_api_test.exs
@@ -31,43 +31,43 @@ defmodule ModHttpApiTest do
:ok = :mnesia.start
:stringprep.start
:ok = :ejabberd_config.start(["localhost"], [])
-
:ok = :ejabberd_commands.init
-
:ok = :ejabberd_commands.register_commands(cmds)
- on_exit fn -> unregister_commands(cmds) end
+ on_exit fn ->
+ :meck.unload
+ unregister_commands(cmds) end
end
test "We can expose several commands to API at a time" do
setup_mocks()
- :ejabberd_config.add_local_option(:commands, [[{:add_commands, [:open_cmd, :user_cmd]}]])
- commands = :ejabberd_commands.get_commands()
+ :ejabberd_commands.expose_commands([:open_cmd, :user_cmd])
+ commands = :ejabberd_commands.get_exposed_commands()
assert Enum.member?(commands, :open_cmd)
assert Enum.member?(commands, :user_cmd)
end
test "We can call open commands without authentication" do
setup_mocks()
- :ejabberd_config.add_local_option(:commands, [[{:add_commands, [:open_cmd]}]])
+ :ejabberd_commands.expose_commands([:open_cmd])
request = request(method: :POST, ip: {{127,0,0,1},50000}, data: "[]")
{200, _, _} = :mod_http_api.process(["open_cmd"], request)
end
# This related to the commands config file option
- test "Attempting to access a command that is not exposed as HTTP API returns 401" do
+ test "Attempting to access a command that is not exposed as HTTP API returns 403" do
setup_mocks()
- :ejabberd_config.add_local_option(:commands, [])
+ :ejabberd_commands.expose_commands([])
request = request(method: :POST, ip: {{127,0,0,1},50000}, data: "[]")
- {401, _, _} = :mod_http_api.process(["open_cmd"], request)
+ {403, _, _} = :mod_http_api.process(["open_cmd"], request)
end
test "Call to user, admin or restricted commands without authentication are rejected" do
setup_mocks()
- :ejabberd_config.add_local_option(:commands, [[{:add_commands, [:user_cmd, :admin_cmd, :restricted]}]])
+ :ejabberd_commands.expose_commands([:user_cmd, :admin_cmd, :restricted])
request = request(method: :POST, ip: {{127,0,0,1},50000}, data: "[]")
- {401, _, _} = :mod_http_api.process(["user_cmd"], request)
- {401, _, _} = :mod_http_api.process(["admin_cmd"], request)
- {401, _, _} = :mod_http_api.process(["restricted_cmd"], request)
+ {403, _, _} = :mod_http_api.process(["user_cmd"], request)
+ {403, _, _} = :mod_http_api.process(["admin_cmd"], request)
+ {403, _, _} = :mod_http_api.process(["restricted_cmd"], request)
end
@tag pending: true
@@ -98,7 +98,7 @@ defmodule ModHttpApiTest do
defp setup_mocks() do
:meck.unload
mock(:gen_mod, :get_module_opt,
- fn (_server, :mod_http_api, admin_ip_access, _, _) ->
+ fn (_server, :mod_http_api, _admin_ip_access, _, _) ->
[{:allow, [{:ip, {{127,0,0,2}, 32}}]}]
end)
end
diff --git a/test/test_helper.exs b/test/test_helper.exs
new file mode 100644
index 000000000..454f2338a
--- /dev/null
+++ b/test/test_helper.exs
@@ -0,0 +1,7 @@
+Code.require_file "ejabberd_auth_mock.exs", __DIR__
+Code.require_file "ejabberd_oauth_mock.exs", __DIR__
+Code.require_file "ejabberd_sm_mock.exs", __DIR__
+Code.require_file "mod_last_mock.exs", __DIR__
+Code.require_file "mod_roster_mock.exs", __DIR__
+
+ExUnit.start