aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/ci.yml55
-rw-r--r--CHANGELOG.md1
-rw-r--r--Makefile.in27
-rw-r--r--ejabberd.service.template4
-rw-r--r--include/ejabberd_commands.hrl1
-rw-r--r--include/mod_muc_room.hrl17
-rw-r--r--mix.exs6
-rw-r--r--mix.lock22
-rw-r--r--rebar.config14
-rw-r--r--sql/mysql.new.sql2
-rw-r--r--sql/mysql.old-to-new.sql2
-rw-r--r--sql/pg.new.sql8
-rw-r--r--src/ejabberd_acme.erl3
-rw-r--r--src/ejabberd_admin.erl54
-rw-r--r--src/ejabberd_auth_sql.erl14
-rw-r--r--src/ejabberd_commands.erl16
-rw-r--r--src/ejabberd_commands_doc.erl45
-rw-r--r--src/ejabberd_ctl.erl13
-rw-r--r--src/ejabberd_logger.erl12
-rw-r--r--src/ejabberd_options_doc.erl187
-rw-r--r--src/ejabberd_piefxis.erl126
-rw-r--r--src/ejabberd_s2s.erl79
-rw-r--r--src/ejabberd_s2s_out.erl22
-rw-r--r--src/ejabberd_sql_pt.erl77
-rw-r--r--src/ejabberd_web_admin.erl10
-rw-r--r--src/ejd2sql.erl15
-rw-r--r--src/ext_mod.erl14
-rw-r--r--src/gen_pubsub_node.erl9
-rw-r--r--src/gen_pubsub_nodetree.erl3
-rw-r--r--src/mod_admin_extra.erl13
-rw-r--r--src/mod_admin_update_sql.erl8
-rw-r--r--src/mod_announce.erl14
-rw-r--r--src/mod_avatar.erl4
-rw-r--r--src/mod_bosh.erl12
-rw-r--r--src/mod_caps.erl26
-rw-r--r--src/mod_configure.erl2
-rw-r--r--src/mod_conversejs.erl156
-rw-r--r--src/mod_conversejs_opt.erl41
-rw-r--r--src/mod_delegation.erl2
-rw-r--r--src/mod_disco.erl4
-rw-r--r--src/mod_fail2ban.erl2
-rw-r--r--src/mod_http_api.erl16
-rw-r--r--src/mod_http_upload.erl12
-rw-r--r--src/mod_http_upload_quota.erl23
-rw-r--r--src/mod_last.erl10
-rw-r--r--src/mod_mam.erl17
-rw-r--r--src/mod_mam_sql.erl20
-rw-r--r--src/mod_mix.erl6
-rw-r--r--src/mod_mix_pam.erl10
-rw-r--r--src/mod_mqtt.erl19
-rw-r--r--src/mod_muc.erl25
-rw-r--r--src/mod_muc_admin.erl6
-rw-r--r--src/mod_muc_log.erl2
-rw-r--r--src/mod_muc_room.erl762
-rw-r--r--src/mod_muc_sql.erl24
-rw-r--r--src/mod_multicast.erl426
-rw-r--r--src/mod_offline.erl18
-rw-r--r--src/mod_ping.erl8
-rw-r--r--src/mod_privacy.erl12
-rw-r--r--src/mod_private.erl12
-rw-r--r--src/mod_privilege.erl2
-rw-r--r--src/mod_pubsub.erl148
-rw-r--r--src/mod_pubsub_opt.erl9
-rw-r--r--src/mod_push.erl12
-rw-r--r--src/mod_push_keepalive.erl8
-rw-r--r--src/mod_push_sql.erl2
-rw-r--r--src/mod_register.erl39
-rw-r--r--src/mod_register_opt.erl7
-rw-r--r--src/mod_register_web.erl110
-rw-r--r--src/mod_roster.erl14
-rw-r--r--src/mod_roster_sql.erl10
-rw-r--r--src/mod_shared_roster.erl14
-rw-r--r--src/mod_shared_roster_ldap.erl2
-rw-r--r--src/mod_stream_mgmt.erl8
-rw-r--r--src/mod_stun_disco.erl2
-rw-r--r--src/mod_vcard.erl10
-rw-r--r--src/mod_vcard_ldap.erl2
-rw-r--r--src/mod_vcard_xupdate.erl12
-rw-r--r--src/node_flat.erl43
-rw-r--r--src/node_flat_sql.erl71
-rw-r--r--src/node_pep.erl9
-rw-r--r--src/node_pep_sql.erl9
-rw-r--r--src/nodetree_tree.erl11
-rw-r--r--src/nodetree_tree_sql.erl31
-rw-r--r--src/nodetree_virtual.erl6
-rw-r--r--test/offline_tests.erl16
-rw-r--r--test/roster_tests.erl10
-rwxr-xr-xtools/captcha-ng.sh3
88 files changed, 2187 insertions, 1003 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d498760be..ac384e375 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -52,6 +52,8 @@ jobs:
- name: Prepare databases
run: |
+ sudo sed -i 's|#password_encryption.*|password_encryption = md5|g' /etc/postgresql/14/main/postgresql.conf
+ sudo sed -i 's|scram-sha-256|md5|g' /etc/postgresql/14/main/pg_hba.conf
sudo systemctl start mysql.service
sudo systemctl start postgresql.service
mysql -u root -proot -e "CREATE USER 'ejabberd_test'@'localhost'
@@ -158,6 +160,10 @@ jobs:
if: failure()
run: find logs/ -name ejabberd.log -exec cat '{}' ';'
+ - name: View error.log
+ if: failure()
+ run: find logs/ -name error.log -exec cat '{}' ';'
+
- name: View exunit.log
if: failure()
run: find logs/ -name exunit.log -exec cat '{}' ';'
@@ -167,7 +173,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
- rebar3 as test coveralls send
+ DIAGNOSTIC=1 rebar3 as test coveralls send
curl -v -k https://coveralls.io/webhook \
--header "Content-Type: application/json" \
--data '{"repo_name":"$GITHUB_REPOSITORY",
@@ -175,6 +181,53 @@ jobs:
"payload":{"build_num":$GITHUB_RUN_ID,
"status":"done"}}'
+ - name: Prepare new schema
+ run: |
+ [[ -d logs ]] && rm -rf logs
+ [[ -d _build/test/logs ]] && rm -rf _build/test/logs
+ mysql -u root -proot -e "DROP DATABASE ejabberd_test;"
+ sudo -u postgres psql -c "DROP DATABASE ejabberd_test;"
+ mysql -u root -proot -e "CREATE DATABASE ejabberd_test;"
+ mysql -u root -proot -e "GRANT ALL ON ejabberd_test.*
+ TO 'ejabberd_test'@'localhost';"
+ mysql -u root -proot ejabberd_test < sql/mysql.new.sql
+ sudo -u postgres psql -c "CREATE DATABASE ejabberd_test;"
+ sudo -u postgres psql ejabberd_test -f sql/pg.new.sql
+ sudo -u postgres psql -c "GRANT ALL PRIVILEGES
+ ON DATABASE ejabberd_test TO ejabberd_test;"
+ sudo -u postgres psql ejabberd_test -c "GRANT ALL PRIVILEGES ON ALL
+ TABLES IN SCHEMA public
+ TO ejabberd_test;"
+ sudo -u postgres psql ejabberd_test -c "GRANT ALL PRIVILEGES ON ALL
+ SEQUENCES IN SCHEMA public
+ TO ejabberd_test;"
+ sudo sed -i 's|new_schema, false|new_schema, true|g' test/suite.erl
+ - run: CT_BACKENDS=mysql,pgsql make test
+ - name: Check results
+ if: always()
+ run: |
+ [[ -d _build ]] && ln -s _build/test/logs/ logs \
+ || ln dialyzer/error.log logs/dialyzer.log
+ ln `find logs/ -name suite.log` logs/suite.log
+ grep 'TEST COMPLETE' logs/suite.log
+ grep -q 'TEST COMPLETE,.* 0 failed' logs/suite.log
+ test $(find logs/ -empty -name error.log)
+ - name: View full suite.log
+ run: cat logs/suite.log
+ - name: View suite.log failures
+ if: failure()
+ run: cat logs/suite.log | awk
+ 'BEGIN{RS="\n=case";FS="\n"} /=result\s*failed/ {print "=case" $0}'
+ - name: View full ejabberd.log
+ if: failure()
+ run: find logs/ -name ejabberd.log -exec cat '{}' ';'
+ - name: View error.log
+ if: failure()
+ run: find logs/ -name error.log -exec cat '{}' ';'
+ - name: View exunit.log
+ if: failure()
+ run: find logs/ -name exunit.log -exec cat '{}' ';'
+
binaries:
name: Binaries
needs: [tests]
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f55ce7127..13385b864 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,7 +3,6 @@
Compilation
- Add rebar3 3.15.2 binary
- Add support for mix to: `./configure --enable-rebar=mix`
-- Add workaround so rebar2 can use Elixir 1.12.0
- Improved `make rel` to work with rebar3 and mix
- Add `make dev` to build a development release with rebar3 or mix
- Hex: Add `sql/` and `vars.config` to Hex package files
diff --git a/Makefile.in b/Makefile.in
index c39195ac7..fc9718006 100644
--- a/Makefile.in
+++ b/Makefile.in
@@ -71,9 +71,6 @@ SPOOLDIR = $(DESTDIR)@localstatedir@/lib/ejabberd
# /var/lock/ejabberdctl
CTLLOCKDIR = $(DESTDIR)@localstatedir@/lock/ejabberdctl
-# /var/lib/ejabberd/.erlang.cookie
-COOKIEFILE = $(SPOOLDIR)/.erlang.cookie
-
# /var/log/ejabberd/
LOGDIR = $(DESTDIR)@localstatedir@/log/ejabberd
@@ -99,12 +96,12 @@ ifneq ($(INSTALLGROUP),)
endif
ifeq "$(MIX)" "mix"
-IS_REBAR:=6
+REBAR_VER:=6
else
-IS_REBAR:=$(shell expr `$(REBAR) --version | awk -F '[ .]' '/rebar / {print $$2}'`)
+REBAR_VER:=$(shell $(REBAR) --version | awk -F '[ .]' '/rebar / {print $$2}')
endif
-ifeq "$(IS_REBAR)" "6"
+ifeq "$(REBAR_VER)" "6"
REBAR=$(MIX)
SKIPDEPS=
LISTDEPS=deps.tree
@@ -118,7 +115,7 @@ ifeq "$(IS_REBAR)" "6"
REBARREL=MIX_ENV=prod $(REBAR) release --overwrite
REBARDEV=MIX_ENV=dev $(REBAR) release --overwrite
else
-ifeq "$(IS_REBAR)" "3"
+ifeq "$(REBAR_VER)" "3"
SKIPDEPS=
LISTDEPS=tree
UPDATEDEPS=upgrade
@@ -249,7 +246,15 @@ $(call TO_DEST,priv/bin/captcha.sh): tools/captcha.sh $(call TO_DEST,priv/bin)
$(call TO_DEST,priv/lua/redis_sm.lua): priv/lua/redis_sm.lua $(call TO_DEST,priv/lua)
$(INSTALL) -m 644 $< $@
-copy-files-sub2: $(call TO_DEST,$(DEPS_FILES) $(MAIN_FILES) priv/bin/captcha.sh priv/sql/lite.sql priv/sql/lite.new.sql priv/lua/redis_sm.lua)
+ifeq (@sqlite@,true)
+SQLITE_FILES = priv/sql/lite.sql priv/sql/lite.new.sql
+endif
+
+ifeq (@redis@,true)
+REDIS_FILES = priv/lua/redis_sm.lua
+endif
+
+copy-files-sub2: $(call TO_DEST,$(DEPS_FILES) $(MAIN_FILES) priv/bin/captcha.sh $(SQLITE_FILES) $(REDIS_FILES))
.PHONY: $(call TO_DEST,$(DEPS_FILES) $(MAIN_DIRS) $(DEPS_DIRS))
@@ -298,7 +303,8 @@ install: copy-files
chmod 755 ejabberd.init
#
# Service script
- $(SED) -e "s*@ctlscriptpath@*$(SBINDIR)*g" ejabberd.service.template \
+ $(SED) -e "s*@ctlscriptpath@*$(SBINDIR)*g" \
+ -e "s*@installuser@*$(INIT_USER)*g" ejabberd.service.template \
> ejabberd.service
chmod 644 ejabberd.service
#
@@ -306,7 +312,6 @@ install: copy-files
$(INSTALL) -d -m 750 $(O_USER) $(SPOOLDIR)
$(CHOWN_COMMAND) -R @INSTALLUSER@ $(SPOOLDIR) >$(CHOWN_OUTPUT)
chmod -R 750 $(SPOOLDIR)
- [ ! -f $(COOKIEFILE) ] || { $(CHOWN_COMMAND) @INSTALLUSER@ $(COOKIEFILE) >$(CHOWN_OUTPUT) ; chmod 400 $(COOKIEFILE) ; }
#
# ejabberdctl lock directory
$(INSTALL) -d -m 750 $(O_USER) $(CTLLOCKDIR)
@@ -398,7 +403,7 @@ TAGS:
Makefile: Makefile.in
-ifeq "$(IS_REBAR)" "3"
+ifeq "$(REBAR_VER)" "3"
dialyzer:
$(REBAR) dialyzer
else
diff --git a/ejabberd.service.template b/ejabberd.service.template
index c779ea031..685a104d0 100644
--- a/ejabberd.service.template
+++ b/ejabberd.service.template
@@ -4,8 +4,8 @@ After=network.target
[Service]
Type=notify
-User=ejabberd
-Group=ejabberd
+User=@installuser@
+Group=@installuser@
LimitNOFILE=65536
Restart=on-failure
RestartSec=5
diff --git a/include/ejabberd_commands.hrl b/include/ejabberd_commands.hrl
index bfef47080..d0f5ba70a 100644
--- a/include/ejabberd_commands.hrl
+++ b/include/ejabberd_commands.hrl
@@ -59,6 +59,7 @@
policy = restricted :: open | restricted | admin | user,
%% access is: [accessRuleName] or [{Module, AccessOption, DefaultAccessRuleName}]
access = [] :: [{atom(),atom(),atom()}|atom()],
+ definer = unknown :: atom(),
result = {res, rescode} :: rterm() | '_' | '$2',
args_rename = [] :: [{atom(),atom()}],
args_desc = none :: none | [string()] | '_',
diff --git a/include/mod_muc_room.hrl b/include/mod_muc_room.hrl
index f981f9e7c..cc00f73c8 100644
--- a/include/mod_muc_room.hrl
+++ b/include/mod_muc_room.hrl
@@ -65,6 +65,7 @@
captcha_whitelist = (?SETS):empty() :: gb_sets:set(),
mam = false :: boolean(),
pubsub = <<"">> :: binary(),
+ enable_hats = false :: boolean(),
lang = ejabberd_option:language() :: binary()
}).
@@ -87,6 +88,16 @@
nick = <<>> :: binary(),
nodes = [] :: [binary()]}).
+-record(muc_subscribers,
+ {subscribers = #{} :: subscribers(),
+ subscriber_nicks = #{} :: subscriber_nicks(),
+ subscriber_nodes = #{} :: subscriber_nodes()
+ }).
+
+-type subscribers() :: #{ljid() => #subscriber{}}.
+-type subscriber_nicks() :: #{binary() => [ljid()]}.
+-type subscriber_nodes() :: #{binary() => subscribers()}.
+
-record(activity,
{
message_time = 0 :: integer(),
@@ -106,8 +117,7 @@
jid = #jid{} :: jid(),
config = #config{} :: config(),
users = #{} :: users(),
- subscribers = #{} :: subscribers(),
- subscriber_nicks = #{} :: subscriber_nicks(),
+ muc_subscribers = #muc_subscribers{} :: #muc_subscribers{},
last_voice_request_time = treap:empty() :: treap:treap(),
robots = #{} :: robots(),
nicks = #{} :: nicks(),
@@ -115,6 +125,7 @@
history = #lqueue{} :: lqueue(),
subject = [] :: [text()],
subject_author = <<"">> :: binary(),
+ hats_users = #{} :: map(), % FIXME on OTP 21+: #{ljid() => #{binary() => binary()}},
just_created = erlang:system_time(microsecond) :: true | integer(),
activity = treap:empty() :: treap:treap(),
room_shaper = none :: ejabberd_shaper:shaper(),
@@ -126,5 +137,3 @@
-type robots() :: #{jid() => {binary(), stanza()}}.
-type nicks() :: #{binary() => [ljid()]}.
-type affiliations() :: #{ljid() => affiliation() | {affiliation(), binary()}}.
--type subscribers() :: #{ljid() => #subscriber{}}.
--type subscriber_nicks() :: #{binary() => [ljid()]}.
diff --git a/mix.exs b/mix.exs
index c33710b45..fbed459a4 100644
--- a/mix.exs
+++ b/mix.exs
@@ -102,7 +102,7 @@ defmodule Ejabberd.MixProject do
end
defp deps do
- [{:base64url, "~> 0.0.1"},
+ [{:base64url, "~> 1.0"},
{:cache_tab, "~> 1.0"},
{:distillery, "~> 2.0"},
{:eimp, "~> 1.0"},
@@ -113,7 +113,7 @@ defmodule Ejabberd.MixProject do
{:fast_yaml, "~> 1.0"},
{:idna, "~> 6.0"},
{:jiffy, "~> 1.0.5"},
- {:jose, "~> 1.8"},
+ {:jose, "~> 1.11.1"},
{:lager, "~> 3.9.1"},
{:mqtree, "~> 1.0"},
{:p1_acme, "~> 1.0"},
@@ -124,7 +124,7 @@ defmodule Ejabberd.MixProject do
{:pkix, "~> 1.0"},
{:stringprep, ">= 1.0.26"},
{:stun, "~> 1.0"},
- {:xmpp, "~> 1.5"},
+ {:xmpp, ">= 1.5.5"},
{:yconf, "~> 1.0"}]
++ cond_deps()
end
diff --git a/mix.lock b/mix.lock
index a5b158e0f..773cc2bbf 100644
--- a/mix.lock
+++ b/mix.lock
@@ -1,36 +1,36 @@
%{
"artificery": {:hex, :artificery, "0.4.3", "0bc4260f988dcb9dda4b23f9fc3c6c8b99a6220a331534fdf5bf2fd0d4333b02", [:mix], [], "hexpm", "12e95333a30e20884e937abdbefa3e7f5e05609c2ba8cf37b33f000b9ffc0504"},
- "base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm", "fab09b20e3f5db886725544cbcf875b8e73ec93363954eb8a1a9ed834aa8c1f9"},
+ "base64url": {:hex, :base64url, "1.0.1", "f8c7f2da04ca9a5d0f5f50258f055e1d699f0e8bf4cfdb30b750865368403cf6", [:rebar3], [], "hexpm", "f9b3add4731a02a9b0410398b475b33e7566a695365237a6bdee1bb447719f5c"},
"cache_tab": {:hex, :cache_tab, "1.0.29", "6c161988620b788d8df28c8f6af557571609c8e4b671dbadab295a4722cd501b", [:rebar3], [{:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "a02a638021cce91ed1a8628dcbb4795bf5c01c9d11db8c613065923142824ce9"},
"distillery": {:hex, :distillery, "2.1.1", "f9332afc2eec8a1a2b86f22429e068ef35f84a93ea1718265e740d90dd367814", [:mix], [{:artificery, "~> 0.2", [hex: :artificery, repo: "hexpm", optional: false]}], "hexpm", "bbc7008b0161a6f130d8d903b5b3232351fccc9c31a991f8fcbf2a12ace22995"},
- "earmark_parser": {:hex, :earmark_parser, "1.4.13", "0c98163e7d04a15feb62000e1a891489feb29f3d10cb57d4f845c405852bbef8", [:mix], [], "hexpm", "d602c26af3a0af43d2f2645613f65841657ad6efc9f0e361c3b6c06b578214ba"},
+ "earmark_parser": {:hex, :earmark_parser, "1.4.17", "6f3c7e94170377ba45241d394389e800fb15adc5de51d0a3cd52ae766aafd63f", [:mix], [], "hexpm", "f93ac89c9feca61c165b264b5837bf82344d13bebc634cd575cb711e2e342023"},
"eimp": {:hex, :eimp, "1.0.21", "2e918a5dc9a1959ef8713a2360499e3baeee64cfd7881bd9d1f361ca9ddf07e8", [:rebar3], [{:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "998f58538f58aa0cff103414994d7ce56dc253e6576cd6fb40c1ead64aa73a28"},
"epam": {:hex, :epam, "1.0.12", "2a5625d4133bca4b3943791a3f723ba764455a461ae9b6ba5debb262efcf4b40", [:rebar3], [], "hexpm", "54c166c4459cef72f2990a3d89a8f0be27180fe0ab0f24b28ddcc3b815f49f7f"},
- "esip": {:hex, :esip, "1.0.43", "1cbdc073073f80b9b50e2759f66ca13a353eb4f874bcf92501bd4cd767e34d46", [:rebar3], [{:fast_tls, "1.1.13", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}, {:stun, "1.0.44", [hex: :stun, repo: "hexpm", optional: false]}], "hexpm", "b2c758ae52c4588e0399c0b4ce550bfa56551a5a2f828a28389f2614797e4f4b"},
- "ex_doc": {:hex, :ex_doc, "0.25.0", "4070a254664ee5495c2f7cce87c2f43064a8752f7976f2de4937b65871b05223", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2d90883bd4f3d826af0bde7fea733a4c20adba1c79158e2330f7465821c8949b"},
+ "esip": {:hex, :esip, "1.0.44", "dcf3a92e581210ab161d8a8201245df5a56058460b08d89cf3193d944adff71e", [:rebar3], [{:fast_tls, "1.1.13", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}, {:stun, "1.0.45", [hex: :stun, repo: "hexpm", optional: false]}], "hexpm", "ea2086a1de95fe48f40b78251f74a6828799e318c55da346934e7d4fea675f04"},
+ "ex_doc": {:hex, :ex_doc, "0.26.0", "1922164bac0b18b02f84d6f69cab1b93bc3e870e2ad18d5dacb50a9e06b542a3", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2775d66e494a9a48355db7867478ffd997864c61c65a47d31c4949459281c78d"},
"ezlib": {:hex, :ezlib, "1.0.10", "c1c24eb18944cfde55f0574e9922d5b0392fa864282f769f82b2ea15e54f6003", [:rebar3], [], "hexpm", "1d317f1d85373686199eb3b4164d3477e95033ac68e45a95ba18e7b7a8c23241"},
"fast_tls": {:hex, :fast_tls, "1.1.13", "828cdc75e1e8fce8158846d2b971d8b4fe2b2ddcc75b759e88d751079bf78afd", [:rebar3], [{:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "d1f422af40c7777fe534496f508ee86515cb929ad10f7d1d56aa94ce899b44a0"},
- "fast_xml": {:hex, :fast_xml, "1.1.47", "bd1d6c081b69c7bce0d2f22b013c1b864ed2588d48f34e2156d9428f8f772c66", [:rebar3], [{:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "dd014c45247498effb9a28cf98cb716db79be635ad1e98c951240763119f24c7"},
+ "fast_xml": {:hex, :fast_xml, "1.1.48", "d41d14015227999a2367264cc97ac1e6770285aab1dc69545ac4f822be01a2d2", [:rebar3], [{:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "afcf9b808c77599395d4bd22ed4560b3d82aa1a24ff5b65f3930fe72a423b3cf"},
"fast_yaml": {:hex, :fast_yaml, "1.0.32", "43f53a2c8572f2e4d66cd4e787fc6761b1c65b9132e42c511d8b9540b0989d65", [:rebar3], [{:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "7258e322739ff0824237ebe44cd158e0bf52cd27a15fe731cf92f4b4c70b913e"},
"goldrush": {:hex, :goldrush, "0.1.9", "f06e5d5f1277da5c413e84d5a2924174182fb108dabb39d5ec548b27424cd106", [:rebar3], [], "hexpm", "99cb4128cffcb3227581e5d4d803d5413fa643f4eb96523f77d9e6937d994ceb"},
"idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "4bdd305eb64e18b0273864920695cb18d7a2021f31a11b9c5fbcd9a253f936e2"},
"jiffy": {:hex, :jiffy, "1.0.5", "a69b58faf7123534c20e1b0b7ae97ac52079ca02ed4b6989b4b380179cd63a54", [:rebar3], [], "hexpm", "b617a53f46ae84f20d0c38951367dc947a2cf8cff922aa5c6ac6b64b8b052289"},
- "jose": {:hex, :jose, "1.9.0", "4167c5f6d06ffaebffd15cdb8da61a108445ef5e85ab8f5a7ad926fdf3ada154", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm", "6429c4fee52b2dda7861ee19a4f09c8c1ffa213bee3a1ec187828fde95d447ed"},
+ "jose": {:hex, :jose, "1.11.1", "59da64010c69aad6cde2f5b9248b896b84472e99bd18f246085b7b9fe435dcdb", [:mix, :rebar3], [], "hexpm", "078f6c9fb3cd2f4cfafc972c814261a7d1e8d2b3685c0a76eb87e158efff1ac5"},
"lager": {:hex, :lager, "3.9.2", "4cab289120eb24964e3886bd22323cb5fefe4510c076992a23ad18cf85413d8c", [:rebar3], [{:goldrush, "0.1.9", [hex: :goldrush, repo: "hexpm", optional: false]}], "hexpm", "7f904d9e87a8cb7e66156ed31768d1c8e26eba1d54f4bc85b1aa4ac1f6340c28"},
"makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"},
- "makeup_elixir": {:hex, :makeup_elixir, "0.15.1", "b5888c880d17d1cc3e598f05cdb5b5a91b7b17ac4eaf5f297cb697663a1094dd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "db68c173234b07ab2a07f645a5acdc117b9f99d69ebf521821d89690ae6c6ec8"},
+ "makeup_elixir": {:hex, :makeup_elixir, "0.15.2", "dc72dfe17eb240552857465cc00cce390960d9a0c055c4ccd38b70629227e97c", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fd23ae48d09b32eff49d4ced2b43c9f086d402ee4fd4fcb2d7fad97fa8823e75"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
"mqtree": {:hex, :mqtree, "1.0.14", "d201a79b51a9232b80e764b4b77a866f7c30a90c7ac6205d71f391eb3ea7eb31", [:rebar3], [{:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "8626dac5e862b575eaf4836f0fc1be5a7c8435c378c5a309e34ee012d48b6f6e"},
- "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"},
- "p1_acme": {:hex, :p1_acme, "1.0.13", "fec71df416004ce49e295f4846fe5ba3478b41fbe4f73a06b4a8fbc967d6e659", [:rebar3], [{:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jiffy, "1.0.5", [hex: :jiffy, repo: "hexpm", optional: false]}, {:jose, "1.9.0", [hex: :jose, repo: "hexpm", optional: false]}, {:yconf, "1.0.12", [hex: :yconf, repo: "hexpm", optional: false]}], "hexpm", "a2ce9d4904304df020c8e92e8577e0fc88f32623540656317c7e25440b4ac8d2"},
+ "nimble_parsec": {:hex, :nimble_parsec, "1.2.0", "b44d75e2a6542dcb6acf5d71c32c74ca88960421b6874777f79153bbbbd7dccc", [:mix], [], "hexpm", "52b2871a7515a5ac49b00f214e4165a40724cf99798d8e4a65e4fd64ebd002c1"},
+ "p1_acme": {:hex, :p1_acme, "1.0.16", "88b84cc24c9b6eb87204ea53969ccd9b524dcd4142de632441fdd2859ccab778", [:rebar3], [{:base64url, "1.0.1", [hex: :base64url, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jiffy, "1.0.5", [hex: :jiffy, repo: "hexpm", optional: false]}, {:jose, "1.11.1", [hex: :jose, repo: "hexpm", optional: false]}, {:yconf, "1.0.12", [hex: :yconf, repo: "hexpm", optional: false]}], "hexpm", "ec0ef380a7345c38b57899733f6fece97c337a3d44fd02cc8898f6a2491a38a8"},
"p1_mysql": {:hex, :p1_mysql, "1.0.19", "22f1be58397780a7d580a954e7af66cde32a29dee1a24ab2aa196272fc654a4a", [:rebar3], [], "hexpm", "88f6cdb510e8959c14b6ae84ccda04967e3de239228f859d8341da67949622b1"},
"p1_oauth2": {:hex, :p1_oauth2, "0.6.10", "09ba1fbd447b1f480b223903e36d0415f21be592a1b00db964eea01285749028", [:rebar3], [], "hexpm", "c79cb61ababee4a8c85409b7f4932035797c093aeef1f9f53985e512b26f2a64"},
"p1_pgsql": {:hex, :p1_pgsql, "1.1.12", "10ae79eeb35ea98c0424a8b6420542fef9e4469eb12ccf41475d10840c291e68", [:rebar3], [], "hexpm", "32203f779e01cf0353270df24833a1d831ad7cb3e3e8e35a7556dfa1f40948d5"},
"p1_utils": {:hex, :p1_utils, "1.0.23", "7f94466ada69bd982ea7bb80fbca18e7053e7d0b82c9d9e37621fa508587069b", [:rebar3], [], "hexpm", "47f21618694eeee5006af1c88731ad86b757161e7823c29b6f73921b571c8502"},
"pkix": {:hex, :pkix, "1.0.8", "98ea05243847fd4504f7c7a0cd82cecd1010ac327a082e1c674c5384006eae75", [:rebar3], [], "hexpm", "399508819501fab9d2e586dfa601b5ee3ef22b5612d3db58204dd2d089ef45d7"},
"stringprep": {:hex, :stringprep, "1.0.27", "02808c7024bc6285ca6a8a67e7addfc16f35dda55551a582c5181d8ea960e890", [:rebar3], [{:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "a5967b1144ca8002a58a03d16dd109fbd0bcdb82616cead2f983944314af6a00"},
- "stun": {:hex, :stun, "1.0.44", "30b6b774864b24b05ba901291abe583bff19081e7c4efb3361df50b781ec9d3b", [:rebar3], [{:fast_tls, "1.1.13", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "e45bba816cbefff01d820e49e66814f450df25a7a468a70d68d1e64218d46520"},
+ "stun": {:hex, :stun, "1.0.45", "3363fd4911f408c712c48baa3811548768dcd49a6c30d8f3c54c119bbbacd3ea", [:rebar3], [{:fast_tls, "1.1.13", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "89774eb5500d5360dc0c1d0504b34afa891f91c807637309c449841ae91f7818"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm", "1d1848c40487cdb0b30e8ed975e34e025860c02e419cb615d255849f3427439d"},
- "xmpp": {:hex, :xmpp, "1.5.4", "6cd8144b3fe04745dc2cb3e746d6f2a963bb283db48a61f159b49cbe3fab8623", [:rebar3], [{:ezlib, "1.0.10", [hex: :ezlib, repo: "hexpm", optional: false]}, {:fast_tls, "1.1.13", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:fast_xml, "1.1.47", [hex: :fast_xml, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}, {:stringprep, "1.0.27", [hex: :stringprep, repo: "hexpm", optional: false]}], "hexpm", "3bc2b5cb24e52964fb11641422ce2b7ba7c261dd50080689a1cbe3d952a9db35"},
+ "xmpp": {:hex, :xmpp, "1.5.6", "09259177a39c880d682817932f4da0537c471160fd43aa891ea9cb71cf827b52", [:rebar3], [{:ezlib, "1.0.10", [hex: :ezlib, repo: "hexpm", optional: false]}, {:fast_tls, "1.1.13", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:fast_xml, "1.1.48", [hex: :fast_xml, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}, {:stringprep, "1.0.27", [hex: :stringprep, repo: "hexpm", optional: false]}], "hexpm", "59b7317c4077d3384f9a891e0517a591cdbd44a323260b835eafbede4f4eb12e"},
"yconf": {:hex, :yconf, "1.0.12", "78c119d39bb805207fcb7671cb884805d75ee89c9ec98632b678f90a597dee2c", [:rebar3], [{:fast_yaml, "1.0.32", [hex: :fast_yaml, repo: "hexpm", optional: false]}], "hexpm", "12faa51c281e95bcb6abf185fd034a242209621a7bb04b6cc411c867b192e207"},
}
diff --git a/rebar.config b/rebar.config
index 798e6350b..3490d976c 100644
--- a/rebar.config
+++ b/rebar.config
@@ -30,25 +30,25 @@
{if_var_true, redis,
{eredis, ".*", {git, "https://github.com/wooga/eredis", {tag, "v1.2.0"}}}},
{if_var_true, sip,
- {esip, ".*", {git, "https://github.com/processone/esip", {tag, "1.0.43"}}}},
+ {esip, ".*", {git, "https://github.com/processone/esip", {tag, "1.0.44"}}}},
{if_var_true, zlib,
{ezlib, ".*", {git, "https://github.com/processone/ezlib", {tag, "1.0.10"}}}},
{fast_tls, ".*", {git, "https://github.com/processone/fast_tls", {tag, "1.1.13"}}},
- {fast_xml, ".*", {git, "https://github.com/processone/fast_xml", {tag, "1.1.47"}}},
+ {fast_xml, ".*", {git, "https://github.com/processone/fast_xml", {tag, "1.1.48"}}},
{fast_yaml, ".*", {git, "https://github.com/processone/fast_yaml", {tag, "1.0.32"}}},
{idna, ".*", {git, "https://github.com/benoitc/erlang-idna", {tag, "6.0.0"}}},
{jiffy, ".*", {git, "https://github.com/davisp/jiffy", {tag, "1.0.5"}}},
- {jose, ".*", {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.9.0"}}},
+ {jose, ".*", {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.1"}}},
{lager, ".*", {git, "https://github.com/erlang-lager/lager", {tag, "3.9.1"}}},
{if_var_true, lua,
{luerl, ".*", {git, "https://github.com/rvirding/luerl", {tag, "v0.3"}}}},
{mqtree, ".*", {git, "https://github.com/processone/mqtree", {tag, "1.0.14"}}},
- {p1_acme, ".*", {git, "https://github.com/processone/p1_acme", {tag, "1.0.13"}}},
+ {p1_acme, ".*", {git, "https://github.com/processone/p1_acme", {tag, "1.0.16"}}},
{if_var_true, mysql,
{p1_mysql, ".*", {git, "https://github.com/processone/p1_mysql", {tag, "1.0.19"}}}},
{p1_oauth2, ".*", {git, "https://github.com/processone/p1_oauth2", {tag, "0.6.10"}}},
{if_var_true, pgsql,
- {p1_pgsql, ".*", {git, "https://github.com/processone/p1_pgsql", {tag, "1.1.12"}}}},
+ {p1_pgsql, ".*", {git, "https://github.com/processone/p1_pgsql", {tag, "1.1.16"}}}},
{p1_utils, ".*", {git, "https://github.com/processone/p1_utils", {tag, "1.0.23"}}},
{pkix, ".*", {git, "https://github.com/processone/pkix", {tag, "1.0.8"}}},
{if_not_rebar3, %% Needed because modules are not fully migrated to new structure and mix
@@ -58,8 +58,8 @@
{sqlite3, ".*", {git, "https://github.com/processone/erlang-sqlite3", {tag, "1.1.13"}}}},
{stringprep, ".*", {git, "https://github.com/processone/stringprep", {tag, "1.0.27"}}},
{if_var_true, stun,
- {stun, ".*", {git, "https://github.com/processone/stun", {tag, "1.0.44"}}}},
- {xmpp, ".*", {git, "https://github.com/processone/xmpp", {tag, "1.5.4"}}},
+ {stun, ".*", {git, "https://github.com/processone/stun", {tag, "1.0.45"}}}},
+ {xmpp, ".*", {git, "https://github.com/processone/xmpp", {tag, "1.5.6"}}},
{yconf, ".*", {git, "https://github.com/processone/yconf", {tag, "1.0.12"}}}
]}.
diff --git a/sql/mysql.new.sql b/sql/mysql.new.sql
index c6d74bb91..01aeffbc5 100644
--- a/sql/mysql.new.sql
+++ b/sql/mysql.new.sql
@@ -86,7 +86,7 @@ CREATE TABLE sr_user (
PRIMARY KEY (server_host(191), jid, grp)
) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-CREATE UNIQUE INDEX i_sr_user_sh_jid_group ON sr_group(server_host(191), jid, grp);
+CREATE UNIQUE INDEX i_sr_user_sh_jid_group ON sr_user(server_host(191), jid, grp);
CREATE INDEX i_sr_user_sh_jid ON sr_user(server_host(191), jid);
CREATE INDEX i_sr_user_sh_grp ON sr_user(server_host(191), grp);
diff --git a/sql/mysql.old-to-new.sql b/sql/mysql.old-to-new.sql
index 59c9befe2..9614d55a8 100644
--- a/sql/mysql.old-to-new.sql
+++ b/sql/mysql.old-to-new.sql
@@ -77,6 +77,7 @@ BEGIN
ALTER TABLE `last` ADD PRIMARY KEY (`server_host`, `username`);
ALTER TABLE `sr_group` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `name`;
ALTER TABLE `sr_group` ALTER COLUMN `server_host` DROP DEFAULT;
+ ALTER TABLE `sr_group` ADD UNIQUE INDEX `i_sr_group_sh_name` (`server_host`, `name`);
ALTER TABLE `sr_group` ADD PRIMARY KEY (`server_host`, `name`);
ALTER TABLE `muc_registered` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `host`;
ALTER TABLE `muc_registered` ALTER COLUMN `server_host` DROP DEFAULT;
@@ -99,6 +100,7 @@ BEGIN
ALTER TABLE `sr_user` DROP INDEX `i_sr_user_jid_group`;
ALTER TABLE `sr_user` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `jid`;
ALTER TABLE `sr_user` ALTER COLUMN `server_host` DROP DEFAULT;
+ ALTER TABLE `sr_user` ADD UNIQUE INDEX `i_sr_user_sh_jid_group` (`server_host`, `jid`, `grp`);
ALTER TABLE `sr_user` ADD INDEX `i_sr_user_sh_jid` (`server_host`, `jid`);
ALTER TABLE `sr_user` ADD INDEX `i_sr_user_sh_grp` (`server_host`, `grp`);
ALTER TABLE `sr_user` ADD PRIMARY KEY (`server_host`, `jid`, `grp`);
diff --git a/sql/pg.new.sql b/sql/pg.new.sql
index 8a35ec6c9..6a0743f23 100644
--- a/sql/pg.new.sql
+++ b/sql/pg.new.sql
@@ -156,6 +156,12 @@
-- CREATE INDEX i_sm_sh_username ON sm USING btree (server_host, username);
-- ALTER TABLE sm ALTER COLUMN server_host DROP DEFAULT;
+-- ALTER TABLE push_session ADD COLUMN server_host text NOT NULL DEFAULT '<HOST>';
+-- DROP INDEX i_push_usn;
+-- DROP INDEX i_push_ut;
+-- ALTER TABLE push_session ADD PRIMARY KEY (server_host, username, timestamp);
+-- CREATE UNIQUE INDEX i_push_session_susn ON push_session USING btree (server_host, username, service, node);
+
CREATE TABLE users (
username text NOT NULL,
@@ -305,7 +311,7 @@ CREATE TABLE vcard_search (
lorgname text NOT NULL,
orgunit text NOT NULL,
lorgunit text NOT NULL,
- PRIMARY KEY (server_host, username)
+ PRIMARY KEY (server_host, lusername)
);
CREATE INDEX i_vcard_search_sh_lfn ON vcard_search(server_host, lfn);
diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl
index 42f17baa1..93ce3c815 100644
--- a/src/ejabberd_acme.erl
+++ b/src/ejabberd_acme.erl
@@ -27,7 +27,8 @@
%% Hooks
-export([ejabberd_started/0, register_certfiles/0, cert_expired/2]).
%% ejabberd commands
--export([request_certificate/1, revoke_certificate/1, list_certificates/0]).
+-export([get_commands_spec/0, request_certificate/1,
+ revoke_certificate/1, list_certificates/0]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3, format_status/2]).
diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl
index 2eeb97b99..14305e84b 100644
--- a/src/ejabberd_admin.erl
+++ b/src/ejabberd_admin.erl
@@ -119,12 +119,15 @@ get_commands_spec() ->
desc = "Restart ejabberd gracefully",
module = init, function = restart,
args = [], result = {res, rescode}},
- #ejabberd_commands{name = reopen_log, tags = [logs, server],
- desc = "Reopen the log files",
+ #ejabberd_commands{name = reopen_log, tags = [logs],
+ desc = "Reopen the log files after being renamed",
+ longdesc = "This can be useful when an external tool is "
+ "used for log rotation. See "
+ "https://docs.ejabberd.im/admin/guide/troubleshooting/#log-files",
policy = admin,
module = ?MODULE, function = reopen_log,
args = [], result = {res, rescode}},
- #ejabberd_commands{name = rotate_log, tags = [logs, server],
+ #ejabberd_commands{name = rotate_log, tags = [logs],
desc = "Rotate the log files",
module = ?MODULE, function = rotate_log,
args = [], result = {res, rescode}},
@@ -139,14 +142,14 @@ get_commands_spec() ->
args_example = [60, <<"Server will stop now.">>],
args = [{delay, integer}, {announcement, string}],
result = {res, rescode}},
- #ejabberd_commands{name = get_loglevel, tags = [logs, server],
+ #ejabberd_commands{name = get_loglevel, tags = [logs],
desc = "Get the current loglevel",
module = ejabberd_logger, function = get,
result_desc = "Tuple with the log level number, its keyword and description",
result_example = warning,
args = [],
result = {levelatom, atom}},
- #ejabberd_commands{name = set_loglevel, tags = [logs, server],
+ #ejabberd_commands{name = set_loglevel, tags = [logs],
desc = "Set the loglevel",
module = ?MODULE, function = set_loglevel,
args_desc = ["Desired logging level: none | emergency | alert | critical "
@@ -200,7 +203,7 @@ get_commands_spec() ->
result_example = [<<"example.com">>, <<"anon.example.com">>],
args = [],
result = {vhosts, {list, {vhost, string}}}},
- #ejabberd_commands{name = reload_config, tags = [server, config],
+ #ejabberd_commands{name = reload_config, tags = [config],
desc = "Reload config file in memory",
module = ?MODULE, function = reload_config,
args = [],
@@ -254,21 +257,21 @@ get_commands_spec() ->
module = ejabberd_piefxis, function = import_file,
args_desc = ["Full path to the PIEFXIS file"],
args_example = ["/var/lib/ejabberd/example.com.xml"],
- args = [{file, string}], result = {res, rescode}},
+ args = [{file, binary}], result = {res, rescode}},
#ejabberd_commands{name = export_piefxis, tags = [mnesia],
desc = "Export data of all users in the server to PIEFXIS files (XEP-0227)",
module = ejabberd_piefxis, function = export_server,
args_desc = ["Full path to a directory"],
args_example = ["/var/lib/ejabberd/"],
- args = [{dir, string}], result = {res, rescode}},
+ args = [{dir, binary}], result = {res, rescode}},
#ejabberd_commands{name = export_piefxis_host, tags = [mnesia],
desc = "Export data of users in a host to PIEFXIS files (XEP-0227)",
module = ejabberd_piefxis, function = export_host,
args_desc = ["Full path to a directory", "Vhost to export"],
args_example = ["/var/lib/ejabberd/", "example.com"],
- args = [{dir, string}, {host, string}], result = {res, rescode}},
+ args = [{dir, binary}, {host, binary}], result = {res, rescode}},
- #ejabberd_commands{name = delete_mnesia, tags = [mnesia, sql],
+ #ejabberd_commands{name = delete_mnesia, tags = [mnesia],
desc = "Delete elements in Mnesia database for a given vhost",
module = ejd2sql, function = delete,
args_desc = ["Vhost which content will be deleted in Mnesia database"],
@@ -326,7 +329,7 @@ get_commands_spec() ->
args_example = ["example.com", "/var/lib/ejabberd/example.com.sql"],
args = [{host, string}, {file, string}],
result = {res, rescode}},
- #ejabberd_commands{name = set_master, tags = [mnesia],
+ #ejabberd_commands{name = set_master, tags = [cluster],
desc = "Set master node of the clustered Mnesia tables",
longdesc = "If you provide as nodename \"self\", this "
"node will be set as its own master.",
@@ -345,31 +348,41 @@ get_commands_spec() ->
{oldbackup, string}, {newbackup, string}],
result = {res, restuple}},
#ejabberd_commands{name = backup, tags = [mnesia],
- desc = "Store the database to backup file",
+ desc = "Backup the Mnesia database to a binary file",
module = ?MODULE, function = backup_mnesia,
args_desc = ["Full path for the destination backup file"],
args_example = ["/var/lib/ejabberd/database.backup"],
args = [{file, string}], result = {res, restuple}},
#ejabberd_commands{name = restore, tags = [mnesia],
- desc = "Restore the database from backup file",
+ desc = "Restore the Mnesia database from a binary backup file",
+ longdesc = "This restores immediately from a "
+ "binary backup file the internal Mnesia "
+ "database. This will consume a lot of memory if "
+ "you have a large database, you may prefer "
+ "'install_fallback'.",
module = ?MODULE, function = restore_mnesia,
args_desc = ["Full path to the backup file"],
args_example = ["/var/lib/ejabberd/database.backup"],
args = [{file, string}], result = {res, restuple}},
#ejabberd_commands{name = dump, tags = [mnesia],
- desc = "Dump the database to a text file",
+ desc = "Dump the Mnesia database to a text file",
module = ?MODULE, function = dump_mnesia,
args_desc = ["Full path for the text file"],
args_example = ["/var/lib/ejabberd/database.txt"],
args = [{file, string}], result = {res, restuple}},
#ejabberd_commands{name = dump_table, tags = [mnesia],
- desc = "Dump a table to a text file",
+ desc = "Dump a Mnesia table to a text file",
module = ?MODULE, function = dump_table,
args_desc = ["Full path for the text file", "Table name"],
args_example = ["/var/lib/ejabberd/table-muc-registered.txt", "muc_registered"],
args = [{file, string}, {table, string}], result = {res, restuple}},
#ejabberd_commands{name = load, tags = [mnesia],
- desc = "Restore the database from a text file",
+ desc = "Restore Mnesia database from a text dump file",
+ longdesc = "Restore immediately. This is not "
+ "recommended for big databases, as it will "
+ "consume much time, memory and processor. In "
+ "that case it's preferable to use 'backup' and "
+ "'install_fallback'.",
module = ?MODULE, function = load_mnesia,
args_desc = ["Full path to the text file"],
args_example = ["/var/lib/ejabberd/database.txt"],
@@ -385,7 +398,14 @@ get_commands_spec() ->
args_example = ["roster"],
args = [{table, string}], result = {res, string}},
#ejabberd_commands{name = install_fallback, tags = [mnesia],
- desc = "Install the database from a fallback file",
+ desc = "Install Mnesia database from a binary backup file",
+ longdesc = "The binary backup file is "
+ "installed as fallback: it will be used to "
+ "restore the database at the next ejabberd "
+ "start. This means that, after running this "
+ "command, you have to restart ejabberd. This "
+ "command requires less memory than
+ 'restore'.",
module = ?MODULE, function = install_fallback_mnesia,
args_desc = ["Full path to the fallback file"],
args_example = ["/var/lib/ejabberd/database.fallback"],
diff --git a/src/ejabberd_auth_sql.erl b/src/ejabberd_auth_sql.erl
index 1f7106c59..50cc1902e 100644
--- a/src/ejabberd_auth_sql.erl
+++ b/src/ejabberd_auth_sql.erl
@@ -299,6 +299,20 @@ export(_Server) ->
["username=%(LUser)s",
"server_host=%(LServer)s",
"password=%(Password)s"])];
+ (Host, {passwd, {LUser, LServer},
+ {scram, StoredKey1, ServerKey, Salt, IterationCount}})
+ when LServer == Host ->
+ Hash = sha,
+ StoredKey = scram_hash_encode(Hash, StoredKey1),
+ [?SQL("delete from users where username=%(LUser)s and %(LServer)H;"),
+ ?SQL_INSERT(
+ "users",
+ ["username=%(LUser)s",
+ "server_host=%(LServer)s",
+ "password=%(StoredKey)s",
+ "serverkey=%(ServerKey)s",
+ "salt=%(Salt)s",
+ "iterationcount=%(IterationCount)d"])];
(Host, #passwd{us = {LUser, LServer}, password = #scram{} = Scram})
when LServer == Host ->
StoredKey = scram_hash_encode(Scram#scram.hash, Scram#scram.storedkey),
diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl
index 0f86f23d3..c00b1469a 100644
--- a/src/ejabberd_commands.erl
+++ b/src/ejabberd_commands.erl
@@ -41,6 +41,7 @@
get_tags_commands/0,
get_tags_commands/1,
register_commands/1,
+ register_commands/2,
unregister_commands/1,
get_commands_spec/0,
get_commands_definition/0,
@@ -90,6 +91,16 @@ get_commands_spec() ->
"that will have example invocation include in markdown document"],
result_desc = "0 if command failed, 1 when succeeded",
args_example = ["/home/me/docs/api.html", "mod_admin", "java,json"],
+ result_example = ok},
+ #ejabberd_commands{name = gen_markdown_doc_for_tags, tags = [documentation],
+ desc = "Generates markdown documentation for ejabberd_commands",
+ module = ejabberd_commands_doc, function = generate_tags_md,
+ args = [{file, binary}],
+ result = {res, rescode},
+ args_desc = ["Path to file where generated "
+ "documentation should be stored"],
+ result_desc = "0 if command failed, 1 when succeeded",
+ args_example = ["/home/me/docs/tags.md"],
result_example = ok}].
start_link() ->
@@ -129,10 +140,13 @@ code_change(_OldVsn, State, _Extra) ->
-spec register_commands([ejabberd_commands()]) -> ok.
register_commands(Commands) ->
+ register_commands(unknown, Commands).
+
+register_commands(Definer, Commands) ->
lists:foreach(
fun(Command) ->
%% XXX check if command exists
- mnesia:dirty_write(Command)
+ mnesia:dirty_write(Command#ejabberd_commands{definer = Definer})
%% ?DEBUG("This command is already defined:~n~p", [Command])
end,
Commands),
diff --git a/src/ejabberd_commands_doc.erl b/src/ejabberd_commands_doc.erl
index 4981577b6..eec334d04 100644
--- a/src/ejabberd_commands_doc.erl
+++ b/src/ejabberd_commands_doc.erl
@@ -28,6 +28,7 @@
-export([generate_html_output/3]).
-export([generate_md_output/3]).
+-export([generate_tags_md/1]).
-include("ejabberd_commands.hrl").
@@ -360,8 +361,16 @@ gen_param(Name, Type, Desc, HTMLOutput) ->
[?TAG(dt, [?TAG_R(strong, atom_to_list(Name)), <<" :: ">>, ?RAW(format_type(Type))]),
?TAG(dd, ?RAW(Desc))].
-gen_doc(#ejabberd_commands{name=Name, tags=_Tags, desc=Desc, longdesc=LongDesc,
- args=Args, args_desc=ArgsDesc, note=Note,
+make_tags(HTMLOutput) ->
+ TagsList = ejabberd_commands:get_tags_commands(1000000),
+ lists:map(fun(T) -> gen_tags(T, HTMLOutput) end, TagsList).
+
+-dialyzer({no_match, gen_tags/2}).
+gen_tags({TagName, Commands}, HTMLOutput) ->
+ [?TAG(h1, TagName) | [?TAG(p, ?RAW("* *`"++C++"`*")) || C <- Commands]].
+
+gen_doc(#ejabberd_commands{name=Name, tags=Tags, desc=Desc, longdesc=LongDesc,
+ args=Args, args_desc=ArgsDesc, note=Note, definer=Definer,
result=Result, result_desc=ResultDesc}=Cmd, HTMLOutput, Langs) ->
try
ArgsText = case ArgsDesc of
@@ -389,6 +398,17 @@ gen_doc(#ejabberd_commands{name=Name, tags=_Tags, desc=Desc, longdesc=LongDesc,
[?TAG(dl, [gen_param(RName, Type, ResultDesc, HTMLOutput)])]
end
end,
+ TagsText = [?RAW("*`"++atom_to_list(Tag)++"`* ") || Tag <- Tags],
+ IsDefinerMod = case Definer of
+ unknown -> true;
+ _ -> lists:member(gen_mod, proplists:get_value(behaviour, Definer:module_info(attributes)))
+ end,
+ ModuleText = case IsDefinerMod of
+ true ->
+ [?TAG(h2, <<"Module:">>), ?TAG(p, ?RAW("*`"++atom_to_list(Definer)++"`*"))];
+ false ->
+ []
+ end,
NoteEl = case Note of
"" -> [];
_ -> ?TAG('div', "note-down", ?RAW(Note))
@@ -403,6 +423,8 @@ gen_doc(#ejabberd_commands{name=Name, tags=_Tags, desc=Desc, longdesc=LongDesc,
end,
?TAG(h2, <<"Arguments:">>), ArgsText,
?TAG(h2, <<"Result:">>), ResultText,
+ ?TAG(h2, <<"Tags:">>), ?TAG(p, TagsText)]
+ ++ ModuleText ++ [
?TAG(h2, <<"Examples:">>), gen_calls(Cmd, HTMLOutput, Langs)]
catch
_:Ex ->
@@ -421,12 +443,13 @@ find_commands_definitions() ->
lists:flatmap(fun(P) ->
Mod = list_to_atom(filename:rootname(P)),
code:ensure_loaded(Mod),
- case erlang:function_exported(Mod, get_commands_spec, 0) of
+ Cs = case erlang:function_exported(Mod, get_commands_spec, 0) of
true ->
apply(Mod, get_commands_spec, []);
_ ->
[]
- end
+ end,
+ [C#ejabberd_commands{definer = Mod} || C <- Cs]
end, filelib:wildcard("*.beam", Path))
end.
@@ -466,15 +489,25 @@ generate_md_output(File, RegExp, Languages) ->
end, Cmds2),
Cmds4 = [maybe_add_policy_arguments(Cmd) || Cmd <- Cmds3],
Langs = binary:split(Languages, <<",">>, [global]),
- Header = <<"---\ntitle: Administration API reference\ntoc: true\nmenu: Administration API\norder: 40\n"
+ Header = <<"---\ntitle: Administration API reference\ntoc: true\nmenu: API Reference\norder: 1\n"
"// Autogenerated with 'ejabberdctl gen_markdown_doc_for_commands'\n---\n\n"
- "This section describes API of ejabberd.">>,
+ "This section describes API of ejabberd.\n">>,
Out = lists:map(fun(C) -> gen_doc(C, false, Langs) end, Cmds4),
{ok, Fh} = file:open(File, [write]),
io:format(Fh, "~ts~ts", [Header, Out]),
file:close(Fh),
ok.
+generate_tags_md(File) ->
+ Header = <<"---\ntitle: API Tags\ntoc: true\nmenu: API Tags\norder: 2\n"
+ "// Autogenerated with 'ejabberdctl gen_markdown_doc_for_tags'\n---\n\n"
+ "This section enumerates the tags and their associated API.\n">>,
+ Tags = make_tags(false),
+ {ok, Fh} = file:open(File, [write]),
+ io:format(Fh, "~ts~ts", [Header, Tags]),
+ file:close(Fh),
+ ok.
+
html_pre() ->
"<!DOCTYPE>
<html>
diff --git a/src/ejabberd_ctl.erl b/src/ejabberd_ctl.erl
index ce642727f..354e5ba2f 100644
--- a/src/ejabberd_ctl.erl
+++ b/src/ejabberd_ctl.erl
@@ -525,6 +525,7 @@ print_usage(Version) ->
print_usage(HelpMode, MaxC, ShCode, Version) ->
AllCommands =
[
+ {"help", ["[arguments]"], "Get help"},
{"status", [], "Get ejabberd status"},
{"stop", [], "Stop ejabberd"},
{"restart", [], "Restart ejabberd"},
@@ -827,6 +828,7 @@ print_usage_command(Cmd, MaxC, ShCode, Version) ->
print_usage_command2(Cmd, C, MaxC, ShCode) ->
#ejabberd_commands{
tags = TagsAtoms,
+ definer = Definer,
desc = Desc,
args = ArgsDef,
longdesc = LongDesc,
@@ -851,6 +853,15 @@ print_usage_command2(Cmd, C, MaxC, ShCode) ->
TagsFmt = [" ",?B("Tags"),":", prepare_long_line(8, MaxC, [?G(atom_to_list(TagA)) || TagA <- TagsAtoms])],
+ IsDefinerMod = case Definer of
+ unknown -> true;
+ _ -> lists:member(gen_mod, proplists:get_value(behaviour, Definer:module_info(attributes)))
+ end,
+ ModuleFmt = case IsDefinerMod of
+ true -> [" ",?B("Module"),": ", atom_to_list(Definer), "\n\n"];
+ false -> []
+ end,
+
DescFmt = [" ",?B("Description"),":", prepare_description(15, MaxC, Desc)],
LongDescFmt = case LongDesc of
@@ -866,7 +877,7 @@ print_usage_command2(Cmd, C, MaxC, ShCode) ->
case Cmd of
"help" -> ok;
_ -> print([NameFmt, "\n", ArgsFmt, "\n", ReturnsFmt,
- "\n\n", XmlrpcFmt, TagsFmt, "\n\n", DescFmt, "\n\n"], [])
+ "\n\n", XmlrpcFmt, TagsFmt, "\n\n", ModuleFmt, DescFmt, "\n\n"], [])
end,
print([LongDescFmt, NoteEjabberdctl], []).
diff --git a/src/ejabberd_logger.erl b/src/ejabberd_logger.erl
index 5ace8115f..05e197b3e 100644
--- a/src/ejabberd_logger.erl
+++ b/src/ejabberd_logger.erl
@@ -288,10 +288,11 @@ start(Level) ->
ConsoleFmtConfig = FmtConfig#{template => console_template()},
try
ok = logger:set_primary_config(level, Level),
- ok = logger:update_formatter_config(default, ConsoleFmtConfig),
+ DefaultHandlerId = get_default_handlerid(),
+ ok = logger:update_formatter_config(DefaultHandlerId, ConsoleFmtConfig),
case quiet_mode() of
true ->
- ok = logger:set_handler_config(default, level, critical);
+ ok = logger:set_handler_config(DefaultHandlerId, level, critical);
_ ->
ok
end,
@@ -319,6 +320,13 @@ start(Level) ->
Err
end.
+get_default_handlerid() ->
+ Ids = logger:get_handler_ids(),
+ case lists:member(default, Ids) of
+ true -> default;
+ false -> hd(Ids)
+ end.
+
-spec restart() -> ok.
restart() ->
ok.
diff --git a/src/ejabberd_options_doc.erl b/src/ejabberd_options_doc.erl
index 64eb850d8..a725f4572 100644
--- a/src/ejabberd_options_doc.erl
+++ b/src/ejabberd_options_doc.erl
@@ -61,11 +61,11 @@ doc() ->
desc =>
?T("The time of a cached item to keep in cache. "
"Once it's expired, the corresponding item is "
- "erased from cache. The default value is 'one hour'. "
+ "erased from cache. The default value is '1 hour'. "
"Several modules have a similar option; and some core "
"ejabberd parts support similar options too, see "
- "'auth_cache_life_time', 'oauth_cache_life_time', "
- "'router_cache_life_time', and 'sm_cache_life_time'.")}},
+ "_`auth_cache_life_time`_, _`oauth_cache_life_time`_, "
+ "_`router_cache_life_time`_, and _`sm_cache_life_time`_.")}},
{cache_missed,
#{value => "true | false",
desc =>
@@ -73,12 +73,12 @@ doc() ->
"an attempt to lookup for a value in a database and "
"this value is not found and the option is set to 'true', "
"this attempt will be cached and no attempts will be "
- "performed until the cache expires (see 'cache_life_time'). "
+ "performed until the cache expires (see _`cache_life_time`_). "
"Usually you don't want to change it. Default is 'true'. "
"Several modules have a similar option; and some core "
"ejabberd parts support similar options too, see "
- "'auth_cache_missed', 'oauth_cache_missed', "
- "'router_cache_missed', and 'sm_cache_missed'.")}},
+ "_`auth_cache_missed`_, _`oauth_cache_missed`_, "
+ "_`router_cache_missed`_, and _`sm_cache_missed`_.")}},
{cache_size,
#{value => "pos_integer() | infinity",
desc =>
@@ -93,16 +93,16 @@ doc() ->
"performance. The default value is '1000'. "
"Several modules have a similar option; and some core "
"ejabberd parts support similar options too, see "
- "'auth_cache_size', 'oauth_cache_size', "
- "'router_cache_size', and 'sm_cache_size'.")}},
+ "_`auth_cache_size`_, _`oauth_cache_size`_, "
+ "_`router_cache_size`_, and _`sm_cache_size`_.")}},
{use_cache,
#{value => "true | false",
desc =>
?T("Enable or disable cache. The default is 'true'. "
"Several modules have a similar option; and some core "
"ejabberd parts support similar options too, see "
- "'auth_use_cache', 'oauth_use_cache', 'router_use_cache', "
- "and 'sm_use_cache'.")}},
+ "_`auth_use_cache`_, _`oauth_use_cache`_, _`router_use_cache`_, "
+ "and _`sm_use_cache`_.")}},
{default_db,
#{value => "mnesia | sql",
desc =>
@@ -122,14 +122,14 @@ doc() ->
"Modules may have its own value of the option. "
"The value of 'ram' means that queues will be kept in memory. "
"If value 'file' is set, you may also specify directory "
- "in 'queue_dir' option where file queues will be placed. "
+ "in _`queue_dir`_ option where file queues will be placed. "
"The default value is 'ram'.")}},
{version,
#{value => "string()",
desc =>
?T("The option can be used to set custom ejabberd version, "
"that will be used by different parts of ejabberd, for "
- "example by 'mod_version' module. The default value is "
+ "example by _`mod_version`_ module. The default value is "
"obtained at compile time from the underlying version "
"control system.")}},
{acl,
@@ -141,7 +141,7 @@ doc() ->
"has name 'ACLName': it can be any string except 'all' or 'none' "
"(those are predefined names for the rules that match all or nothing "
"respectively). The name 'ACLName' can be referenced from other "
- "parts of the configuration file, for example in 'access_rules' "
+ "parts of the configuration file, for example in _`access_rules`_ "
"option. The rules of 'ACLName' are represented by mapping "
"'pass:[{ACLType: ACLValue}]'. These can be one of the following:")},
[{user,
@@ -225,7 +225,7 @@ doc() ->
"of the configuration file (mostly from 'access' options of "
"ejabberd modules). Each rule definition may contain "
"arbitrary number of 'allow' or 'deny' sections, and each "
- "section may contain any number of ACL rules (see 'acl' option). "
+ "section may contain any number of ACL rules (see _`acl`_ option). "
"There are no access rules defined by default."),
example =>
["access_rules:",
@@ -313,10 +313,12 @@ doc() ->
{anonymous_protocol,
#{value => "login_anon | sasl_anon | both",
desc =>
- ?T("'login_anon' means that the anonymous login method will be used. "
- "'sasl_anon' means that the SASL Anonymous method will be used. "
- "'both' means that SASL Anonymous and login anonymous are both "
- "enabled. The default value is 'sasl_anon'.")}},
+ [?T("Define what anonymous protocol will be used: "), "",
+ ?T("* 'login_anon' means that the anonymous login method will be used. "), "",
+ ?T("* 'sasl_anon' means that the SASL Anonymous method will be used. "), "",
+ ?T("* 'both' means that SASL Anonymous and login anonymous are both "
+ "enabled."), "",
+ ?T("The default value is 'sasl_anon'."), ""]}},
{api_permissions,
#{value => "[Permission, ...]",
desc =>
@@ -334,18 +336,18 @@ doc() ->
{auth_cache_life_time,
#{value => "timeout()",
desc =>
- ?T("Same as 'cache_life_time', but applied to authentication cache "
- "only. If not set, the value from 'cache_life_time' will be used.")}},
+ ?T("Same as _`cache_life_time`_, but applied to authentication cache "
+ "only. If not set, the value from _`cache_life_time`_ will be used.")}},
{auth_cache_missed,
#{value => "true | false",
desc =>
- ?T("Same as 'cache_missed', but applied to authentication cache "
- "only. If not set, the value from 'cache_missed' will be used.")}},
+ ?T("Same as _`cache_missed`_, but applied to authentication cache "
+ "only. If not set, the value from _`cache_missed`_ will be used.")}},
{auth_cache_size,
#{value => "pos_integer() | infinity",
desc =>
- ?T("Same as 'cache_size', but applied to authentication cache "
- "only. If not set, the value from 'cache_size' will be used.")}},
+ ?T("Same as _`cache_size`_, but applied to authentication cache "
+ "only. If not set, the value from _`cache_size`_ will be used.")}},
{auth_method,
#{value => "[mnesia | sql | anonymous | external | jwt | ldap | pam, ...]",
desc =>
@@ -359,38 +361,41 @@ doc() ->
desc =>
?T("This is used by the contributed module "
"'ejabberd_auth_http' that can be installed from the "
- "'ejabberd-contrib' Git repository. Please refer to that "
+ "https://github.com/processone/ejabberd-contrib[ejabberd-contrib] "
+ "Git repository. Please refer to that "
"module's README file for details.")}},
{auth_password_format,
#{value => "plain | scram",
note => "improved in 20.01",
desc =>
- ?T("The option defines in what format the users passwords "
- "are stored. 'plain': The password is stored as plain text "
+ [?T("The option defines in what format the users passwords "
+ "are stored:"), "",
+ ?T("* 'plain': The password is stored as plain text "
"in the database. This is risky because the passwords "
"can be read if your database gets compromised. "
"This is the default value. This format allows clients to "
"authenticate using: the old Jabber Non-SASL (XEP-0078), "
- "SASL PLAIN, SASL DIGEST-MD5, and SASL SCRAM-SHA-1. "
- "'scram': The password is not stored, only some information "
+ "SASL PLAIN, SASL DIGEST-MD5, and SASL SCRAM-SHA-1. "), "",
+ ?T("* 'scram': The password is not stored, only some information "
"that allows to verify the hash provided by the client. "
"It is impossible to obtain the original plain password "
"from the stored information; for this reason, when this "
"value is configured it cannot be changed to plain anymore. "
"This format allows clients to authenticate using: "
- "SASL PLAIN and SASL SCRAM-SHA-1.")}},
+ "SASL PLAIN and SASL SCRAM-SHA-1."),
+ ?T("The default value is 'plain'.")]}},
{auth_scram_hash,
#{value => "sha | sha256 | sha512",
desc =>
?T("Hash algorith that should be used to store password in SCRAM format. "
"You shouldn't change this if you already have passwords generated with "
"a different algorithm - users that have such passwords will not be able "
- "to authenticate.")}},
+ "to authenticate. The default value is 'sha'.")}},
{auth_use_cache,
#{value => "true | false",
desc =>
- ?T("Same as 'use_cache', but applied to authentication cache "
- "only. If not set, the value from 'use_cache' will be used.")}},
+ ?T("Same as _`use_cache`_, but applied to authentication cache "
+ "only. If not set, the value from _`use_cache`_ will be used.")}},
{c2s_cafile,
#{value => ?T("Path"),
desc =>
@@ -449,22 +454,22 @@ doc() ->
{captcha_cmd,
#{value => ?T("Path"),
desc =>
- ?T("Full path to a script that generates CAPTCHA images. "
+ ?T("Full path to a script that generates http://../basic/#captcha[CAPTCHA] images. "
"There is no default value: when this option is not "
"set, CAPTCHA functionality is completely disabled.")}},
{captcha_limit,
#{value => "pos_integer() | infinity",
desc =>
- ?T("Maximum number of CAPTCHA generated images per minute for "
+ ?T("Maximum number of http://../basic/#captcha[CAPTCHA] generated images per minute for "
"any given JID. The option is intended to protect the server "
"from CAPTCHA DoS. The default value is 'infinity'.")}},
{captcha_host,
#{value => "String",
- desc => ?T("Deprecated. Use 'captcha_url' instead.")}},
+ desc => ?T("Deprecated. Use _`captcha_url`_ instead.")}},
{captcha_url,
#{value => ?T("URL"),
desc =>
- ?T("An URL where CAPTCHA requests should be sent. NOTE: you need "
+ ?T("An URL where http://../basic/#captcha[CAPTCHA] requests should be sent. NOTE: you need "
"to configure 'request_handlers' for 'ejabberd_http' listener "
"as well. There is no default value.")}},
{certfiles,
@@ -674,7 +679,8 @@ doc() ->
desc =>
?T("The option defines the default language of server strings "
"that can be seen by XMPP clients. If an XMPP client does not "
- "possess 'xml:lang' attribute, the specified language is used.")}},
+ "possess 'xml:lang' attribute, the specified language is used. "
+ "The default value is '\"en\"'.")}},
{ldap_servers,
#{value => "[Host, ...]",
desc =>
@@ -684,7 +690,7 @@ doc() ->
#{value => "[Host, ...]",
desc =>
?T("A list of IP addresses or DNS names of LDAP backup servers. "
- "When no servers listed in 'ldap_servers' option are reachable, "
+ "When no servers listed in _`ldap_servers`_ option are reachable, "
"ejabberd will try to connect to these backup servers. "
"The default is an empty list, i.e. no backup servers specified. "
"WARNING: ejabberd doesn't try to reconnect back to the main "
@@ -791,7 +797,7 @@ doc() ->
"the result set. There is no default value, which means the "
"result is not filtered. WARNING: Since this filter makes "
"additional LDAP lookups, use it only as the last resort: "
- "try to define all filter rules in 'ldap_filter' option if possible."),
+ "try to define all filter rules in _`ldap_filter`_ option if possible."),
example =>
["ldap_dn_filter:",
" \"(&(name=%s)(owner=%D)(user=%u@%d))\": [sn]"]}},
@@ -799,7 +805,8 @@ doc() ->
#{value => ?T("Number"),
desc =>
?T("The number of rotated log files to keep. "
- "The default value is '1'.")}},
+ "The default value is '1', which means that only keeps "
+ "`ejabberd.log.0`, `error.log.0` and `crash.log.0`.")}},
{log_rotate_size,
#{value => "pos_integer() | infinity",
desc =>
@@ -835,7 +842,7 @@ doc() ->
"must have identical value on all nodes, or it will lead to subtle "
"bugs. Usually leaving default value of this is option is best, "
"tweak it only if you know what you are doing. "
- "The default value is '1' minute.")}},
+ "The default value is '1 minute'.")}},
{new_sql_schema,
#{value => "true | false",
desc =>
@@ -861,13 +868,13 @@ doc() ->
{oauth_cache_life_time,
#{value => "timeout()",
desc =>
- ?T("Same as 'cache_life_time', but applied to OAuth cache "
- "only. If not set, the value from 'cache_life_time' will be used.")}},
+ ?T("Same as _`cache_life_time`_, but applied to OAuth cache "
+ "only. If not set, the value from _`cache_life_time`_ will be used.")}},
{oauth_cache_missed,
#{value => "true | false",
desc =>
- ?T("Same as 'cache_missed', but applied to OAuth cache "
- "only. If not set, the value from 'cache_missed' will be used.")}},
+ ?T("Same as _`cache_missed`_, but applied to OAuth cache "
+ "only. If not set, the value from _`cache_missed`_ will be used.")}},
{oauth_cache_rest_failure_life_time,
#{value => "timeout()",
note => "added in 21.01",
@@ -877,8 +884,8 @@ doc() ->
{oauth_cache_size,
#{value => "pos_integer() | infinity",
desc =>
- ?T("Same as 'cache_size', but applied to OAuth cache "
- "only. If not set, the value from 'cache_size' will be used.")}},
+ ?T("Same as _`cache_size`_, but applied to OAuth cache "
+ "only. If not set, the value from _`cache_size`_ will be used.")}},
{oauth_client_id_check,
#{value => "allow | db | deny",
desc =>
@@ -888,13 +895,13 @@ doc() ->
{oauth_use_cache,
#{value => "true | false",
desc =>
- ?T("Same as 'use_cache', but applied to OAuth cache "
- "only. If not set, the value from 'use_cache' will be used.")}},
+ ?T("Same as _`use_cache`_, but applied to OAuth cache "
+ "only. If not set, the value from _`use_cache`_ will be used.")}},
{oauth_db_type,
#{value => "mnesia | sql",
desc =>
?T("Database backend to use for OAuth authentication. "
- "The default value is picked from 'default_db' option, or "
+ "The default value is picked from _`default_db`_ option, or "
"if it's not set, 'mnesia' will be used.")}},
{oauth_expire,
#{value => "timeout()",
@@ -908,7 +915,7 @@ doc() ->
desc =>
?T("Enable or disable OOM (out-of-memory) killer. "
"When system memory raises above the limit defined in "
- "'oom_watermark' option, ejabberd triggers OOM killer "
+ "_`oom_watermark`_ option, ejabberd triggers OOM killer "
"to terminate most memory consuming Erlang processes. "
"Note that in order to maintain functionality, ejabberd only "
"attempts to kill transient processes, such as those managing "
@@ -919,14 +926,14 @@ doc() ->
desc =>
?T("Trigger OOM killer when some of the running Erlang processes "
"have messages queue above this 'Size'. Note that "
- "such processes won't be killed if 'oom_killer' option is set "
+ "such processes won't be killed if _`oom_killer`_ option is set "
"to 'false' or if 'oom_watermark' is not reached yet.")}},
{oom_watermark,
#{value => ?T("Percent"),
desc =>
?T("A percent of total system memory consumed at which "
"OOM killer should be activated with some of the processes "
- "possibly be killed (see 'oom_killer' option). Later, when "
+ "possibly be killed (see _`oom_killer`_ option). Later, when "
"memory drops below this 'Percent', OOM killer is deactivated. "
"The default value is '80' percents.")}},
{outgoing_s2s_families,
@@ -979,7 +986,7 @@ doc() ->
{queue_dir,
#{value => ?T("Directory"),
desc =>
- ?T("If 'queue_type' option is set to 'file', use this 'Directory' "
+ ?T("If _`queue_type`_ option is set to 'file', use this 'Directory' "
"to store file queues. The default is to keep queues inside "
"Mnesia directory.")}},
{redis_connect_timeout,
@@ -1009,8 +1016,8 @@ doc() ->
#{value => "ram | file",
desc =>
?T("The type of request queue for the Redis server. "
- "See description of 'queue_type' option for the explanation. "
- "The default value is the value defined in 'queue_type' "
+ "See description of _`queue_type`_ option for the explanation. "
+ "The default value is the value defined in _`queue_type`_ "
"or 'ram' if the latter is not set.")}},
{redis_server,
#{value => ?T("Hostname"),
@@ -1020,7 +1027,7 @@ doc() ->
{registration_timeout,
#{value => "timeout()",
desc =>
- ?T("This is a global option for module 'mod_register'. "
+ ?T("This is a global option for module _`mod_register`_. "
"It limits the frequency of registrations from a given "
"IP or username. So, a user that tries to register a "
"new account from the same IP address or JID during "
@@ -1043,29 +1050,29 @@ doc() ->
{router_cache_life_time,
#{value => "timeout()",
desc =>
- ?T("Same as 'cache_life_time', but applied to routing table cache "
- "only. If not set, the value from 'cache_life_time' will be used.")}},
+ ?T("Same as _`cache_life_time`_, but applied to routing table cache "
+ "only. If not set, the value from _`cache_life_time`_ will be used.")}},
{router_cache_missed,
#{value => "true | false",
desc =>
- ?T("Same as 'cache_missed', but applied to routing table cache "
- "only. If not set, the value from 'cache_missed' will be used.")}},
+ ?T("Same as _`cache_missed`_, but applied to routing table cache "
+ "only. If not set, the value from _`cache_missed`_ will be used.")}},
{router_cache_size,
#{value => "pos_integer() | infinity",
desc =>
- ?T("Same as 'cache_size', but applied to routing table cache "
- "only. If not set, the value from 'cache_size' will be used.")}},
+ ?T("Same as _`cache_size`_, but applied to routing table cache "
+ "only. If not set, the value from _`cache_size`_ will be used.")}},
{router_db_type,
#{value => "mnesia | redis | sql",
desc =>
?T("Database backend to use for routing information. "
- "The default value is picked from 'default_ram_db' option, or "
+ "The default value is picked from _`default_ram_db`_ option, or "
"if it's not set, 'mnesia' will be used.")}},
{router_use_cache,
#{value => "true | false",
desc =>
- ?T("Same as 'use_cache', but applied to routing table cache "
- "only. If not set, the value from 'use_cache' will be used.")}},
+ ?T("Same as _`use_cache`_, but applied to routing table cache "
+ "only. If not set, the value from _`use_cache`_ will be used.")}},
{rpc_timeout,
#{value => "timeout()",
desc =>
@@ -1142,14 +1149,14 @@ doc() ->
#{value => "ram | file",
desc =>
?T("The type of a queue for s2s packets. "
- "See description of 'queue_type' option for the explanation. "
- "The default value is the value defined in 'queue_type' "
+ "See description of _`queue_type`_ option for the explanation. "
+ "The default value is the value defined in _`queue_type`_ "
"or 'ram' if the latter is not set.")}},
{s2s_timeout,
#{value => "timeout()",
desc =>
?T("A time to wait before closing an idle s2s connection. "
- "The default value is '10' minutes.")}},
+ "The default value is '10 minutes'.")}},
{s2s_use_starttls,
#{value => "true | false | optional | required",
desc =>
@@ -1171,7 +1178,7 @@ doc() ->
desc =>
?T("The option defines a set of shapers. Every shaper is assigned "
"a name 'ShaperName' that can be used in other parts of the "
- "configuration file, such as 'shaper_rules' option. The shaper "
+ "configuration file, such as _`shaper_rules`_ option. The shaper "
"itself is defined by its 'Rate', where 'Rate' stands for the "
"maximum allowed incoming rate in **bytes** per second. "
"When a connection exceeds this limit, ejabberd stops reading "
@@ -1187,9 +1194,9 @@ doc() ->
#{value => "{ShaperRuleName: {Number|ShaperName: ACLRule|ACLName}}",
desc =>
?T("An entry allowing to declaring shaper to use for matching user/hosts. "
- "Semantics is similar to 'access_rules' option, the only difference is "
+ "Semantics is similar to _`access_rules`_ option, the only difference is "
"that instead using 'allow' or 'deny', a name of a shaper (defined in "
- "'shaper' option) or a positive number should be used."),
+ "_`shaper`_ option) or a positive number should be used."),
example =>
["shaper_rules:",
" connections_limit:",
@@ -1205,29 +1212,29 @@ doc() ->
{sm_cache_life_time,
#{value => "timeout()",
desc =>
- ?T("Same as 'cache_life_time', but applied to client sessions table cache "
- "only. If not set, the value from 'cache_life_time' will be used.")}},
+ ?T("Same as _`cache_life_time`_, but applied to client sessions table cache "
+ "only. If not set, the value from _`cache_life_time`_ will be used.")}},
{sm_cache_missed,
#{value => "true | false",
desc =>
- ?T("Same as 'cache_missed', but applied to client sessions table cache "
- "only. If not set, the value from 'cache_missed' will be used.")}},
+ ?T("Same as _`cache_missed`_, but applied to client sessions table cache "
+ "only. If not set, the value from _`cache_missed`_ will be used.")}},
{sm_cache_size,
#{value => "pos_integer() | infinity",
desc =>
- ?T("Same as 'cache_size', but applied to client sessions table cache "
- "only. If not set, the value from 'cache_size' will be used.")}},
+ ?T("Same as _`cache_size`_, but applied to client sessions table cache "
+ "only. If not set, the value from _`cache_size`_ will be used.")}},
{sm_db_type,
#{value => "mnesia | redis | sql",
desc =>
?T("Database backend to use for client sessions information. "
- "The default value is picked from 'default_ram_db' option, or "
+ "The default value is picked from _`default_ram_db`_ option, or "
"if it's not set, 'mnesia' will be used.")}},
{sm_use_cache,
#{value => "true | false",
desc =>
- ?T("Same as 'use_cache', but applied to client sessions table cache "
- "only. If not set, the value from 'use_cache' will be used.")}},
+ ?T("Same as _`use_cache`_, but applied to client sessions table cache "
+ "only. If not set, the value from _`use_cache`_ will be used.")}},
{sql_type,
#{value => "mssql | mysql | odbc | pgsql | sqlite",
desc =>
@@ -1253,7 +1260,7 @@ doc() ->
note => "added in 20.12",
desc =>
?T("Path to the ODBC driver to use to connect to a Microsoft SQL "
- "Server database. This option is only valid if the 'sql_type' "
+ "Server database. This option is only valid if the _`sql_type`_ "
"option is set to 'mssql'. "
"The default value is: 'libtdsodbc.so'")}},
{sql_password,
@@ -1288,8 +1295,8 @@ doc() ->
#{value => "ram | file",
desc =>
?T("The type of a request queue for the SQL server. "
- "See description of 'queue_type' option for the explanation. "
- "The default value is the value defined in 'queue_type' "
+ "See description of _`queue_type`_ option for the explanation. "
+ "The default value is the value defined in _`queue_type`_ "
"or 'ram' if the latter is not set.")}},
{sql_server,
#{value => ?T("Host"),
@@ -1307,15 +1314,15 @@ doc() ->
#{value => ?T("Path"),
desc =>
?T("A path to a file with CA root certificates that will "
- "be used to verify SQL connections. Implies 'sql_ssl' "
- "and 'sql_ssl_verify' options are set to 'true'. "
+ "be used to verify SQL connections. Implies _`sql_ssl`_ "
+ "and _`sql_ssl_verify`_ options are set to 'true'. "
"There is no default which means "
"certificate verification is disabled.")}},
{sql_ssl_certfile,
#{value => ?T("Path"),
desc =>
?T("A path to a certificate file that will be used "
- "for SSL connections to the SQL server. Implies 'sql_ssl' "
+ "for SSL connections to the SQL server. Implies _`sql_ssl`_ "
"option is set to 'true'. There is no default which means "
"ejabberd won't provide a client certificate to the SQL "
"server.")}},
@@ -1323,8 +1330,8 @@ doc() ->
#{value => "true | false",
desc =>
?T("Whether to verify SSL connection to the SQL server against "
- "CA root certificates defined in 'sql_ssl_cafile' option. "
- "Implies 'sql_ssl' option is set to 'true'. "
+ "CA root certificates defined in _`sql_ssl_cafile`_ option. "
+ "Implies _`sql_ssl`_ option is set to 'true'. "
"The default value is 'false'.")}},
{sql_start_interval,
#{value => "timeout()",
diff --git a/src/ejabberd_piefxis.erl b/src/ejabberd_piefxis.erl
index 8dff06837..d62efb300 100644
--- a/src/ejabberd_piefxis.erl
+++ b/src/ejabberd_piefxis.erl
@@ -24,17 +24,15 @@
%%%----------------------------------------------------------------------
%%% Not implemented:
+%%% - PEP nodes export/import
+%%% - message archives export/import
%%% - write mod_piefxis with ejabberdctl commands
-%%% - Export from mod_offline_sql.erl
-%%% - Export from mod_private_sql.erl
-%%% - XEP-227: 6. Security Considerations
%%% - Other schemas of XInclude are not tested, and may not be imported correctly.
%%% - If a host has many users, split that host in XML files with 50 users each.
-%%%% Headers
-module(ejabberd_piefxis).
--protocol({xep, 227, '1.0'}).
+-protocol({xep, 227, '1.1'}).
-export([import_file/1, export_server/1, export_host/2]).
@@ -166,33 +164,66 @@ export_users([], _Server, _Fd) ->
export_user(User, Server, Fd) ->
Password = ejabberd_auth:get_password_s(User, Server),
LServer = jid:nameprep(Server),
- Pass = case ejabberd_auth:password_format(LServer) of
- scram -> format_scram_password(Password);
- _ -> Password
+ {PassPlain, PassScram} = case ejabberd_auth:password_format(LServer) of
+ scram -> {[], [format_scram_password(Password)]};
+ _ -> {[{<<"password">>, Password}], []}
end,
- Els = get_offline(User, Server) ++
+ Els =
+ PassScram ++
+ get_offline(User, Server) ++
get_vcard(User, Server) ++
get_privacy(User, Server) ++
get_roster(User, Server) ++
get_private(User, Server),
print(Fd, fxml:element_to_binary(
#xmlel{name = <<"user">>,
- attrs = [{<<"name">>, User},
- {<<"password">>, Pass}],
+ attrs = [{<<"name">>, User} | PassPlain],
children = Els})).
format_scram_password(#scram{hash = Hash, storedkey = StoredKey, serverkey = ServerKey,
salt = Salt, iterationcount = IterationCount}) ->
- StoredKeyB64 = base64:encode(StoredKey),
- ServerKeyB64 = base64:encode(ServerKey),
- SaltB64 = base64:encode(Salt),
- IterationCountBin = (integer_to_binary(IterationCount)),
- Hash2 = case Hash of
- sha -> <<>>;
- sha256 -> <<"sha256,">>;
- sha512 -> <<"sha512,">>
- end,
- <<"scram:", Hash2/binary, StoredKeyB64/binary, ",", ServerKeyB64/binary, ",", SaltB64/binary, ",", IterationCountBin/binary>>.
+ StoredKeyB64 = base64:encode(StoredKey),
+ ServerKeyB64 = base64:encode(ServerKey),
+ SaltB64 = base64:encode(Salt),
+ IterationCountBin = (integer_to_binary(IterationCount)),
+ MechanismB = case Hash of
+ sha -> <<"SCRAM-SHA-1">>;
+ sha256 -> <<"SCRAM-SHA-256">>;
+ sha512 -> <<"SCRAM-SHA-512">>
+ end,
+ Children =
+ [
+ #xmlel{name = <<"iter-count">>,
+ children = [{xmlcdata, IterationCountBin}]},
+ #xmlel{name = <<"salt">>,
+ children = [{xmlcdata, SaltB64}]},
+ #xmlel{name = <<"server-key">>,
+ children = [{xmlcdata, ServerKeyB64}]},
+ #xmlel{name = <<"stored-key">>,
+ children = [{xmlcdata, StoredKeyB64}]}
+ ],
+ #xmlel{name = <<"scram-credentials">>,
+ attrs = [{<<"xmlns">>, <<?NS_PIE/binary, "#scram">>},
+ {<<"mechanism">>, MechanismB}],
+ children = Children}.
+
+parse_scram_password(#xmlel{attrs = Attrs} = El) ->
+ Hash = case fxml:get_attr_s(<<"mechanism">>, Attrs) of
+ <<"SCRAM-SHA-1">> -> sha;
+ <<"SCRAM-SHA-256">> -> sha256;
+ <<"SCRAM-SHA-512">> -> sha512
+ end,
+ StoredKeyB64 = fxml:get_path_s(El, [{elem, <<"stored-key">>}, cdata]),
+ ServerKeyB64 = fxml:get_path_s(El, [{elem, <<"server-key">>}, cdata]),
+ IterationCountBin = fxml:get_path_s(El, [{elem, <<"iter-count">>}, cdata]),
+ SaltB64 = fxml:get_path_s(El, [{elem, <<"salt">>}, cdata]),
+ #scram{
+ storedkey = base64:decode(StoredKeyB64),
+ serverkey = base64:decode(ServerKeyB64),
+ salt = base64:decode(SaltB64),
+ hash = Hash,
+ iterationcount = (binary_to_integer(IterationCountBin))
+ };
parse_scram_password(PassData) ->
Split = binary:split(PassData, <<",">>, [global]),
@@ -214,26 +245,30 @@ parse_scram_password(PassData) ->
get_vcard(User, Server) ->
LUser = jid:nodeprep(User),
LServer = jid:nameprep(Server),
- case mod_vcard:get_vcard(LUser, LServer) of
+ try mod_vcard:get_vcard(LUser, LServer) of
error -> [];
Els -> Els
+ catch
+ error:{module_not_loaded, _, _} -> []
end.
-spec get_offline(binary(), binary()) -> [xmlel()].
get_offline(User, Server) ->
LUser = jid:nodeprep(User),
LServer = jid:nameprep(Server),
- case mod_offline:get_offline_els(LUser, LServer) of
+ try mod_offline:get_offline_els(LUser, LServer) of
[] ->
[];
Els ->
NewEls = lists:map(fun xmpp:encode/1, Els),
[#xmlel{name = <<"offline-messages">>, children = NewEls}]
+ catch
+ error:{module_not_loaded, _, _} -> []
end.
-spec get_privacy(binary(), binary()) -> [xmlel()].
get_privacy(User, Server) ->
- case mod_privacy:get_user_lists(User, Server) of
+ try mod_privacy:get_user_lists(User, Server) of
{ok, #privacy{default = Default,
lists = [_|_] = Lists}} ->
XLists = lists:map(
@@ -246,12 +281,14 @@ get_privacy(User, Server) ->
[xmpp:encode(#privacy_query{default = Default, lists = XLists})];
_ ->
[]
+ catch
+ error:{module_not_loaded, _, _} -> []
end.
-spec get_roster(binary(), binary()) -> [xmlel()].
get_roster(User, Server) ->
JID = jid:make(User, Server),
- case mod_roster:get_roster(User, Server) of
+ try mod_roster:get_roster(User, Server) of
[_|_] = Items ->
Subs =
lists:flatmap(
@@ -278,15 +315,19 @@ get_roster(User, Server) ->
[xmpp:encode(#roster_query{items = Rs}) | Subs];
_ ->
[]
+ catch
+ error:{module_not_loaded, _, _} -> []
end.
-spec get_private(binary(), binary()) -> [xmlel()].
get_private(User, Server) ->
- case mod_private:get_data(User, Server) of
+ try mod_private:get_data(User, Server) of
[_|_] = Els ->
[xmpp:encode(#private{sub_els = Els})];
_ ->
[]
+ catch
+ error:{module_not_loaded, _, _} -> []
end.
process(#state{xml_stream_state = XMLStreamState, fd = Fd} = State) ->
@@ -398,21 +439,10 @@ process_users([_|Els], State) ->
process_users([], State) ->
{ok, State}.
-process_user(#xmlel{name = <<"user">>, attrs = Attrs, children = Els},
+process_user(#xmlel{name = <<"user">>, attrs = Attrs, children = Els} = El,
#state{server = LServer} = State) ->
Name = fxml:get_attr_s(<<"name">>, Attrs),
- Password = fxml:get_attr_s(<<"password">>, Attrs),
- PasswordFormat = ejabberd_auth:password_format(LServer),
- Pass = case PasswordFormat of
- scram ->
- case Password of
- <<"scram:", PassData/binary>> ->
- parse_scram_password(PassData);
- P -> P
- end;
- _ -> Password
- end,
-
+ Pass = process_password(El, LServer),
case jid:nodeprep(Name) of
error ->
stop("Invalid 'user': ~ts", [Name]);
@@ -420,13 +450,29 @@ process_user(#xmlel{name = <<"user">>, attrs = Attrs, children = Els},
case ejabberd_auth:try_register(LUser, LServer, Pass) of
ok ->
process_user_els(Els, State#state{user = LUser});
- {error, invalid_password} when (Password == <<>>) ->
+ {error, invalid_password} when (Pass == <<>>) ->
process_user_els(Els, State#state{user = LUser});
{error, Err} ->
stop("Failed to create user '~ts': ~p", [Name, Err])
end
end.
+process_password(#xmlel{name = <<"user">>, attrs = Attrs} = El, LServer) ->
+ {PassPlain, PassOldScram} = case fxml:get_attr_s(<<"password">>, Attrs) of
+ <<"scram:", PassData/binary>> -> {<<"">>, PassData};
+ P -> {P, false}
+ end,
+ ScramCred = fxml:get_subtag(El, <<"scram-credentials">>),
+ PasswordFormat = ejabberd_auth:password_format(LServer),
+ case {PassPlain, PassOldScram, ScramCred, PasswordFormat} of
+ {PassPlain, false, false, plain} -> PassPlain;
+ {<<"">>, false, ScramCred, plain} -> parse_scram_password(ScramCred);
+ {<<"">>, PassOldScram, false, plain} -> parse_scram_password(PassOldScram);
+ {PassPlain, false, false, scram} -> PassPlain;
+ {<<"">>, false, ScramCred, scram} -> parse_scram_password(ScramCred);
+ {<<"">>, PassOldScram, false, scram} -> parse_scram_password(PassOldScram)
+ end.
+
process_user_els([#xmlel{} = El|Els], State) ->
case process_user_el(El, State) of
{ok, NewState} ->
diff --git a/src/ejabberd_s2s.erl b/src/ejabberd_s2s.erl
index 8057c9a35..04490071c 100644
--- a/src/ejabberd_s2s.erl
+++ b/src/ejabberd_s2s.erl
@@ -33,8 +33,8 @@
%% API
-export([start_link/0, stop/0, route/1, have_connection/1,
- get_connections_pids/1, try_register/1,
- remove_connection/2, start_connection/2, start_connection/3,
+ get_connections_pids/1,
+ start_connection/2, start_connection/3,
dirty_get_connections/0, allow_host/2,
incoming_s2s_number/0, outgoing_s2s_number/0,
stop_s2s_connections/0,
@@ -64,7 +64,7 @@
%% once a server is temporary blocked, it stay blocked for 60 seconds
--record(s2s, {fromto :: {binary(), binary()},
+-record(s2s, {fromto :: {binary(), binary()} | '_',
pid :: pid()}).
-record(state, {}).
@@ -112,24 +112,6 @@ is_temporarly_blocked(Host) ->
end
end.
--spec remove_connection({binary(), binary()}, pid()) -> ok.
-remove_connection({From, To} = FromTo, Pid) ->
- case mnesia:dirty_match_object(s2s, #s2s{fromto = FromTo, pid = Pid}) of
- [#s2s{pid = Pid}] ->
- F = fun() ->
- mnesia:delete_object(#s2s{fromto = FromTo, pid = Pid})
- end,
- case mnesia:transaction(F) of
- {atomic, _} -> ok;
- {aborted, Reason} ->
- ?ERROR_MSG("Failed to unregister s2s connection ~ts -> ~ts: "
- "Mnesia failure: ~p",
- [From, To, Reason])
- end;
- _ ->
- ok
- end.
-
-spec have_connection({binary(), binary()}) -> boolean().
have_connection(FromTo) ->
case catch mnesia:dirty_read(s2s, FromTo) of
@@ -148,31 +130,6 @@ get_connections_pids(FromTo) ->
[]
end.
--spec try_register({binary(), binary()}) -> boolean().
-try_register({From, To} = FromTo) ->
- MaxS2SConnectionsNumber = max_s2s_connections_number(FromTo),
- MaxS2SConnectionsNumberPerNode =
- max_s2s_connections_number_per_node(FromTo),
- F = fun () ->
- L = mnesia:read({s2s, FromTo}),
- NeededConnections = needed_connections_number(L,
- MaxS2SConnectionsNumber,
- MaxS2SConnectionsNumberPerNode),
- if NeededConnections > 0 ->
- mnesia:write(#s2s{fromto = FromTo, pid = self()}),
- true;
- true -> false
- end
- end,
- case mnesia:transaction(F) of
- {atomic, Res} -> Res;
- {aborted, Reason} ->
- ?ERROR_MSG("Failed to register s2s connection ~ts -> ~ts: "
- "Mnesia failure: ~p",
- [From, To, Reason]),
- false
- end.
-
-spec dirty_get_connections() -> [{binary(), binary()}].
dirty_get_connections() ->
mnesia:dirty_all_keys(s2s).
@@ -269,6 +226,8 @@ init([]) ->
{stop, Reason}
end.
+handle_call({new_connection, Args}, _From, State) ->
+ {reply, erlang:apply(fun new_connection_int/7, Args), State};
handle_call(Request, From, State) ->
?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
{noreply, State}.
@@ -289,6 +248,21 @@ handle_info({route, Packet}, State) ->
misc:format_exception(2, Class, Reason, StackTrace)])
end,
{noreply, State};
+handle_info({'DOWN', _Ref, process, Pid, _Reason}, State) ->
+ case mnesia:dirty_match_object(s2s, #s2s{fromto = '_', pid = Pid}) of
+ [#s2s{pid = Pid, fromto = {From, To}} = Obj] ->
+ F = fun() -> mnesia:delete_object(Obj) end,
+ case mnesia:transaction(F) of
+ {atomic, _} -> ok;
+ {aborted, Reason} ->
+ ?ERROR_MSG("Failed to unregister s2s connection for pid ~p (~ts -> ~ts):"
+ "Mnesia failure: ~p",
+ [Pid, From, To, Reason])
+ end,
+ {noreply, State};
+ _ ->
+ {noreply, State}
+ end;
handle_info(Info, State) ->
?WARNING_MSG("Unexpected info: ~p", [Info]),
{noreply, State}.
@@ -458,6 +432,18 @@ open_several_connections(N, MyServer, Server, From,
integer(), integer(), [proplists:property()]) -> [pid()].
new_connection(MyServer, Server, From, FromTo,
MaxS2SConnectionsNumber, MaxS2SConnectionsNumberPerNode, Opts) ->
+ case whereis(ejabberd_s2s) == self() of
+ true ->
+ new_connection_int(MyServer, Server, From, FromTo,
+ MaxS2SConnectionsNumber, MaxS2SConnectionsNumberPerNode, Opts);
+ false ->
+ gen_server:call(ejabberd_s2s, {new_connection, [MyServer, Server, From, FromTo,
+ MaxS2SConnectionsNumber,
+ MaxS2SConnectionsNumberPerNode, Opts]})
+ end.
+
+new_connection_int(MyServer, Server, From, FromTo,
+ MaxS2SConnectionsNumber, MaxS2SConnectionsNumberPerNode, Opts) ->
{ok, Pid} = ejabberd_s2s_out:start(MyServer, Server, Opts),
F = fun() ->
L = mnesia:read({s2s, FromTo}),
@@ -474,6 +460,7 @@ new_connection(MyServer, Server, From, FromTo,
case TRes of
{atomic, Pid1} ->
if Pid1 == Pid ->
+ erlang:monitor(process, Pid),
ejabberd_s2s_out:connect(Pid);
true ->
ejabberd_s2s_out:stop_async(Pid)
diff --git a/src/ejabberd_s2s_out.erl b/src/ejabberd_s2s_out.erl
index d58396533..f057705ed 100644
--- a/src/ejabberd_s2s_out.erl
+++ b/src/ejabberd_s2s_out.erl
@@ -318,7 +318,6 @@ handle_info(Info, #{server_host := ServerHost} = State) ->
terminate(Reason, #{server := LServer,
remote_server := RServer} = State) ->
- ejabberd_s2s:remove_connection({LServer, RServer}, self()),
State1 = case Reason of
normal -> State;
_ -> State#{stop_reason => internal_failure}
@@ -351,21 +350,12 @@ bounce_queue(State) ->
end, State).
-spec bounce_message_queue({binary(), binary()}, state()) -> state().
-bounce_message_queue({LServer, RServer} = FromTo, State) ->
- Pids = ejabberd_s2s:get_connections_pids(FromTo),
- case lists:member(self(), Pids) of
- true ->
- ?WARNING_MSG("Outgoing s2s connection ~ts -> ~ts is supposed "
- "to be unregistered, but pid ~p still presents "
- "in 's2s' table", [LServer, RServer, self()]),
- State;
- false ->
- receive {route, Pkt} ->
- State1 = bounce_packet(Pkt, State),
- bounce_message_queue(FromTo, State1)
- after 0 ->
- State
- end
+bounce_message_queue(FromTo, State) ->
+ receive {route, Pkt} ->
+ State1 = bounce_packet(Pkt, State),
+ bounce_message_queue(FromTo, State1)
+ after 0 ->
+ State
end.
-spec bounce_packet(xmpp_element(), state()) -> state().
diff --git a/src/ejabberd_sql_pt.erl b/src/ejabberd_sql_pt.erl
index 130228fe1..d131570f7 100644
--- a/src/ejabberd_sql_pt.erl
+++ b/src/ejabberd_sql_pt.erl
@@ -564,15 +564,23 @@ make_sql_upsert(Table, ParseRes, Pos) ->
[]
end,
erl_syntax:fun_expr(
- [erl_syntax:clause(
- [erl_syntax:atom(pgsql), erl_syntax:variable("__Version")],
- [erl_syntax:infix_expr(
- erl_syntax:variable("__Version"),
- erl_syntax:operator('>='),
- erl_syntax:integer(90100))],
- [make_sql_upsert_pgsql901(Table, ParseRes),
- erl_syntax:atom(ok)])] ++
- MySqlReplace ++
+ [erl_syntax:clause(
+ [erl_syntax:atom(pgsql), erl_syntax:variable("__Version")],
+ [erl_syntax:infix_expr(
+ erl_syntax:variable("__Version"),
+ erl_syntax:operator('>='),
+ erl_syntax:integer(90500))],
+ [make_sql_upsert_pgsql905(Table, ParseRes),
+ erl_syntax:atom(ok)]),
+ erl_syntax:clause(
+ [erl_syntax:atom(pgsql), erl_syntax:variable("__Version")],
+ [erl_syntax:infix_expr(
+ erl_syntax:variable("__Version"),
+ erl_syntax:operator('>='),
+ erl_syntax:integer(90100))],
+ [make_sql_upsert_pgsql901(Table, ParseRes),
+ erl_syntax:atom(ok)])] ++
+ MySqlReplace ++
[erl_syntax:clause(
[erl_syntax:underscore(), erl_syntax:underscore()],
none,
@@ -713,6 +721,57 @@ make_sql_upsert_pgsql901(Table, ParseRes0) ->
erl_syntax:atom(sql_query_t),
[Upsert]).
+make_sql_upsert_pgsql905(Table, ParseRes0) ->
+ ParseRes = lists:map(
+ fun({"family", A2, A3}) -> {"\"family\"", A2, A3};
+ (Other) -> Other
+ end, ParseRes0),
+ Vals =
+ lists:map(
+ fun({_Field, _, ST}) ->
+ ST
+ end, ParseRes),
+ Fields =
+ lists:map(
+ fun({Field, _, _ST}) ->
+ #state{'query' = [{str, Field}]}
+ end, ParseRes),
+ SPairs =
+ lists:flatmap(
+ fun({_Field, key, _ST}) ->
+ [];
+ ({_Field, {false}, _ST}) ->
+ [];
+ ({Field, {true}, ST}) ->
+ [ST#state{
+ 'query' = [{str, Field}, {str, "="}] ++ ST#state.'query'
+ }]
+ end, ParseRes),
+ Set = join_states(SPairs, ", "),
+ KeyFields =
+ lists:flatmap(
+ fun({Field, key, _ST}) ->
+ [#state{'query' = [{str, Field}]}];
+ ({_Field, _, _ST}) ->
+ []
+ end, ParseRes),
+ State =
+ concat_states(
+ [#state{'query' = [{str, "INSERT INTO "}, {str, Table}, {str, "("}]},
+ join_states(Fields, ", "),
+ #state{'query' = [{str, ") VALUES ("}]},
+ join_states(Vals, ", "),
+ #state{'query' = [{str, ") ON CONFLICT ("}]},
+ join_states(KeyFields, ", "),
+ #state{'query' = [{str, ") DO UPDATE SET "}]},
+ Set
+ ]),
+ Upsert = make_sql_query(State),
+ erl_syntax:application(
+ erl_syntax:atom(ejabberd_sql),
+ erl_syntax:atom(sql_query_t),
+ [Upsert]).
+
check_upsert(ParseRes, Pos) ->
Set =
diff --git a/src/ejabberd_web_admin.erl b/src/ejabberd_web_admin.erl
index 7dc11b571..ee50a0031 100644
--- a/src/ejabberd_web_admin.erl
+++ b/src/ejabberd_web_admin.erl
@@ -602,12 +602,14 @@ list_vhosts2(Lang, Hosts) ->
[?AC(<<"../server/", Host/binary,
"/">>,
Host)]),
- ?XAC(<<"td">>,
+ ?XAE(<<"td">>,
[{<<"class">>, <<"alignright">>}],
- (pretty_string_int(RegisteredUsers))),
- ?XAC(<<"td">>,
+ [?AC(<<"../server/", Host/binary, "/users/">>,
+ pretty_string_int(RegisteredUsers))]),
+ ?XAE(<<"td">>,
[{<<"class">>, <<"alignright">>}],
- (pretty_string_int(OnlineUsers)))])
+ [?AC(<<"../server/", Host/binary, "/online-users/">>,
+ pretty_string_int(OnlineUsers))])])
end,
SHosts)))])].
diff --git a/src/ejd2sql.erl b/src/ejd2sql.erl
index ad0cc5e88..427e13087 100644
--- a/src/ejd2sql.erl
+++ b/src/ejd2sql.erl
@@ -73,11 +73,16 @@ export(Server, Output) ->
end, Modules),
close_output(Output, IO).
-export(Server, Output, Module1) ->
- Module = case Module1 of
- mod_pubsub -> pubsub_db;
- _ -> Module1
- end,
+export(Server, Output, mod_mam = M1) ->
+ MucServices = gen_mod:get_module_opt_hosts(Server, mod_muc),
+ [export2(MucService, Output, M1, M1) || MucService <- MucServices],
+ export2(Server, Output, M1, M1);
+export(Server, Output, mod_pubsub = M1) ->
+ export2(Server, Output, M1, pubsub_db);
+export(Server, Output, M1) ->
+ export2(Server, Output, M1, M1).
+
+export2(Server, Output, Module1, Module) ->
SQLMod = gen_mod:db_mod(sql, Module),
LServer = jid:nameprep(iolist_to_binary(Server)),
IO = prepare_output(Output),
diff --git a/src/ext_mod.erl b/src/ext_mod.erl
index 5353019bb..b3e88a9cb 100644
--- a/src/ext_mod.erl
+++ b/src/ext_mod.erl
@@ -84,14 +84,14 @@ code_change(_OldVsn, State, _Extra) ->
%% -- ejabberd commands
get_commands_spec() ->
[#ejabberd_commands{name = modules_update_specs,
- tags = [admin,modules],
+ tags = [modules],
desc = "Update the module source code from Git",
longdesc = "A connection to Internet is required",
module = ?MODULE, function = update,
args = [],
result = {res, rescode}},
#ejabberd_commands{name = modules_available,
- tags = [admin,modules],
+ tags = [modules],
desc = "List the contributed modules available to install",
module = ?MODULE, function = available_command,
result_desc = "List of tuples with module name and description",
@@ -103,7 +103,7 @@ get_commands_spec() ->
[{name, atom},
{summary, string}]}}}}},
#ejabberd_commands{name = modules_installed,
- tags = [admin,modules],
+ tags = [modules],
desc = "List the contributed modules already installed",
module = ?MODULE, function = installed_command,
result_desc = "List of tuples with module name and description",
@@ -115,7 +115,7 @@ get_commands_spec() ->
[{name, atom},
{summary, string}]}}}}},
#ejabberd_commands{name = module_install,
- tags = [admin,modules],
+ tags = [modules],
desc = "Compile, install and start an available contributed module",
module = ?MODULE, function = install,
args_desc = ["Module name"],
@@ -123,7 +123,7 @@ get_commands_spec() ->
args = [{module, binary}],
result = {res, rescode}},
#ejabberd_commands{name = module_uninstall,
- tags = [admin,modules],
+ tags = [modules],
desc = "Uninstall a contributed module",
module = ?MODULE, function = uninstall,
args_desc = ["Module name"],
@@ -131,7 +131,7 @@ get_commands_spec() ->
args = [{module, binary}],
result = {res, rescode}},
#ejabberd_commands{name = module_upgrade,
- tags = [admin,modules],
+ tags = [modules],
desc = "Upgrade the running code of an installed module",
longdesc = "In practice, this uninstalls and installs the module",
module = ?MODULE, function = upgrade,
@@ -140,7 +140,7 @@ get_commands_spec() ->
args = [{module, binary}],
result = {res, rescode}},
#ejabberd_commands{name = module_check,
- tags = [admin,modules],
+ tags = [modules],
desc = "Check the contributed module repository compliance",
module = ?MODULE, function = check,
args_desc = ["Module name"],
diff --git a/src/gen_pubsub_node.erl b/src/gen_pubsub_node.erl
index 5bdebdfc6..3f83fe48f 100644
--- a/src/gen_pubsub_node.erl
+++ b/src/gen_pubsub_node.erl
@@ -123,11 +123,20 @@
{error, stanza_error()}.
-callback remove_extra_items(NodeIdx :: nodeIdx(),
+ Max_Items :: unlimited | non_neg_integer()) ->
+ {result, {[itemId()], [itemId()]}
+ }.
+
+-callback remove_extra_items(NodeIdx :: nodeIdx(),
Max_Items :: unlimited | non_neg_integer(),
ItemIds :: [itemId()]) ->
{result, {[itemId()], [itemId()]}
}.
+-callback remove_expired_items(NodeIdx :: nodeIdx(),
+ Seconds :: infinity | non_neg_integer()) ->
+ {result, [itemId()]}.
+
-callback get_node_affiliations(NodeIdx :: nodeIdx()) ->
{result, [{ljid(), affiliation()}]}.
diff --git a/src/gen_pubsub_nodetree.erl b/src/gen_pubsub_nodetree.erl
index 5a24db2c4..b6b73b8cb 100644
--- a/src/gen_pubsub_nodetree.erl
+++ b/src/gen_pubsub_nodetree.erl
@@ -67,6 +67,9 @@
-callback get_nodes(Host :: host())->
[pubsubNode()].
+-callback get_all_nodes(Host :: host()) ->
+ [pubsubNode()].
+
-callback get_parentnodes(Host :: host(),
NodeId :: nodeId(),
From :: jid:jid()) ->
diff --git a/src/mod_admin_extra.erl b/src/mod_admin_extra.erl
index 04cb39cfb..9834acf01 100644
--- a/src/mod_admin_extra.erl
+++ b/src/mod_admin_extra.erl
@@ -92,7 +92,7 @@
%%%
start(_Host, _Opts) ->
- ejabberd_commands:register_commands(get_commands_spec()).
+ ejabberd_commands:register_commands(?MODULE, get_commands_spec()).
stop(Host) ->
case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of
@@ -1652,14 +1652,6 @@ mod_doc() ->
?T("If you want to put a group Name with blankspaces, use the "
"characters \"\' and \'\" to define when the Name starts and "
"ends. See an example below.")],
- opts =>
- [{module_resource,
- #{value => ?T("Resource"),
- desc =>
- ?T("Indicate the resource that the XMPP stanzas must use "
- "in the FROM or TO JIDs. This is only useful in the "
- "'get_vcard*' and 'set_vcard*' commands. The default "
- "value is 'mod_admin_extra'.")}}],
example =>
[{?T("With this configuration, vCards can only be modified with "
"mod_admin_extra commands:"),
@@ -1670,8 +1662,7 @@ mod_doc() ->
" vcard_set:",
" - allow: adminextraresource",
"modules:",
- " mod_admin_extra:",
- " module_resource: \"modadminextraf8x,31ad\"",
+ " mod_admin_extra: {}",
" mod_vcard:",
" access_set: vcard_set"]},
{?T("Content of roster file for 'pushroster' command:"),
diff --git a/src/mod_admin_update_sql.erl b/src/mod_admin_update_sql.erl
index 0215a5172..02beb4bf8 100644
--- a/src/mod_admin_update_sql.erl
+++ b/src/mod_admin_update_sql.erl
@@ -46,7 +46,7 @@
%%%
start(_Host, _Opts) ->
- ejabberd_commands:register_commands(get_commands_spec()).
+ ejabberd_commands:register_commands(?MODULE, get_commands_spec()).
stop(_Host) ->
ejabberd_commands:unregister_commands(get_commands_spec()).
@@ -140,6 +140,7 @@ update_tables(State) ->
add_sh_column(State, "sr_group"),
add_pkey(State, "sr_group", ["server_host", "name"]),
+ create_unique_index(State, "sr_group", "i_sr_group_sh_name", ["server_host", "name"]),
drop_sh_default(State, "sr_group"),
add_sh_column(State, "sr_user"),
@@ -147,6 +148,7 @@ update_tables(State) ->
drop_index(State, "i_sr_user_jid"),
drop_index(State, "i_sr_user_grp"),
add_pkey(State, "sr_user", ["server_host", "jid", "grp"]),
+ create_unique_index(State, "sr_user", "i_sr_user_sh_jid_grp", ["server_host", "jid", "grp"]),
create_index(State, "sr_user", "i_sr_user_sh_jid", ["server_host", "jid"]),
create_index(State, "sr_user", "i_sr_user_sh_grp", ["server_host", "grp"]),
drop_sh_default(State, "sr_user"),
@@ -364,6 +366,6 @@ mod_doc() ->
#{desc =>
?T("This module can be used to update existing SQL database "
"from the default to the new schema. Check the section "
- "http://../database-ldap/#default-and-new-schemas[Default and New Schemas] for details. "
+ "http://../database/#default-and-new-schemas[Default and New Schemas] for details. "
"Please note that only PostgreSQL is supported. "
- "When the module is loaded use 'update_sql' ejabberdctl command.")}.
+ "When the module is loaded use _`update_sql`_ API.")}.
diff --git a/src/mod_announce.erl b/src/mod_announce.erl
index e0501d96b..21213615c 100644
--- a/src/mod_announce.erl
+++ b/src/mod_announce.erl
@@ -930,7 +930,7 @@ mod_doc() ->
"should be disabled for instances of ejabberd with hundreds of "
"thousands users."), "",
?T("The Ad-hoc Commands are listed in the Server Discovery. "
- "For this feature to work, 'mod_adhoc' must be enabled."), "",
+ "For this feature to work, _`mod_adhoc`_ must be enabled."), "",
?T("The specific JIDs where messages can be sent are listed below. "
"The first JID in each entry will apply only to the specified "
"virtual host example.org, while the JID between brackets "
@@ -940,7 +940,7 @@ mod_doc() ->
"online and connected to several resources, only the resource "
"with the highest priority will receive the message. "
"If the registered user is not connected, the message will be "
- "stored offline in assumption that offline storage (see 'mod_offline') "
+ "stored offline in assumption that offline storage (see _`mod_offline`_) "
"is enabled."),
"- example.org/announce/online (example.org/announce/all-hosts/online)::",
?T("The message is sent to all connected users. If the user is "
@@ -965,20 +965,20 @@ mod_doc() ->
{db_type,
#{value => "mnesia | sql",
desc =>
- ?T("Same as top-level 'default_db' option, but applied to this module only.")}},
+ ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}},
{use_cache,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'use_cache' option, but applied to this module only.")}},
+ ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}},
{cache_size,
#{value => "pos_integer() | infinity",
desc =>
- ?T("Same as top-level 'cache_size' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}},
{cache_missed,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'cache_missed' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}},
{cache_life_time,
#{value => "timeout()",
desc =>
- ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}}]}.
+ ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}.
diff --git a/src/mod_avatar.erl b/src/mod_avatar.erl
index 3ad55f6d0..09329853d 100644
--- a/src/mod_avatar.erl
+++ b/src/mod_avatar.erl
@@ -469,8 +469,8 @@ mod_doc() ->
"[XEP-0398: User Avatar to vCard-Based Avatars Conversion]."), "",
?T("Also, the module supports conversion between avatar "
"image formats on the fly."), "",
- ?T("The module depends on 'mod_vcard', 'mod_vcard_xupdate' and "
- "'mod_pubsub'.")],
+ ?T("The module depends on _`mod_vcard`_, _`mod_vcard_xupdate`_ and "
+ "_`mod_pubsub`_.")],
opts =>
[{convert,
#{value => "{From: To}",
diff --git a/src/mod_bosh.erl b/src/mod_bosh.erl
index 1770d27a7..37a7fa809 100644
--- a/src/mod_bosh.erl
+++ b/src/mod_bosh.erl
@@ -240,27 +240,27 @@ mod_doc() ->
{queue_type,
#{value => "ram | file",
desc =>
- ?T("Same as top-level 'queue_type' option, but applied to this module only.")}},
+ ?T("Same as top-level _`queue_type`_ option, but applied to this module only.")}},
{ram_db_type,
#{value => "mnesia | sql | redis",
desc =>
- ?T("Same as 'default_ram_db' but applied to this module only.")}},
+ ?T("Same as _`default_ram_db`_ but applied to this module only.")}},
{use_cache,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'use_cache' option, but applied to this module only.")}},
+ ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}},
{cache_size,
#{value => "pos_integer() | infinity",
desc =>
- ?T("Same as top-level 'cache_size' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}},
{cache_missed,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'cache_missed' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}},
{cache_life_time,
#{value => "timeout()",
desc =>
- ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}}],
+ ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}],
example =>
["listen:",
" -",
diff --git a/src/mod_caps.erl b/src/mod_caps.erl
index 92869fc7d..bc48dac6f 100644
--- a/src/mod_caps.erl
+++ b/src/mod_caps.erl
@@ -49,7 +49,8 @@
handle_cast/2, terminate/2, code_change/3]).
-export([user_send_packet/1, user_receive_packet/1,
- c2s_presence_in/2, mod_opt_type/1, mod_options/1, mod_doc/0]).
+ c2s_presence_in/2, c2s_copy_session/2,
+ mod_opt_type/1, mod_options/1, mod_doc/0]).
-include("logger.hrl").
@@ -274,6 +275,13 @@ c2s_presence_in(C2SState,
C2SState#{caps_resources => NewRs}
end.
+-spec c2s_copy_session(ejabberd_c2s:state(), ejabberd_c2s:state())
+ -> ejabberd_c2s:state().
+c2s_copy_session(C2SState, #{caps_resources := Rs}) ->
+ C2SState#{caps_resources => Rs};
+c2s_copy_session(C2SState, _) ->
+ C2SState.
+
-spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}].
depends(_Host, _Opts) ->
[].
@@ -304,6 +312,8 @@ init([Host|_]) ->
caps_stream_features, 75),
ejabberd_hooks:add(s2s_in_post_auth_features, Host, ?MODULE,
caps_stream_features, 75),
+ ejabberd_hooks:add(c2s_copy_session, Host, ?MODULE,
+ c2s_copy_session, 75),
ejabberd_hooks:add(disco_local_features, Host, ?MODULE,
disco_features, 75),
ejabberd_hooks:add(disco_local_identity, Host, ?MODULE,
@@ -341,6 +351,8 @@ terminate(_Reason, State) ->
?MODULE, caps_stream_features, 75),
ejabberd_hooks:delete(s2s_in_post_auth_features, Host,
?MODULE, caps_stream_features, 75),
+ ejabberd_hooks:delete(c2s_copy_session, Host, ?MODULE,
+ c2s_copy_session, 75),
ejabberd_hooks:delete(disco_local_features, Host,
?MODULE, disco_features, 75),
ejabberd_hooks:delete(disco_local_identity, Host,
@@ -595,25 +607,25 @@ mod_doc() ->
"https://xmpp.org/extensions/xep-0115.html"
"[XEP-0115: Entity Capabilities]."),
?T("The main purpose of the module is to provide "
- "PEP functionality (see 'mod_pubsub').")],
+ "PEP functionality (see _`mod_pubsub`_).")],
opts =>
[{db_type,
#{value => "mnesia | sql",
desc =>
- ?T("Same as top-level 'default_db' option, but applied to this module only.")}},
+ ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}},
{use_cache,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'use_cache' option, but applied to this module only.")}},
+ ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}},
{cache_size,
#{value => "pos_integer() | infinity",
desc =>
- ?T("Same as top-level 'cache_size' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}},
{cache_missed,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'cache_missed' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}},
{cache_life_time,
#{value => "timeout()",
desc =>
- ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}}]}.
+ ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}.
diff --git a/src/mod_configure.erl b/src/mod_configure.erl
index 99a20ef3a..32c7ebb31 100644
--- a/src/mod_configure.erl
+++ b/src/mod_configure.erl
@@ -1564,4 +1564,4 @@ mod_doc() ->
?T("The module provides server configuration functionality via "
"https://xmpp.org/extensions/xep-0050.html"
"[XEP-0050: Ad-Hoc Commands]. This module requires "
- "'mod_adhoc' to be loaded.")}.
+ "_`mod_adhoc`_ to be loaded.")}.
diff --git a/src/mod_conversejs.erl b/src/mod_conversejs.erl
new file mode 100644
index 000000000..0b5d57a15
--- /dev/null
+++ b/src/mod_conversejs.erl
@@ -0,0 +1,156 @@
+%%%----------------------------------------------------------------------
+%%% File : mod_conversejs.erl
+%%% Author : Alexey Shchepin <alexey@process-one.net>
+%%% Purpose : Serve simple page for Converse.js XMPP web browser client
+%%% Created : 8 Nov 2021 by Alexey Shchepin <alexey@process-one.net>
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2002-2021 ProcessOne
+%%%
+%%% This program is free software; you can redistribute it and/or
+%%% modify it under the terms of the GNU General Public License as
+%%% published by the Free Software Foundation; either version 2 of the
+%%% License, or (at your option) any later version.
+%%%
+%%% This program is distributed in the hope that it will be useful,
+%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
+%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%%% General Public License for more details.
+%%%
+%%% You should have received a copy of the GNU General Public License along
+%%% with this program; if not, write to the Free Software Foundation, Inc.,
+%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+%%%
+%%%----------------------------------------------------------------------
+
+-module(mod_conversejs).
+
+-author('alexey@process-one.net').
+
+-behaviour(gen_mod).
+
+-export([start/2, stop/1, reload/3, process/2, depends/2,
+ mod_opt_type/1, mod_options/1, mod_doc/0]).
+
+-include_lib("xmpp/include/xmpp.hrl").
+-include("logger.hrl").
+-include("ejabberd_http.hrl").
+-include("translate.hrl").
+-include("ejabberd_web_admin.hrl").
+
+start(_Host, _Opts) ->
+ ok.
+
+stop(_Host) ->
+ ok.
+
+reload(_Host, _NewOpts, _OldOpts) ->
+ ok.
+
+depends(_Host, _Opts) ->
+ [].
+
+process([], #request{method = 'GET'}) ->
+ Host = ejabberd_config:get_myname(),
+ Domain = gen_mod:get_module_opt(Host, ?MODULE, default_domain),
+ Script = gen_mod:get_module_opt(Host, ?MODULE, conversejs_script),
+ CSS = gen_mod:get_module_opt(Host, ?MODULE, conversejs_css),
+ Init = [{<<"discover_connection_methods">>, false},
+ {<<"jid">>, Domain},
+ {<<"default_domain">>, Domain},
+ {<<"domain_placeholder">>, Domain},
+ {<<"view_mode">>, <<"fullscreen">>}],
+ Init2 =
+ case gen_mod:get_module_opt(Host, ?MODULE, websocket_url) of
+ undefined -> Init;
+ WSURL -> [{<<"websocket_url">>, WSURL} | Init]
+ end,
+ Init3 =
+ case gen_mod:get_module_opt(Host, ?MODULE, bosh_service_url) of
+ undefined -> Init2;
+ BoshURL -> [{<<"bosh_service_url">>, BoshURL} | Init2]
+ end,
+ {200, [html],
+ [<<"<!DOCTYPE html>">>,
+ <<"<html>">>,
+ <<"<head>">>,
+ <<"<meta charset='utf-8'>">>,
+ <<"<link rel='stylesheet' type='text/css' media='screen' href='">>,
+ fxml:crypt(CSS), <<"'>">>,
+ <<"<script src='">>, fxml:crypt(Script), <<"' charset='utf-8'></script>">>,
+ <<"</head>">>,
+ <<"<body>">>,
+ <<"<script>">>,
+ <<"converse.initialize(">>, jiffy:encode({Init3}), <<");">>,
+ <<"</script>">>,
+ <<"</body>">>,
+ <<"</html>">>]};
+process(_, _) ->
+ ejabberd_web:error(not_found).
+
+mod_opt_type(bosh_service_url) ->
+ econf:either(undefined, econf:binary());
+mod_opt_type(websocket_url) ->
+ econf:either(undefined, econf:binary());
+mod_opt_type(conversejs_script) ->
+ econf:binary();
+mod_opt_type(conversejs_css) ->
+ econf:binary();
+mod_opt_type(default_domain) ->
+ econf:binary().
+
+mod_options(_) ->
+ [{bosh_service_url, undefined},
+ {websocket_url, undefined},
+ {default_domain, ejabberd_config:get_myname()},
+ {conversejs_script, <<"https://cdn.conversejs.org/dist/converse.min.js">>},
+ {conversejs_css, <<"https://cdn.conversejs.org/dist/converse.min.css">>}].
+
+mod_doc() ->
+ #{desc =>
+ [?T("This module serves a simple page for the "
+ "https://conversejs.org/[Converse] XMPP web browser client."), "",
+ ?T("To use this module, in addition to adding it to the 'modules' "
+ "section, you must also enable it in 'listen' -> 'ejabberd_http' -> "
+ "http://../listen-options/#request-handlers[request_handlers]."), "",
+ ?T("You must also setup either the option 'websocket_url' or 'bosh_service_url'."), "",
+ ?T("By default, the options 'conversejs_css' and 'conversejs_script'"
+ " point to the public Converse.js client. Alternatively, you can"
+ " host the client locally using _`mod_http_fileserver`_.")
+ ],
+ example =>
+ ["listen:",
+ " -",
+ " port: 5280",
+ " module: ejabberd_http",
+ " request_handlers:",
+ " /websocket: ejabberd_http_ws",
+ " /conversejs: mod_conversejs",
+ "",
+ "modules:",
+ " mod_conversejs:",
+ " websocket_url: \"ws://example.org:5280/websocket\""],
+ opts =>
+ [{websocket_url,
+ #{value => ?T("WebsocketURL"),
+ desc =>
+ ?T("A websocket URL to which Converse.js can connect to.")}},
+ {bosh_service_url,
+ #{value => ?T("BoshURL"),
+ desc =>
+ ?T("BOSH service URL to which Converse.js can connect to.")}},
+ {default_domain,
+ #{value => ?T("Domain"),
+ desc =>
+ ?T("Specify a domain to act as the default for user JIDs. "
+ "The default value is the first domain defined in the "
+ "ejabberd configuration file.")}},
+ {conversejs_script,
+ #{value => ?T("URL"),
+ desc =>
+ ?T("Converse.js main script URL.")}},
+ {conversejs_css,
+ #{value => ?T("URL"),
+ desc =>
+ ?T("Converse.js CSS URL.")}}]
+ }.
diff --git a/src/mod_conversejs_opt.erl b/src/mod_conversejs_opt.erl
new file mode 100644
index 000000000..9e53978ea
--- /dev/null
+++ b/src/mod_conversejs_opt.erl
@@ -0,0 +1,41 @@
+%% Generated automatically
+%% DO NOT EDIT: run `make options` instead
+
+-module(mod_conversejs_opt).
+
+-export([bosh_service_url/1]).
+-export([conversejs_css/1]).
+-export([conversejs_script/1]).
+-export([default_domain/1]).
+-export([websocket_url/1]).
+
+-spec bosh_service_url(gen_mod:opts() | global | binary()) -> 'undefined' | binary().
+bosh_service_url(Opts) when is_map(Opts) ->
+ gen_mod:get_opt(bosh_service_url, Opts);
+bosh_service_url(Host) ->
+ gen_mod:get_module_opt(Host, mod_conversejs, bosh_service_url).
+
+-spec conversejs_css(gen_mod:opts() | global | binary()) -> binary().
+conversejs_css(Opts) when is_map(Opts) ->
+ gen_mod:get_opt(conversejs_css, Opts);
+conversejs_css(Host) ->
+ gen_mod:get_module_opt(Host, mod_conversejs, conversejs_css).
+
+-spec conversejs_script(gen_mod:opts() | global | binary()) -> binary().
+conversejs_script(Opts) when is_map(Opts) ->
+ gen_mod:get_opt(conversejs_script, Opts);
+conversejs_script(Host) ->
+ gen_mod:get_module_opt(Host, mod_conversejs, conversejs_script).
+
+-spec default_domain(gen_mod:opts() | global | binary()) -> binary().
+default_domain(Opts) when is_map(Opts) ->
+ gen_mod:get_opt(default_domain, Opts);
+default_domain(Host) ->
+ gen_mod:get_module_opt(Host, mod_conversejs, default_domain).
+
+-spec websocket_url(gen_mod:opts() | global | binary()) -> 'undefined' | binary().
+websocket_url(Opts) when is_map(Opts) ->
+ gen_mod:get_opt(websocket_url, Opts);
+websocket_url(Host) ->
+ gen_mod:get_module_opt(Host, mod_conversejs, websocket_url).
+
diff --git a/src/mod_delegation.erl b/src/mod_delegation.erl
index a588e81ef..3ae3b8a51 100644
--- a/src/mod_delegation.erl
+++ b/src/mod_delegation.erl
@@ -95,7 +95,7 @@ mod_doc() ->
?T("WARNING: Security issue: Namespace delegation gives components "
"access to sensitive data, so permission should be granted "
"carefully, only if you trust the component."), "",
- ?T("NOTE: This module is complementary to 'mod_privilege' but can "
+ ?T("NOTE: This module is complementary to _`mod_privilege`_ but can "
"also be used separately.")],
opts =>
[{namespaces,
diff --git a/src/mod_disco.erl b/src/mod_disco.erl
index cf888bc4e..deb9d15a2 100644
--- a/src/mod_disco.erl
+++ b/src/mod_disco.erl
@@ -464,11 +464,11 @@ mod_doc() ->
" -",
" modules: all",
" name: abuse-addresses",
- " urls: [mailto:abuse@shakespeare.lit]",
+ " urls: [\"mailto:abuse@shakespeare.lit\"]",
" -",
" modules: [mod_muc]",
" name: \"Web chatroom logs\"",
- " urls: [http://www.example.org/muc-logs]",
+ " urls: [\"http://www.example.org/muc-logs\"]",
" -",
" modules: [mod_disco]",
" name: feedback-addresses",
diff --git a/src/mod_fail2ban.erl b/src/mod_fail2ban.erl
index 99ff3d127..8751653d8 100644
--- a/src/mod_fail2ban.erl
+++ b/src/mod_fail2ban.erl
@@ -107,7 +107,7 @@ c2s_stream_started(#{ip := {Addr, _}} = State, _) ->
start(Host, Opts) ->
catch ets:new(failed_auth, [named_table, public,
{heir, erlang:group_leader(), none}]),
- ejabberd_commands:register_commands(get_commands_spec()),
+ ejabberd_commands:register_commands(?MODULE, get_commands_spec()),
gen_mod:start_child(?MODULE, Host, Opts).
stop(Host) ->
diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl
index 0a3942024..427833584 100644
--- a/src/mod_http_api.erl
+++ b/src/mod_http_api.erl
@@ -527,10 +527,20 @@ mod_doc() ->
[?T("This module provides a ReST API to call ejabberd commands "
"using JSON data."), "",
?T("To use this module, in addition to adding it to the 'modules' "
- "section, you must also add it to 'request_handlers' of some "
- "listener."), "",
+ "section, you must also enable it in 'listen' -> 'ejabberd_http' -> "
+ "http://../listen-options/#request-handlers[request_handlers]."), "",
?T("To use a specific API version N, when defining the URL path "
"in the request_handlers, add a 'vN'. "
"For example: '/api/v2: mod_http_api'"), "",
?T("To run a command, send a POST request to the corresponding "
- "URL: 'http://localhost:5280/api/<command_name>'")]}.
+ "URL: 'http://localhost:5280/api/<command_name>'")],
+ example =>
+ ["listen:",
+ " -",
+ " port: 5280",
+ " module: ejabberd_http",
+ " request_handlers:",
+ " /api: mod_http_api",
+ "",
+ "modules:",
+ " mod_http_api: {}"]}.
diff --git a/src/mod_http_upload.erl b/src/mod_http_upload.erl
index 0cc8d8488..1ff94eb4a 100644
--- a/src/mod_http_upload.erl
+++ b/src/mod_http_upload.erl
@@ -232,8 +232,9 @@ mod_doc() ->
"[XEP-0363: HTTP File Upload]. If the request is accepted, "
"the client receives a URL for uploading the file and "
"another URL from which that file can later be downloaded."), "",
- ?T("In order to use this module, it must be configured as "
- "a 'request_handler' for 'ejabberd_http' listener.")],
+ ?T("In order to use this module, it must be enabled "
+ "in 'listen' -> 'ejabberd_http' -> "
+ "http://../listen-options/#request-handlers[request_handlers].")],
opts =>
[{host,
#{desc => ?T("Deprecated. Use 'hosts' instead.")}},
@@ -320,17 +321,18 @@ mod_doc() ->
"used for file uploads. The keyword @HOST@ is replaced "
"with the virtual host name. NOTE: different virtual "
"hosts cannot use the same PUT URL. "
- "The default value is \"https://@HOST@:5443\".")}},
+ "The default value is \"https://@HOST@:5443/upload\".")}},
{get_url,
#{value => ?T("URL"),
desc =>
?T("This option specifies the initial part of the GET URLs "
- "used for downloading the files. By default, it is set "
+ "used for downloading the files. The default value is 'undefined'. "
+ "When this option is 'undefined', this option is set "
"to the same value as 'put_url'. The keyword @HOST@ is "
"replaced with the virtual host name. NOTE: if GET requests "
"are handled by 'mod_http_upload', the 'get_url' must match the "
"'put_url'. Setting it to a different value only makes "
- "sense if an external web server or 'mod_http_fileserver' "
+ "sense if an external web server or _`mod_http_fileserver`_ "
"is used to serve the uploaded files.")}},
{service_url,
#{desc => ?T("Deprecated.")}},
diff --git a/src/mod_http_upload_quota.erl b/src/mod_http_upload_quota.erl
index 48b7b1958..5ed7fcefb 100644
--- a/src/mod_http_upload_quota.erl
+++ b/src/mod_http_upload_quota.erl
@@ -27,7 +27,6 @@
-author('holger@zedat.fu-berlin.de').
-define(TIMEOUT, timer:hours(24)).
--define(INITIAL_TIMEOUT, timer:minutes(10)).
-define(FORMAT(Error), file:format_error(Error)).
-behaviour(gen_server).
@@ -64,7 +63,7 @@
max_days :: pos_integer() | infinity,
docroot :: binary(),
disk_usage = #{} :: disk_usage(),
- timers :: [timer:tref()]}).
+ timer :: reference() | undefined}).
-type disk_usage() :: #{{binary(), binary()} => non_neg_integer()}.
-type state() :: #state{}.
@@ -166,12 +165,11 @@ init([ServerHost|_]) ->
DocRoot1 = mod_http_upload_opt:docroot(ServerHost),
DocRoot2 = mod_http_upload:expand_home(str:strip(DocRoot1, right, $/)),
DocRoot3 = mod_http_upload:expand_host(DocRoot2, ServerHost),
- Timers = if MaxDays == infinity -> [];
- true ->
- {ok, T1} = timer:send_after(?INITIAL_TIMEOUT, sweep),
- {ok, T2} = timer:send_interval(?TIMEOUT, sweep),
- [T1, T2]
- end,
+ Timer = if MaxDays == infinity -> undefined;
+ true ->
+ Timeout = p1_rand:uniform(?TIMEOUT div 2),
+ erlang:send_after(Timeout, self(), sweep)
+ end,
ejabberd_hooks:add(http_upload_slot_request, ServerHost, ?MODULE,
handle_slot_request, 50),
{ok, #state{server_host = ServerHost,
@@ -179,7 +177,7 @@ init([ServerHost|_]) ->
access_hard_quota = AccessHardQuota,
max_days = MaxDays,
docroot = DocRoot3,
- timers = Timers}}.
+ timer = Timer}}.
-spec handle_call(_, {pid(), _}, state()) -> {noreply, state()}.
handle_call(Request, From, State) ->
@@ -249,6 +247,7 @@ handle_info(sweep, #state{server_host = ServerHost,
max_days = MaxDays} = State)
when is_integer(MaxDays), MaxDays > 0 ->
?DEBUG("Got 'sweep' message for ~ts", [ServerHost]),
+ Timer = erlang:send_after(?TIMEOUT, self(), sweep),
case file:list_dir(DocRoot) of
{ok, Entries} ->
BackThen = secs_since_epoch() - (MaxDays * 86400),
@@ -264,17 +263,17 @@ handle_info(sweep, #state{server_host = ServerHost,
?ERROR_MSG("Cannot open document root ~ts: ~ts",
[DocRoot, ?FORMAT(Error)])
end,
- {noreply, State};
+ {noreply, State#state{timer = Timer}};
handle_info(Info, State) ->
?ERROR_MSG("Unexpected info: ~p", [Info]),
{noreply, State}.
-spec terminate(normal | shutdown | {shutdown, _} | _, state()) -> ok.
-terminate(Reason, #state{server_host = ServerHost, timers = Timers}) ->
+terminate(Reason, #state{server_host = ServerHost, timer = Timer}) ->
?DEBUG("Stopping upload quota process for ~ts: ~p", [ServerHost, Reason]),
ejabberd_hooks:delete(http_upload_slot_request, ServerHost, ?MODULE,
handle_slot_request, 50),
- lists:foreach(fun timer:cancel/1, Timers).
+ misc:cancel_timer(Timer).
-spec code_change({down, _} | _, state(), _) -> {ok, state()}.
code_change(_OldVsn, #state{server_host = ServerHost} = State, _Extra) ->
diff --git a/src/mod_last.erl b/src/mod_last.erl
index 295a546f2..a7d36c791 100644
--- a/src/mod_last.erl
+++ b/src/mod_last.erl
@@ -344,20 +344,20 @@ mod_doc() ->
[{db_type,
#{value => "mnesia | sql",
desc =>
- ?T("Same as top-level 'default_db' option, but applied to this module only.")}},
+ ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}},
{use_cache,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'use_cache' option, but applied to this module only.")}},
+ ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}},
{cache_size,
#{value => "pos_integer() | infinity",
desc =>
- ?T("Same as top-level 'cache_size' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}},
{cache_missed,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'cache_missed' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}},
{cache_life_time,
#{value => "timeout()",
desc =>
- ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}}]}.
+ ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}.
diff --git a/src/mod_mam.erl b/src/mod_mam.erl
index 12542bfa5..9bf154f58 100644
--- a/src/mod_mam.erl
+++ b/src/mod_mam.erl
@@ -28,6 +28,7 @@
-protocol({xep, 313, '0.6.1'}).
-protocol({xep, 334, '0.2'}).
-protocol({xep, 359, '0.5.0'}).
+-protocol({xep, 441, '0.2.0'}).
-behaviour(gen_mod).
@@ -148,7 +149,7 @@ start(Host, Opts) ->
ejabberd_hooks:add(check_create_room, Host, ?MODULE,
check_create_room, 50)
end,
- ejabberd_commands:register_commands(get_commands_spec()),
+ ejabberd_commands:register_commands(?MODULE, get_commands_spec()),
ok;
Err ->
Err
@@ -1456,7 +1457,7 @@ mod_doc() ->
#{value => "true | false",
desc =>
?T("This option determines how ejabberd's "
- "stream management code (see 'mod_stream_mgmt') "
+ "stream management code (see _`mod_stream_mgmt`_) "
"handles unacknowledged messages when the "
"connection is lost. Usually, such messages are "
"either bounced or resent. However, neither is "
@@ -1495,28 +1496,28 @@ mod_doc() ->
#{value => "true | false",
desc =>
?T("Whether to destroy message archive of a room "
- "(see 'mod_muc') when it gets destroyed. "
+ "(see _`mod_muc`_) when it gets destroyed. "
"The default value is 'true'.")}},
{db_type,
#{value => "mnesia | sql",
desc =>
- ?T("Same as top-level 'default_db' option, but applied to this module only.")}},
+ ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}},
{use_cache,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'use_cache' option, but applied to this module only.")}},
+ ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}},
{cache_size,
#{value => "pos_integer() | infinity",
desc =>
- ?T("Same as top-level 'cache_size' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}},
{cache_missed,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'cache_missed' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}},
{cache_life_time,
#{value => "timeout()",
desc =>
- ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}},
{user_mucsub_from_muc_archive,
#{value => "true | false",
desc =>
diff --git a/src/mod_mam_sql.erl b/src/mod_mam_sql.erl
index e5069b9a2..269b4c963 100644
--- a/src/mod_mam_sql.erl
+++ b/src/mod_mam_sql.erl
@@ -72,7 +72,7 @@ remove_from_archive(LUser, LServer, WithJid) ->
end.
delete_old_messages(ServerHost, TimeStamp, Type) ->
- TS = now_to_usec(TimeStamp),
+ TS = misc:now_to_usec(TimeStamp),
case Type of
all ->
ejabberd_sql:sql_query(
@@ -315,7 +315,7 @@ export(_Server) ->
id = _ID, timestamp = TS, peer = Peer,
type = Type, nick = Nick, packet = Pkt})
when LServer == Host ->
- TStmp = now_to_usec(TS),
+ TStmp = misc:now_to_usec(TS),
SUser = case Type of
chat -> LUser;
groupchat -> jid:encode({LUser, LServer, <<>>})
@@ -372,16 +372,6 @@ is_empty_for_room(LServer, LName, LHost) ->
%%%===================================================================
%%% Internal functions
%%%===================================================================
-now_to_usec({MSec, Sec, USec}) ->
- (MSec*1000000 + Sec)*1000000 + USec.
-
-usec_to_now(Int) ->
- Secs = Int div 1000000,
- USec = Int rem 1000000,
- MSec = Secs div 1000000,
- Sec = Secs rem 1000000,
- {MSec, Sec, USec}.
-
make_sql_query(User, LServer, MAMQuery, RSM, ExtraUsernames) ->
Start = proplists:get_value(start, MAMQuery),
End = proplists:get_value('end', MAMQuery),
@@ -432,14 +422,14 @@ make_sql_query(User, LServer, MAMQuery, RSM, ExtraUsernames) ->
StartClause = case Start of
{_, _, _} ->
[<<" and timestamp >= ">>,
- integer_to_binary(now_to_usec(Start))];
+ integer_to_binary(misc:now_to_usec(Start))];
_ ->
[]
end,
EndClause = case End of
{_, _, _} ->
[<<" and timestamp <= ">>,
- integer_to_binary(now_to_usec(End))];
+ integer_to_binary(misc:now_to_usec(End))];
_ ->
[]
end,
@@ -526,7 +516,7 @@ make_archive_el(User, TS, XML, Peer, Kind, Nick, MsgType, JidRequestor, JidArchi
TSInt ->
try jid:decode(Peer) of
PeerJID ->
- Now = usec_to_now(TSInt),
+ Now = misc:usec_to_now(TSInt),
PeerLJID = jid:tolower(PeerJID),
T = case Kind of
<<"">> -> chat;
diff --git a/src/mod_mix.erl b/src/mod_mix.erl
index 82b5e41a4..002ef5696 100644
--- a/src/mod_mix.erl
+++ b/src/mod_mix.erl
@@ -106,12 +106,12 @@ mod_doc() ->
"experimental feature, updated in 19.02, and is not "
"yet ready to use in production. It's asserted that "
"the MIX protocol is going to replace the MUC protocol "
- "in the future (see 'mod_muc')."), "",
+ "in the future (see _`mod_muc`_)."), "",
?T("To learn more about how to use that feature, you can refer to "
"our tutorial: https://docs.ejabberd.im/tutorials/mix-010/"
"[Getting started with XEP-0369: Mediated Information "
"eXchange (MIX) v0.1]."), "",
- ?T("The module depends on 'mod_mam'.")],
+ ?T("The module depends on _`mod_mam`_.")],
opts =>
[{access_create,
#{value => ?T("AccessName"),
@@ -136,7 +136,7 @@ mod_doc() ->
{db_type,
#{value => "mnesia | sql",
desc =>
- ?T("Same as top-level 'default_db' option, but applied to this module only.")}}]}.
+ ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}]}.
-spec route(stanza()) -> ok.
route(#iq{} = IQ) ->
diff --git a/src/mod_mix_pam.erl b/src/mod_mix_pam.erl
index c6348b92f..1fa5c1861 100644
--- a/src/mod_mix_pam.erl
+++ b/src/mod_mix_pam.erl
@@ -120,23 +120,23 @@ mod_doc() ->
[{db_type,
#{value => "mnesia | sql",
desc =>
- ?T("Same as top-level 'default_db' option, but applied to this module only.")}},
+ ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}},
{use_cache,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'use_cache' option, but applied to this module only.")}},
+ ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}},
{cache_size,
#{value => "pos_integer() | infinity",
desc =>
- ?T("Same as top-level 'cache_size' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}},
{cache_missed,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'cache_missed' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}},
{cache_life_time,
#{value => "timeout()",
desc =>
- ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}}]}.
+ ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}.
-spec bounce_sm_packet({term(), stanza()}) -> {term(), stanza()}.
bounce_sm_packet({_, #message{to = #jid{lresource = <<>>} = To,
diff --git a/src/mod_mqtt.erl b/src/mod_mqtt.erl
index 8734c778d..5d00408df 100644
--- a/src/mod_mqtt.erl
+++ b/src/mod_mqtt.erl
@@ -278,8 +278,9 @@ listen_options() ->
%%%===================================================================
mod_doc() ->
#{desc =>
- ?T("This module adds support for the MQTT protocol "
- "version '3.1.1' and '5.0'. Remember to configure "
+ ?T("This module adds "
+ "https://docs.ejabberd.im/admin/guide/mqtt/[support for the MQTT] "
+ "protocol version '3.1.1' and '5.0'. Remember to configure "
"'mod_mqtt' in 'modules' and 'listen' sections."),
opts =>
[{access_subscribe,
@@ -326,37 +327,37 @@ mod_doc() ->
{queue_type,
#{value => "ram | file",
desc =>
- ?T("Same as top-level 'queue_type' option, "
+ ?T("Same as top-level _`queue_type`_ option, "
"but applied to this module only.")}},
{ram_db_type,
#{value => "mnesia",
desc =>
- ?T("Same as top-level 'default_ram_db' option, "
+ ?T("Same as top-level _`default_ram_db`_ option, "
"but applied to this module only.")}},
{db_type,
#{value => "mnesia | sql",
desc =>
- ?T("Same as top-level 'default_db' option, "
+ ?T("Same as top-level _`default_db`_ option, "
"but applied to this module only.")}},
{use_cache,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'use_cache' option, "
+ ?T("Same as top-level _`use_cache`_ option, "
"but applied to this module only.")}},
{cache_size,
#{value => "pos_integer() | infinity",
desc =>
- ?T("Same as top-level 'cache_size' option, "
+ ?T("Same as top-level _`cache_size`_ option, "
"but applied to this module only.")}},
{cache_missed,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'cache_missed' option, "
+ ?T("Same as top-level _`cache_missed`_ option, "
"but applied to this module only.")}},
{cache_life_time,
#{value => "timeout()",
desc =>
- ?T("Same as top-level 'cache_life_time' option, "
+ ?T("Same as top-level _`cache_life_time`_ option, "
"but applied to this module only.")}}]}.
%%%===================================================================
diff --git a/src/mod_muc.erl b/src/mod_muc.erl
index 46bfd20e5..b2ebc5c61 100644
--- a/src/mod_muc.erl
+++ b/src/mod_muc.erl
@@ -40,6 +40,7 @@
room_destroyed/4,
store_room/4,
store_room/5,
+ store_changes/4,
restore_room/3,
forget_room/3,
create_room/3,
@@ -91,6 +92,7 @@
-callback init(binary(), gen_mod:opts()) -> any().
-callback import(binary(), binary(), [binary()]) -> ok.
-callback store_room(binary(), binary(), binary(), list(), list()|undefined) -> {atomic, any()}.
+-callback store_changes(binary(), binary(), binary(), list()) -> {atomic, any()}.
-callback restore_room(binary(), binary(), binary()) -> muc_room_opts() | error.
-callback forget_room(binary(), binary(), binary()) -> {atomic, any()}.
-callback can_use_nick(binary(), binary(), jid(), binary()) -> boolean().
@@ -111,7 +113,8 @@
-callback get_subscribed_rooms(binary(), binary(), jid()) ->
{ok, [{jid(), binary(), [binary()]}]} | {error, db_failure}.
--optional_callbacks([get_subscribed_rooms/3]).
+-optional_callbacks([get_subscribed_rooms/3,
+ store_changes/4]).
%%====================================================================
%% API
@@ -313,6 +316,11 @@ store_room(ServerHost, Host, Name, Opts, ChangesHints) ->
Mod = gen_mod:db_mod(LServer, ?MODULE),
Mod:store_room(LServer, Host, Name, Opts, ChangesHints).
+store_changes(ServerHost, Host, Name, ChangesHints) ->
+ LServer = jid:nameprep(ServerHost),
+ Mod = gen_mod:db_mod(LServer, ?MODULE),
+ Mod:store_changes(LServer, Host, Name, ChangesHints).
+
restore_room(ServerHost, Host, Name) ->
LServer = jid:nameprep(ServerHost),
Mod = gen_mod:db_mod(LServer, ?MODULE),
@@ -570,7 +578,7 @@ unhibernate_room(ServerHost, Host, Room) ->
case RMod:find_online_room(ServerHost, Room, Host) of
error ->
Proc = procname(ServerHost, {Room, Host}),
- case ?GEN_SERVER:call(Proc, {unhibernate, Room, Host}) of
+ case ?GEN_SERVER:call(Proc, {unhibernate, Room, Host}, 20000) of
{ok, _} = R -> R;
_ -> error
end;
@@ -1358,19 +1366,19 @@ mod_doc() ->
desc =>
?T("To configure who is allowed to create new rooms at the "
"Multi-User Chat service, this option can be used. "
- "By default any account in the local ejabberd server is "
+ "The default value is 'all', which means everyone is "
"allowed to create rooms.")}},
{access_persistent,
#{value => ?T("AccessName"),
desc =>
?T("To configure who is allowed to modify the 'persistent' room option. "
- "By default any account in the local ejabberd server is allowed to "
+ "The default value is 'all', which means everyone is allowed to "
"modify that option.")}},
{access_mam,
#{value => ?T("AccessName"),
desc =>
?T("To configure who is allowed to modify the 'mam' room option. "
- "By default any account in the local ejabberd server is allowed to "
+ "The default value is 'all', which means everyone is allowed to "
"modify that option.")}},
{access_register,
#{value => ?T("AccessName"),
@@ -1486,7 +1494,8 @@ mod_doc() ->
?T("This option defines after how many users in the room, "
"it is considered overcrowded. When a MUC room is considered "
"overcrowed, presence broadcasts are limited to reduce load, "
- "traffic and excessive presence \"storm\" received by participants.")}},
+ "traffic and excessive presence \"storm\" received by participants. "
+ "The default value is '1000'.")}},
{min_message_interval,
#{value => ?T("Number"),
desc =>
@@ -1517,7 +1526,7 @@ mod_doc() ->
{queue_type,
#{value => "ram | file",
desc =>
- ?T("Same as top-level 'queue_type' option, but applied to this module only.")}},
+ ?T("Same as top-level _`queue_type`_ option, but applied to this module only.")}},
{regexp_room_id,
#{value => "string()",
desc =>
@@ -1634,7 +1643,7 @@ mod_doc() ->
{logging,
#{value => "true | false",
desc =>
- ?T("The public messages are logged using 'mod_muc_log'. "
+ ?T("The public messages are logged using _`mod_muc_log`_. "
"The default value is 'false'.")}},
{members_by_default,
#{value => "true | false",
diff --git a/src/mod_muc_admin.erl b/src/mod_muc_admin.erl
index 72a29a45b..ce4665d7e 100644
--- a/src/mod_muc_admin.erl
+++ b/src/mod_muc_admin.erl
@@ -57,7 +57,7 @@
%%----------------------------
start(Host, _Opts) ->
- ejabberd_commands:register_commands(get_commands_spec()),
+ ejabberd_commands:register_commands(?MODULE, get_commands_spec()),
ejabberd_hooks:add(webadmin_menu_main, ?MODULE, web_menu_main, 50),
ejabberd_hooks:add(webadmin_menu_host, Host, ?MODULE, web_menu_host, 50),
ejabberd_hooks:add(webadmin_page_main, ?MODULE, web_page_main, 50),
@@ -710,7 +710,7 @@ create_room_with_opts(Name1, Host1, ServerHost1, CustomRoomOpts) ->
maybe_store_room(ServerHost, Host, Name, RoomOpts) ->
case proplists:get_bool(persistent, RoomOpts) of
true ->
- {atomic, ok} = mod_muc:store_room(ServerHost, Host, Name, RoomOpts),
+ {atomic, _} = mod_muc:store_room(ServerHost, Host, Name, RoomOpts),
ok;
false ->
ok
@@ -1413,4 +1413,4 @@ mod_doc() ->
[?T("This module provides commands to administer local MUC "
"services and their MUC rooms. It also provides simple "
"WebAdmin pages to view the existing rooms."), "",
- ?T("This module depends on 'mod_muc'.")]}.
+ ?T("This module depends on _`mod_muc`_.")]}.
diff --git a/src/mod_muc_log.erl b/src/mod_muc_log.erl
index 53223a8eb..8bcbc8bc0 100644
--- a/src/mod_muc_log.erl
+++ b/src/mod_muc_log.erl
@@ -1021,7 +1021,7 @@ mod_doc() ->
?T("- URLs on messages and subjects are converted to hyperlinks."), "",
?T("- Timezone used on timestamps is shown on the log files."), "",
?T("- A custom link can be added on top of each page."), "",
- ?T("The module depends on 'mod_muc'.")],
+ ?T("The module depends on _`mod_muc`_.")],
opts =>
[{access_log,
#{value => ?T("AccessName"),
diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl
index fc03e50ef..e8b0d1bce 100644
--- a/src/mod_muc_room.erl
+++ b/src/mod_muc_room.erl
@@ -27,6 +27,8 @@
-author('alexey@process-one.net').
+-protocol({xep, 317, '0.1'}).
+
-behaviour(p1_fsm).
%% External exports
@@ -76,6 +78,12 @@
-define(DEFAULT_MAX_USERS_PRESENCE,1000).
+-define(MUC_HAT_ADD_CMD, <<"http://prosody.im/protocol/hats#add">>).
+-define(MUC_HAT_REMOVE_CMD, <<"http://prosody.im/protocol/hats#remove">>).
+-define(MUC_HAT_LIST_CMD, <<"p1:hats#list">>).
+-define(MAX_HATS_USERS, 100).
+-define(MAX_HATS_PER_USER, 10).
+
%-define(DBGFSM, true).
-ifdef(DBGFSM).
@@ -446,6 +454,8 @@ normal_state({route, <<"">>,
process_iq_mucsub(From, IQ, StateData);
#xcaptcha{} ->
process_iq_captcha(From, IQ, StateData);
+ #adhoc_command{} ->
+ process_iq_adhoc(From, IQ, StateData);
_ ->
Txt = ?T("The feature requested is not "
"supported by the conference"),
@@ -641,7 +651,7 @@ handle_event({service_message, Msg}, _StateName,
MessagePkt = #message{type = groupchat, body = xmpp:mk_text(Msg)},
send_wrapped_multiple(
StateData#state.jid,
- get_users_and_subscribers(StateData),
+ get_users_and_subscribers_with_node(?NS_MUCSUB_NODES_MESSAGES, StateData),
MessagePkt,
?NS_MUCSUB_NODES_MESSAGES,
StateData),
@@ -705,7 +715,7 @@ handle_sync_event({change_state, NewStateData}, _From,
true ->
ok;
_ ->
- erlang:put(muc_subscribers, NewStateData#state.subscribers)
+ erlang:put(muc_subscribers, NewStateData#state.muc_subscribers#muc_subscribers.subscribers)
end,
{reply, {ok, NewStateData}, StateName, NewStateData};
handle_sync_event({process_item_change, Item, UJID}, _From, StateName, StateData) ->
@@ -717,8 +727,10 @@ handle_sync_event({process_item_change, Item, UJID}, _From, StateName, StateData
{reply, {ok, NSD}, StateName, NSD}
end;
handle_sync_event(get_subscribers, _From, StateName, StateData) ->
- JIDs = lists:map(fun jid:make/1,
- maps:keys(StateData#state.subscribers)),
+ JIDs = muc_subscribers_fold(
+ fun(_LBareJID, #subscriber{jid = JID}, Acc) ->
+ [JID | Acc]
+ end, [], StateData#state.muc_subscribers),
{reply, {ok, JIDs}, StateName, StateData};
handle_sync_event({muc_subscribe, From, Nick, Nodes}, _From,
StateName, StateData) ->
@@ -762,7 +774,8 @@ handle_sync_event({muc_unsubscribe, From}, _From, StateName,
{reply, {error, get_error_text(Err)}, StateName, StateData}
end;
handle_sync_event({is_subscribed, From}, _From, StateName, StateData) ->
- IsSubs = try maps:get(jid:split(From), StateData#state.subscribers) of
+ IsSubs = try muc_subscribers_get(
+ jid:split(From), StateData#state.muc_subscribers) of
#subscriber{nick = Nick, nodes = Nodes} -> {true, Nick, Nodes}
catch _:{badkey, _} -> false
end,
@@ -899,7 +912,8 @@ terminate(Reason, _StateName,
_ -> ok
end,
tab_remove_online_user(JID, StateData)
- end, [], get_users_and_subscribers(StateData)),
+ end, [], get_users_and_subscribers_with_node(
+ ?NS_MUCSUB_NODES_PARTICIPANTS, StateData)),
disable_hibernate_timer(StateData),
case StateData#state.hibernate_timer of
@@ -991,7 +1005,7 @@ process_groupchat_message(#message{from = From, lang = Lang} = Packet, StateData
end,
send_wrapped_multiple(
jid:replace_resource(StateData#state.jid, FromNick),
- get_users_and_subscribers(StateData),
+ get_users_and_subscribers_with_node(Node, StateData),
NewPacket, Node, NewStateData1),
NewStateData2 = case has_body_or_subject(NewPacket) of
true ->
@@ -1197,8 +1211,8 @@ get_participant_data(From, StateData) ->
#user{nick = FromNick, role = Role} ->
{FromNick, Role}
catch _:{badkey, _} ->
- try maps:get(jid:tolower(jid:remove_resource(From)),
- StateData#state.subscribers) of
+ try muc_subscribers_get(jid:tolower(jid:remove_resource(From)),
+ StateData#state.muc_subscribers) of
#subscriber{nick = FromNick} ->
{FromNick, none}
catch _:{badkey, _} ->
@@ -1329,7 +1343,7 @@ maybe_strip_status_from_presence(From, Packet, StateData) ->
close_room_if_temporary_and_empty(StateData1) ->
case not (StateData1#state.config)#config.persistent
andalso maps:size(StateData1#state.users) == 0
- andalso maps:size(StateData1#state.subscribers) == 0 of
+ andalso muc_subscribers_size(StateData1#state.muc_subscribers) == 0 of
true ->
?INFO_MSG("Destroyed MUC room ~ts because it's temporary "
"and empty",
@@ -1342,6 +1356,17 @@ close_room_if_temporary_and_empty(StateData1) ->
-spec get_users_and_subscribers(state()) -> users().
get_users_and_subscribers(StateData) ->
+ get_users_and_subscribers_aux(
+ StateData#state.muc_subscribers#muc_subscribers.subscribers,
+ StateData).
+
+-spec get_users_and_subscribers_with_node(binary(), state()) -> users().
+get_users_and_subscribers_with_node(Node, StateData) ->
+ get_users_and_subscribers_aux(
+ muc_subscribers_get_by_node(Node, StateData#state.muc_subscribers),
+ StateData).
+
+get_users_and_subscribers_aux(Subscribers, StateData) ->
OnlineSubscribers = maps:fold(
fun(LJID, _, Acc) ->
LBareJID = jid:remove_resource(LJID),
@@ -1365,7 +1390,7 @@ get_users_and_subscribers(StateData) ->
true ->
Acc
end
- end, StateData#state.users, StateData#state.subscribers).
+ end, StateData#state.users, Subscribers).
-spec is_user_online(jid(), state()) -> boolean().
is_user_online(JID, StateData) ->
@@ -1375,7 +1400,7 @@ is_user_online(JID, StateData) ->
-spec is_subscriber(jid(), state()) -> boolean().
is_subscriber(JID, StateData) ->
LJID = jid:tolower(jid:remove_resource(JID)),
- maps:is_key(LJID, StateData#state.subscribers).
+ muc_subscribers_is_key(LJID, StateData#state.muc_subscribers).
%% Check if the user is occupant of the room, or at least is an admin or owner.
-spec is_occupant_or_admin(jid(), state()) -> boolean().
@@ -1390,6 +1415,12 @@ is_occupant_or_admin(JID, StateData) ->
_ -> false
end.
+%% Check if the user is an admin or owner.
+-spec is_admin(jid(), state()) -> boolean().
+is_admin(JID, StateData) ->
+ FAffiliation = get_affiliation(JID, StateData),
+ FAffiliation == admin orelse FAffiliation == owner.
+
%% Decide the fate of the message and its sender
%% Returns: continue_delivery | forget_message | {expulse_sender, Reason}
-spec decide_fate_message(message(), jid(), state()) ->
@@ -1869,16 +1900,15 @@ set_subscriber(JID, Nick, Nodes,
#state{room = Room, host = Host, server_host = ServerHost} = StateData) ->
BareJID = jid:remove_resource(JID),
LBareJID = jid:tolower(BareJID),
- Subscribers = maps:put(LBareJID,
- #subscriber{jid = BareJID,
- nick = Nick,
- nodes = Nodes},
- StateData#state.subscribers),
- Nicks = maps:put(Nick, [LBareJID], StateData#state.subscriber_nicks),
- NewStateData = StateData#state{subscribers = Subscribers,
- subscriber_nicks = Nicks},
+ MUCSubscribers =
+ muc_subscribers_put(
+ #subscriber{jid = BareJID,
+ nick = Nick,
+ nodes = Nodes},
+ StateData#state.muc_subscribers),
+ NewStateData = StateData#state{muc_subscribers = MUCSubscribers},
store_room(NewStateData, [{add_subscription, BareJID, Nick, Nodes}]),
- case not maps:is_key(LBareJID, StateData#state.subscribers) of
+ case not muc_subscribers_is_key(LBareJID, StateData#state.muc_subscribers) of
true ->
send_subscriptions_change_notifications(BareJID, Nick, subscribe, NewStateData),
ejabberd_hooks:run(muc_subscribed, ServerHost, [ServerHost, Room, Host, BareJID]);
@@ -1921,7 +1951,7 @@ filter_presence(Presence) ->
XMLNS = xmpp:get_ns(El),
case catch binary:part(XMLNS, 0, size(?NS_MUC)) of
?NS_MUC -> false;
- _ -> true
+ _ -> XMLNS /= ?NS_HATS
end
end, xmpp:get_els(Presence)),
xmpp:set_els(Presence, Els).
@@ -1956,7 +1986,8 @@ add_user_presence_un(JID, Presence, StateData) ->
-spec find_jids_by_nick(binary(), state()) -> [jid()].
find_jids_by_nick(Nick, StateData) ->
Users = case maps:get(Nick, StateData#state.nicks, []) of
- [] -> maps:get(Nick, StateData#state.subscriber_nicks, []);
+ [] -> muc_subscribers_get_by_nick(
+ Nick, StateData#state.muc_subscribers);
Us -> Us
end,
[jid:make(LJID) || LJID <- Users].
@@ -2020,10 +2051,10 @@ is_nick_change(JID, Nick, StateData) ->
nick_collision(User, Nick, StateData) ->
UserOfNick = case find_jid_by_nick(Nick, StateData) of
false ->
- try maps:get(Nick, StateData#state.subscriber_nicks) of
- [J] -> J
- catch _:{badkey, _} -> false
- end;
+ case muc_subscribers_get_by_nick(Nick, StateData#state.muc_subscribers) of
+ [J] -> J;
+ [] -> false
+ end;
J -> J
end,
(UserOfNick /= false andalso
@@ -2433,6 +2464,11 @@ send_new_presence(NJID, Reason, IsInitialPresence, StateData, OldStateData) ->
false -> {none, #presence{type = unavailable}}
end,
Affiliation = get_affiliation(LJID, StateData),
+ Node1 = case is_ra_changed(NJID, IsInitialPresence, StateData, OldStateData) of
+ true -> ?NS_MUCSUB_NODES_AFFILIATIONS;
+ false -> ?NS_MUCSUB_NODES_PRESENCE
+ end,
+ Node2 = ?NS_MUCSUB_NODES_PARTICIPANTS,
UserMap =
case is_room_overcrowded(StateData) orelse
(not (presence_broadcast_allowed(NJID, StateData) orelse
@@ -2440,7 +2476,10 @@ send_new_presence(NJID, Reason, IsInitialPresence, StateData, OldStateData) ->
true ->
#{LNJID => UserInfo};
false ->
- get_users_and_subscribers(StateData)
+ %% TODO: optimize further
+ UM1 = get_users_and_subscribers_with_node(Node1, StateData),
+ UM2 = get_users_and_subscribers_with_node(Node2, StateData),
+ maps:merge(UM1, UM2)
end,
maps:fold(
fun(LUJID, Info, _) ->
@@ -2462,13 +2501,10 @@ send_new_presence(NJID, Reason, IsInitialPresence, StateData, OldStateData) ->
Pres = if Presence == undefined -> #presence{};
true -> Presence
end,
- Packet = xmpp:set_subtag(
- Pres, #muc_user{items = [Item],
- status_codes = StatusCodes}),
- Node1 = case is_ra_changed(NJID, IsInitialPresence, StateData, OldStateData) of
- true -> ?NS_MUCSUB_NODES_AFFILIATIONS;
- false -> ?NS_MUCSUB_NODES_PRESENCE
- end,
+ Packet = xmpp:set_subtag(
+ add_presence_hats(NJID, Pres, StateData),
+ #muc_user{items = [Item],
+ status_codes = StatusCodes}),
send_wrapped(jid:replace_resource(StateData#state.jid, Nick),
Info#user.jid, Packet, Node1, StateData),
Type = xmpp:get_type(Packet),
@@ -2476,7 +2512,6 @@ send_new_presence(NJID, Reason, IsInitialPresence, StateData, OldStateData) ->
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 ->
@@ -2518,7 +2553,9 @@ send_existing_presences1(ToJID, StateData) ->
false -> Item0
end,
Packet = xmpp:set_subtag(
- Presence, #muc_user{items = [Item]}),
+ add_presence_hats(
+ FromJID, Presence, StateData),
+ #muc_user{items = [Item]}),
send_wrapped(jid:replace_resource(StateData#state.jid, FromNick),
RealToJID, Packet, ?NS_MUCSUB_NODES_PRESENCE, StateData)
end
@@ -2607,11 +2644,13 @@ send_nick_changing(JID, OldNick, StateData,
end;
(_, _, _) ->
ok
- end, ok, get_users_and_subscribers(StateData)).
+ end, ok, get_users_and_subscribers_with_node(
+ ?NS_MUCSUB_NODES_PRESENCE, StateData)).
-spec maybe_send_affiliation(jid(), affiliation(), state()) -> ok.
maybe_send_affiliation(JID, Affiliation, StateData) ->
LJID = jid:tolower(JID),
+ %% TODO: there should be a better way to check IsOccupant
Users = get_users_and_subscribers(StateData),
IsOccupant = case LJID of
{LUser, LServer, <<"">>} ->
@@ -2637,7 +2676,8 @@ send_affiliation(JID, Affiliation, StateData) ->
role = none},
Message = #message{id = p1_rand:get_string(),
sub_els = [#muc_user{items = [Item]}]},
- Users = get_users_and_subscribers(StateData),
+ Users = get_users_and_subscribers_with_node(
+ ?NS_MUCSUB_NODES_AFFILIATIONS, StateData),
Recipients = case (StateData#state.config)#config.anonymous of
true ->
maps:filter(fun(_, #user{role = moderator}) ->
@@ -3271,6 +3311,13 @@ send_kickban_presence1(MJID, UJID, Reason, Code, Affiliation,
StateData) ->
#user{jid = RealJID, nick = Nick} = maps:get(jid:tolower(UJID), StateData#state.users),
ActorNick = get_actor_nick(MJID, StateData),
+ %% TODO: optimize further
+ UserMap =
+ maps:merge(
+ get_users_and_subscribers_with_node(
+ ?NS_MUCSUB_NODES_AFFILIATIONS, StateData),
+ get_users_and_subscribers_with_node(
+ ?NS_MUCSUB_NODES_PARTICIPANTS, StateData)),
maps:fold(
fun(LJID, Info, _) ->
IsSelfPresence = jid:tolower(UJID) == LJID,
@@ -3304,7 +3351,7 @@ send_kickban_presence1(MJID, UJID, Reason, Code, Affiliation,
true ->
ok
end
- end, ok, get_users_and_subscribers(StateData)).
+ end, ok, UserMap).
-spec get_actor_nick(undefined | jid(), state()) -> binary().
get_actor_nick(undefined, _StateData) ->
@@ -3551,7 +3598,8 @@ get_config(Lang, StateData, From) ->
{allow_voice_requests, Config#config.allow_voice_requests},
{allow_subscription, Config#config.allow_subscription},
{voice_request_min_interval, Config#config.voice_request_min_interval},
- {pubsub, Config#config.pubsub}]
+ {pubsub, Config#config.pubsub},
+ {enable_hats, Config#config.enable_hats}]
++
case ejabberd_captcha:is_feature_available() of
true ->
@@ -3639,6 +3687,7 @@ set_config(Opts, Config, ServerHost, Lang) ->
({maxusers, V}, C) -> C#config{max_users = V};
({enablelogging, V}, C) -> C#config{logging = V};
({pubsub, V}, C) -> C#config{pubsub = V};
+ ({enable_hats, V}, C) -> C#config{enable_hats = V};
({lang, L}, C) -> C#config{lang = L};
({captcha_whitelist, Js}, C) ->
LJIDs = [jid:tolower(J) || J <- Js],
@@ -3720,7 +3769,8 @@ send_config_change_info(New, #state{config = Old} = StateData) ->
id = p1_rand:get_string(),
sub_els = [#muc_user{status_codes = Codes}]},
send_wrapped_multiple(StateData#state.jid,
- get_users_and_subscribers(StateData),
+ get_users_and_subscribers_with_node(
+ ?NS_MUCSUB_NODES_CONFIG, StateData),
Message,
?NS_MUCSUB_NODES_CONFIG,
StateData);
@@ -3868,30 +3918,30 @@ set_opts([{Opt, Val} | Opts], StateData) ->
allow_subscription ->
StateData#state{config =
(StateData#state.config)#config{allow_subscription = Val}};
+ enable_hats ->
+ StateData#state{config =
+ (StateData#state.config)#config{enable_hats = Val}};
lang ->
StateData#state{config =
(StateData#state.config)#config{lang = Val}};
subscribers ->
- {Subscribers, Nicks} =
- lists:foldl(
- fun({JID, Nick, Nodes}, {SubAcc, NickAcc}) ->
- BareJID = case JID of
- #jid{} -> jid:remove_resource(JID);
- _ ->
- ?ERROR_MSG("Invalid subscriber JID in set_opts ~p", [JID]),
- jid:remove_resource(jid:make(JID))
- end,
- LBareJID = jid:tolower(BareJID),
- {maps:put(
- LBareJID,
- #subscriber{jid = BareJID,
- nick = Nick,
- nodes = Nodes},
- SubAcc),
- maps:put(Nick, [LBareJID], NickAcc)}
- end, {#{}, #{}}, Val),
- StateData#state{subscribers = Subscribers,
- subscriber_nicks = Nicks};
+ MUCSubscribers =
+ lists:foldl(
+ fun({JID, Nick, Nodes}, MUCSubs) ->
+ BareJID =
+ case JID of
+ #jid{} -> jid:remove_resource(JID);
+ _ ->
+ ?ERROR_MSG("Invalid subscriber JID in set_opts ~p", [JID]),
+ jid:remove_resource(jid:make(JID))
+ end,
+ muc_subscribers_put(
+ #subscriber{jid = BareJID,
+ nick = Nick,
+ nodes = Nodes},
+ MUCSubs)
+ end, muc_subscribers_new(), Val),
+ StateData#state{muc_subscribers = MUCSubscribers};
affiliations ->
StateData#state{affiliations = maps:from_list(Val)};
subject ->
@@ -3901,6 +3951,11 @@ set_opts([{Opt, Val} | Opts], StateData) ->
end,
StateData#state{subject = Subj};
subject_author -> StateData#state{subject_author = Val};
+ hats_users ->
+ Hats = maps:from_list(
+ lists:map(fun({U, H}) -> {U, maps:from_list(H)} end,
+ Val)),
+ StateData#state{hats_users = Hats};
_ -> StateData
end,
set_opts(Opts, NSD).
@@ -3926,12 +3981,12 @@ set_vcard_xupdate(State) ->
-spec make_opts(state()) -> [{atom(), any()}].
make_opts(StateData) ->
Config = StateData#state.config,
- Subscribers = maps:fold(
+ Subscribers = muc_subscribers_fold(
fun(_LJID, Sub, Acc) ->
[{Sub#subscriber.jid,
Sub#subscriber.nick,
Sub#subscriber.nodes}|Acc]
- end, [], StateData#state.subscribers),
+ end, [], StateData#state.muc_subscribers),
[?MAKE_CONFIG_OPT(#config.title), ?MAKE_CONFIG_OPT(#config.description),
?MAKE_CONFIG_OPT(#config.allow_change_subj),
?MAKE_CONFIG_OPT(#config.allow_query_users),
@@ -3957,6 +4012,7 @@ make_opts(StateData) ->
?MAKE_CONFIG_OPT(#config.vcard),
?MAKE_CONFIG_OPT(#config.vcard_xupdate),
?MAKE_CONFIG_OPT(#config.pubsub),
+ ?MAKE_CONFIG_OPT(#config.enable_hats),
?MAKE_CONFIG_OPT(#config.lang),
{captcha_whitelist,
(?SETS):to_list((StateData#state.config)#config.captcha_whitelist)},
@@ -3964,6 +4020,9 @@ make_opts(StateData) ->
maps:to_list(StateData#state.affiliations)},
{subject, StateData#state.subject},
{subject_author, StateData#state.subject_author},
+ {hats_users,
+ lists:map(fun({U, H}) -> {U, maps:to_list(H)} end,
+ maps:to_list(StateData#state.hats_users))},
{hibernation_time, erlang:system_time(microsecond)},
{subscribers, Subscribers}].
@@ -4013,7 +4072,8 @@ destroy_room(DEl, StateData) ->
send_wrapped(jid:replace_resource(StateData#state.jid, Nick),
Info#user.jid, Packet,
?NS_MUCSUB_NODES_CONFIG, StateData)
- end, ok, get_users_and_subscribers(StateData)),
+ end, ok, get_users_and_subscribers_with_node(
+ ?NS_MUCSUB_NODES_CONFIG, StateData)),
forget_room(StateData),
{result, undefined, stop}.
@@ -4053,6 +4113,7 @@ maybe_forget_room(StateData) ->
make_disco_info(_From, StateData) ->
Config = StateData#state.config,
Feats = [?NS_VCARD, ?NS_MUC, ?NS_DISCO_INFO, ?NS_DISCO_ITEMS,
+ ?NS_COMMANDS,
?CONFIG_OPT_TO_FEATURE((Config#config.public),
<<"muc_public">>, <<"muc_hidden">>),
?CONFIG_OPT_TO_FEATURE((Config#config.persistent),
@@ -4093,6 +4154,77 @@ process_iq_disco_info(From, #iq{type = get, lang = Lang,
Extras = iq_disco_info_extras(Lang, StateData, false),
{result, DiscoInfo#disco_info{xdata = [Extras]}};
process_iq_disco_info(From, #iq{type = get, lang = Lang,
+ sub_els = [#disco_info{node = ?NS_COMMANDS}]},
+ StateData) ->
+ case (StateData#state.config)#config.enable_hats andalso
+ is_admin(From, StateData)
+ of
+ true ->
+ {result,
+ #disco_info{
+ identities = [#identity{category = <<"automation">>,
+ type = <<"command-list">>,
+ name = translate:translate(
+ Lang, ?T("Commands"))}]}};
+ false ->
+ Txt = ?T("Node not found"),
+ {error, xmpp:err_item_not_found(Txt, Lang)}
+ end;
+process_iq_disco_info(From, #iq{type = get, lang = Lang,
+ sub_els = [#disco_info{node = ?MUC_HAT_ADD_CMD}]},
+ StateData) ->
+ case (StateData#state.config)#config.enable_hats andalso
+ is_admin(From, StateData)
+ of
+ true ->
+ {result,
+ #disco_info{
+ identities = [#identity{category = <<"automation">>,
+ type = <<"command-node">>,
+ name = translate:translate(
+ Lang, ?T("Add a hat to a user"))}],
+ features = [?NS_COMMANDS]}};
+ false ->
+ Txt = ?T("Node not found"),
+ {error, xmpp:err_item_not_found(Txt, Lang)}
+ end;
+process_iq_disco_info(From, #iq{type = get, lang = Lang,
+ sub_els = [#disco_info{node = ?MUC_HAT_REMOVE_CMD}]},
+ StateData) ->
+ case (StateData#state.config)#config.enable_hats andalso
+ is_admin(From, StateData)
+ of
+ true ->
+ {result,
+ #disco_info{
+ identities = [#identity{category = <<"automation">>,
+ type = <<"command-node">>,
+ name = translate:translate(
+ Lang, ?T("Remove a hat from a user"))}],
+ features = [?NS_COMMANDS]}};
+ false ->
+ Txt = ?T("Node not found"),
+ {error, xmpp:err_item_not_found(Txt, Lang)}
+ end;
+process_iq_disco_info(From, #iq{type = get, lang = Lang,
+ sub_els = [#disco_info{node = ?MUC_HAT_LIST_CMD}]},
+ StateData) ->
+ case (StateData#state.config)#config.enable_hats andalso
+ is_admin(From, StateData)
+ of
+ true ->
+ {result,
+ #disco_info{
+ identities = [#identity{category = <<"automation">>,
+ type = <<"command-node">>,
+ name = translate:translate(
+ Lang, ?T("List users with hats"))}],
+ features = [?NS_COMMANDS]}};
+ false ->
+ Txt = ?T("Node not found"),
+ {error, xmpp:err_item_not_found(Txt, Lang)}
+ end;
+process_iq_disco_info(From, #iq{type = get, lang = Lang,
sub_els = [#disco_info{node = Node}]},
StateData) ->
try
@@ -4172,6 +4304,46 @@ process_iq_disco_items(From, #iq{type = get, sub_els = [#disco_items{node = <<>>
{result, #disco_items{}}
end
end;
+process_iq_disco_items(From, #iq{type = get, lang = Lang,
+ sub_els = [#disco_items{node = ?NS_COMMANDS}]},
+ StateData) ->
+ case (StateData#state.config)#config.enable_hats andalso
+ is_admin(From, StateData)
+ of
+ true ->
+ {result,
+ #disco_items{
+ items = [#disco_item{jid = StateData#state.jid,
+ node = ?MUC_HAT_ADD_CMD,
+ name = translate:translate(
+ Lang, ?T("Add a hat to a user"))},
+ #disco_item{jid = StateData#state.jid,
+ node = ?MUC_HAT_REMOVE_CMD,
+ name = translate:translate(
+ Lang, ?T("Remove a hat from a user"))},
+ #disco_item{jid = StateData#state.jid,
+ node = ?MUC_HAT_LIST_CMD,
+ name = translate:translate(
+ Lang, ?T("List users with hats"))}]}};
+ false ->
+ Txt = ?T("Node not found"),
+ {error, xmpp:err_item_not_found(Txt, Lang)}
+ end;
+process_iq_disco_items(From, #iq{type = get, lang = Lang,
+ sub_els = [#disco_items{node = Node}]},
+ StateData)
+ when Node == ?MUC_HAT_ADD_CMD;
+ Node == ?MUC_HAT_REMOVE_CMD;
+ Node == ?MUC_HAT_LIST_CMD ->
+ case (StateData#state.config)#config.enable_hats andalso
+ is_admin(From, StateData)
+ of
+ true ->
+ {result, #disco_items{}};
+ false ->
+ Txt = ?T("Node not found"),
+ {error, xmpp:err_item_not_found(Txt, Lang)}
+ end;
process_iq_disco_items(_From, #iq{lang = Lang}, _StateData) ->
Txt = ?T("Node not found"),
{error, xmpp:err_item_not_found(Txt, Lang)}.
@@ -4248,30 +4420,35 @@ process_iq_mucsub(From,
sub_els = [#muc_subscribe{nick = Nick}]} = Packet,
StateData) ->
LBareJID = jid:tolower(jid:remove_resource(From)),
- try maps:get(LBareJID, StateData#state.subscribers) of
+ try muc_subscribers_get(LBareJID, StateData#state.muc_subscribers) of
#subscriber{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, _} ->
+ case nick_collision(From, Nick, StateData) of
+ true ->
ErrText = ?T("That nickname is already in use by another occupant"),
{error, xmpp:err_conflict(ErrText, Lang)};
- {_, false} ->
- Err = case Nick of
- <<>> ->
- xmpp:err_jid_malformed(?T("Nickname can't be empty"),
- Lang);
- _ ->
- xmpp:err_conflict(?T("That nickname is registered"
- " by another person"), Lang)
- end,
- {error, Err};
- _ ->
- NewStateData = set_subscriber(From, Nick, Nodes, StateData),
- {result, subscribe_result(Packet), NewStateData}
- end;
+ false ->
+ case mod_muc:can_use_nick(StateData#state.server_host,
+ StateData#state.host,
+ From, Nick) of
+ false ->
+ Err = case Nick of
+ <<>> ->
+ xmpp:err_jid_malformed(
+ ?T("Nickname can't be empty"),
+ Lang);
+ _ ->
+ xmpp:err_conflict(
+ ?T("That nickname is registered"
+ " by another person"), Lang)
+ end,
+ {error, Err};
+ true ->
+ NewStateData =
+ set_subscriber(From, Nick, Nodes, StateData),
+ {result, subscribe_result(Packet), NewStateData}
+ end
+ end;
#subscriber{} ->
Nodes = get_subscription_nodes(Packet),
NewStateData = set_subscriber(From, Nick, Nodes, StateData),
@@ -4298,12 +4475,9 @@ process_iq_mucsub(From, #iq{type = set, sub_els = [#muc_unsubscribe{}]},
#state{room = Room, host = Host, server_host = ServerHost} = StateData) ->
BareJID = jid:remove_resource(From),
LBareJID = jid:tolower(BareJID),
- try maps:get(LBareJID, StateData#state.subscribers) of
- #subscriber{nick = Nick} ->
- Nicks = maps:remove(Nick, StateData#state.subscriber_nicks),
- Subscribers = maps:remove(LBareJID, StateData#state.subscribers),
- NewStateData = StateData#state{subscribers = Subscribers,
- subscriber_nicks = Nicks},
+ try muc_subscribers_remove_exn(LBareJID, StateData#state.muc_subscribers) of
+ {MUCSubscribers, #subscriber{nick = Nick}} ->
+ NewStateData = StateData#state{muc_subscribers = MUCSubscribers},
store_room(NewStateData, [{del_subscription, LBareJID}]),
send_subscriptions_change_notifications(BareJID, Nick, unsubscribe, StateData),
ejabberd_hooks:run(muc_unsubscribed, ServerHost, [ServerHost, Room, Host, BareJID]),
@@ -4326,7 +4500,7 @@ process_iq_mucsub(From, #iq{type = get, lang = Lang,
true ->
ShowJid = IsModerator orelse
(StateData#state.config)#config.anonymous == false,
- Subs = maps:fold(
+ Subs = muc_subscribers_fold(
fun(_, #subscriber{jid = J, nick = N, nodes = Nodes}, Acc) ->
case ShowJid of
true ->
@@ -4334,7 +4508,7 @@ process_iq_mucsub(From, #iq{type = get, lang = Lang,
_ ->
[#muc_subscription{nick = N, events = Nodes}|Acc]
end
- end, [], StateData#state.subscribers),
+ end, [], StateData#state.muc_subscribers),
{result, #muc_subscriptions{list = Subs}, StateData};
_ ->
Txt = ?T("Moderator privileges required"),
@@ -4347,8 +4521,7 @@ process_iq_mucsub(_From, #iq{type = get, lang = Lang}, _StateData) ->
-spec remove_subscriptions(state()) -> state().
remove_subscriptions(StateData) ->
if not (StateData#state.config)#config.allow_subscription ->
- StateData#state{subscribers = #{},
- subscriber_nicks = #{}};
+ StateData#state{muc_subscribers = muc_subscribers_new()};
true ->
StateData
end.
@@ -4413,6 +4586,271 @@ get_mucroom_disco_items(StateData) ->
end, [], StateData#state.nicks),
#disco_items{items = Items}.
+-spec process_iq_adhoc(jid(), iq(), state()) ->
+ {result, adhoc_command()} |
+ {result, adhoc_command(), state()} |
+ {error, stanza_error()}.
+process_iq_adhoc(_From, #iq{type = get}, _StateData) ->
+ {error, xmpp:err_bad_request()};
+process_iq_adhoc(From, #iq{type = set, lang = Lang1,
+ sub_els = [#adhoc_command{} = Request]},
+ StateData) ->
+ % Ad-Hoc Commands are used only for Hats here
+ case (StateData#state.config)#config.enable_hats andalso
+ is_admin(From, StateData)
+ of
+ true ->
+ #adhoc_command{lang = Lang2, node = Node,
+ action = Action, xdata = XData} = Request,
+ Lang = case Lang2 of
+ <<"">> -> Lang1;
+ _ -> Lang2
+ end,
+ case {Node, Action} of
+ {_, cancel} ->
+ {result,
+ xmpp_util:make_adhoc_response(
+ Request,
+ #adhoc_command{status = canceled, lang = Lang,
+ node = Node})};
+ {?MUC_HAT_ADD_CMD, execute} ->
+ Form =
+ #xdata{
+ title = translate:translate(
+ Lang, ?T("Add a hat to a user")),
+ type = form,
+ fields =
+ [#xdata_field{
+ type = 'jid-single',
+ label = translate:translate(Lang, ?T("Jabber ID")),
+ required = true,
+ var = <<"jid">>},
+ #xdata_field{
+ type = 'text-single',
+ label = translate:translate(Lang, ?T("Hat title")),
+ var = <<"hat_title">>},
+ #xdata_field{
+ type = 'text-single',
+ label = translate:translate(Lang, ?T("Hat URI")),
+ required = true,
+ var = <<"hat_uri">>}
+ ]},
+ {result,
+ xmpp_util:make_adhoc_response(
+ Request,
+ #adhoc_command{
+ status = executing,
+ xdata = Form})};
+ {?MUC_HAT_ADD_CMD, complete} when XData /= undefined ->
+ JID = try
+ jid:decode(hd(xmpp_util:get_xdata_values(
+ <<"jid">>, XData)))
+ catch _:_ -> error
+ end,
+ URI = try
+ hd(xmpp_util:get_xdata_values(
+ <<"hat_uri">>, XData))
+ catch _:_ -> error
+ end,
+ Title = case xmpp_util:get_xdata_values(
+ <<"hat_title">>, XData) of
+ [] -> <<"">>;
+ [T] -> T
+ end,
+ if
+ (JID /= error) and (URI /= error) ->
+ case add_hat(JID, URI, Title, StateData) of
+ {ok, NewStateData} ->
+ store_room(NewStateData),
+ send_update_presence(
+ JID, NewStateData, StateData),
+ {result,
+ xmpp_util:make_adhoc_response(
+ Request,
+ #adhoc_command{status = completed}),
+ NewStateData};
+ {error, size_limit} ->
+ Txt = ?T("Hats limit exceeded"),
+ {error, xmpp:err_not_allowed(Txt, Lang)}
+ end;
+ true ->
+ {error, xmpp:err_bad_request()}
+ end;
+ {?MUC_HAT_ADD_CMD, complete} ->
+ {error, xmpp:err_bad_request()};
+ {?MUC_HAT_ADD_CMD, _} ->
+ Txt = ?T("Incorrect value of 'action' attribute"),
+ {error, xmpp:err_bad_request(Txt, Lang)};
+ {?MUC_HAT_REMOVE_CMD, execute} ->
+ Form =
+ #xdata{
+ title = translate:translate(
+ Lang, ?T("Remove a hat from a user")),
+ type = form,
+ fields =
+ [#xdata_field{
+ type = 'jid-single',
+ label = translate:translate(Lang, ?T("Jabber ID")),
+ required = true,
+ var = <<"jid">>},
+ #xdata_field{
+ type = 'text-single',
+ label = translate:translate(Lang, ?T("Hat URI")),
+ required = true,
+ var = <<"hat_uri">>}
+ ]},
+ {result,
+ xmpp_util:make_adhoc_response(
+ Request,
+ #adhoc_command{
+ status = executing,
+ xdata = Form})};
+ {?MUC_HAT_REMOVE_CMD, complete} when XData /= undefined ->
+ JID = try
+ jid:decode(hd(xmpp_util:get_xdata_values(
+ <<"jid">>, XData)))
+ catch _:_ -> error
+ end,
+ URI = try
+ hd(xmpp_util:get_xdata_values(
+ <<"hat_uri">>, XData))
+ catch _:_ -> error
+ end,
+ if
+ (JID /= error) and (URI /= error) ->
+ NewStateData = del_hat(JID, URI, StateData),
+ store_room(NewStateData),
+ send_update_presence(
+ JID, NewStateData, StateData),
+ {result,
+ xmpp_util:make_adhoc_response(
+ Request,
+ #adhoc_command{status = completed}),
+ NewStateData};
+ true ->
+ {error, xmpp:err_bad_request()}
+ end;
+ {?MUC_HAT_REMOVE_CMD, complete} ->
+ {error, xmpp:err_bad_request()};
+ {?MUC_HAT_REMOVE_CMD, _} ->
+ Txt = ?T("Incorrect value of 'action' attribute"),
+ {error, xmpp:err_bad_request(Txt, Lang)};
+ {?MUC_HAT_LIST_CMD, execute} ->
+ Hats = get_all_hats(StateData),
+ Items =
+ lists:map(
+ fun({JID, URI, Title}) ->
+ [#xdata_field{
+ var = <<"jid">>,
+ values = [jid:encode(JID)]},
+ #xdata_field{
+ var = <<"hat_title">>,
+ values = [URI]},
+ #xdata_field{
+ var = <<"hat_uri">>,
+ values = [Title]}]
+ end, Hats),
+ Form =
+ #xdata{
+ title = translate:translate(
+ Lang, ?T("List of users with hats")),
+ type = result,
+ reported =
+ [#xdata_field{
+ label = translate:translate(Lang, ?T("Jabber ID")),
+ var = <<"jid">>},
+ #xdata_field{
+ label = translate:translate(Lang, ?T("Hat title")),
+ var = <<"hat_title">>},
+ #xdata_field{
+ label = translate:translate(Lang, ?T("Hat URI")),
+ var = <<"hat_uri">>}],
+ items = Items},
+ {result,
+ xmpp_util:make_adhoc_response(
+ Request,
+ #adhoc_command{
+ status = completed,
+ xdata = Form})};
+ {?MUC_HAT_LIST_CMD, _} ->
+ Txt = ?T("Incorrect value of 'action' attribute"),
+ {error, xmpp:err_bad_request(Txt, Lang)};
+ _ ->
+ {error, xmpp:err_item_not_found()}
+ end;
+ _ ->
+ {error, xmpp:err_forbidden()}
+ end.
+
+-spec add_hat(jid(), binary(), binary(), state()) ->
+ {ok, state()} | {error, size_limit}.
+add_hat(JID, URI, Title, StateData) ->
+ Hats = StateData#state.hats_users,
+ LJID = jid:remove_resource(jid:tolower(JID)),
+ UserHats = maps:get(LJID, Hats, #{}),
+ UserHats2 = maps:put(URI, Title, UserHats),
+ USize = maps:size(UserHats2),
+ if
+ USize =< ?MAX_HATS_PER_USER ->
+ Hats2 = maps:put(LJID, UserHats2, Hats),
+ Size = maps:size(Hats2),
+ if
+ Size =< ?MAX_HATS_USERS ->
+ {ok, StateData#state{hats_users = Hats2}};
+ true ->
+ {error, size_limit}
+ end;
+ true ->
+ {error, size_limit}
+ end.
+
+-spec del_hat(jid(), binary(), state()) -> state().
+del_hat(JID, URI, StateData) ->
+ Hats = StateData#state.hats_users,
+ LJID = jid:remove_resource(jid:tolower(JID)),
+ UserHats = maps:get(LJID, Hats, #{}),
+ UserHats2 = maps:remove(URI, UserHats),
+ Hats2 =
+ case maps:size(UserHats2) of
+ 0 ->
+ maps:remove(LJID, Hats);
+ _ ->
+ maps:put(LJID, UserHats2, Hats)
+ end,
+ StateData#state{hats_users = Hats2}.
+
+-spec get_all_hats(state()) -> list({jid(), binary(), binary()}).
+get_all_hats(StateData) ->
+ lists:flatmap(
+ fun({LJID, H}) ->
+ JID = jid:make(LJID),
+ lists:map(fun({URI, Title}) -> {JID, URI, Title} end,
+ maps:to_list(H))
+ end,
+ maps:to_list(StateData#state.hats_users)).
+
+-spec add_presence_hats(jid(), #presence{}, state()) -> #presence{}.
+add_presence_hats(JID, Pres, StateData) ->
+ case (StateData#state.config)#config.enable_hats of
+ true ->
+ Hats = StateData#state.hats_users,
+ LJID = jid:remove_resource(jid:tolower(JID)),
+ UserHats = maps:get(LJID, Hats, #{}),
+ case maps:size(UserHats) of
+ 0 -> Pres;
+ _ ->
+ Items =
+ lists:map(fun({URI, Title}) ->
+ #muc_hat{uri = URI, title = Title}
+ end,
+ maps:to_list(UserHats)),
+ xmpp:set_subtag(Pres,
+ #muc_hats{hats = Items})
+ end;
+ false ->
+ Pres
+ end.
+
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Voice request support
@@ -4597,7 +5035,7 @@ store_room(StateData, ChangesHints) ->
true ->
ok;
_ ->
- erlang:put(muc_subscribers, StateData#state.subscribers)
+ erlang:put(muc_subscribers, StateData#state.muc_subscribers#muc_subscribers.subscribers)
end,
ShouldStore = case (StateData#state.config)#config.persistent of
true ->
@@ -4611,7 +5049,15 @@ store_room(StateData, ChangesHints) ->
end
end,
if ShouldStore ->
- store_room_no_checks(StateData, ChangesHints);
+ case erlang:function_exported(Mod, store_changes, 4) of
+ true when ChangesHints /= [] ->
+ mod_muc:store_changes(
+ StateData#state.server_host,
+ StateData#state.host, StateData#state.room,
+ ChangesHints);
+ _ ->
+ store_room_no_checks(StateData, ChangesHints)
+ end;
true ->
ok
end.
@@ -4625,22 +5071,19 @@ store_room_no_checks(StateData, ChangesHints) ->
-spec send_subscriptions_change_notifications(jid(), binary(), subscribe|unsubscribe, state()) -> ok.
send_subscriptions_change_notifications(From, Nick, Type, State) ->
{WJ, WN} =
- maps:fold(
- fun(_, #subscriber{nodes = Nodes, jid = JID}, {WithJid, WithNick} = Res) ->
- case lists:member(?NS_MUCSUB_NODES_SUBSCRIBERS, Nodes) of
- true ->
- case (State#state.config)#config.anonymous == false orelse
- get_role(JID, State) == moderator orelse
- get_default_role(get_affiliation(JID, State), State) == moderator of
- true ->
- {[JID | WithJid], WithNick};
- _ ->
- {WithJid, [JID | WithNick]}
- end;
- false ->
- Res
- end
- end, {[], []}, State#state.subscribers),
+ maps:fold(
+ fun(_, #subscriber{jid = JID}, {WithJid, WithNick}) ->
+ case (State#state.config)#config.anonymous == false orelse
+ get_role(JID, State) == moderator orelse
+ get_default_role(get_affiliation(JID, State), State) == moderator of
+ true ->
+ {[JID | WithJid], WithNick};
+ _ ->
+ {WithJid, [JID | WithNick]}
+ end
+ end, {[], []},
+ muc_subscribers_get_by_node(?NS_MUCSUB_NODES_SUBSCRIBERS,
+ State#state.muc_subscribers)),
if WJ /= [] ->
Payload1 = case Type of
subscribe -> #muc_subscribe{jid = From, nick = Nick};
@@ -4654,7 +5097,7 @@ send_subscriptions_change_notifications(From, Nick, Type, State) ->
id = p1_rand:get_string(),
sub_els = [Payload1]}]}}]},
ejabberd_router_multicast:route_multicast(State#state.jid, State#state.server_host,
- WJ, Packet1, true);
+ WJ, Packet1, false);
true -> ok
end,
if WN /= [] ->
@@ -4670,7 +5113,7 @@ send_subscriptions_change_notifications(From, Nick, Type, State) ->
id = p1_rand:get_string(),
sub_els = [Payload2]}]}}]},
ejabberd_router_multicast:route_multicast(State#state.jid, State#state.server_host,
- WN, Packet2, true);
+ WN, Packet2, false);
true -> ok
end.
@@ -4684,7 +5127,7 @@ send_wrapped(From, To, Packet, Node, State) ->
_ -> false
end,
if IsOffline ->
- try maps:get(LBareTo, State#state.subscribers) of
+ try muc_subscribers_get(LBareTo, State#state.muc_subscribers) of
#subscriber{nodes = Nodes, jid = JID} ->
case lists:member(Node, Nodes) of
true ->
@@ -4751,12 +5194,13 @@ send_wrapped_multiple(From, Users, Packet, Node, State) ->
IsOffline = LP == undefined,
if IsOffline ->
LBareTo = jid:tolower(jid:remove_resource(To)),
- case maps:find(LBareTo, State#state.subscribers) of
+ case muc_subscribers_find(LBareTo, State#state.muc_subscribers) of
{ok, #subscriber{nodes = Nodes}} ->
case lists:member(Node, Nodes) of
true ->
{Direct, [To | Wrapped]};
_ ->
+ %% TODO: check that this branch is never called
Res
end;
_ ->
@@ -4810,6 +5254,90 @@ send_wrapped_multiple(From, Users, Packet, Node, State) ->
Wra, NewPacket2, true)
end.
+%%%----------------------------------------------------------------------
+%%% #muc_subscribers API
+%%%----------------------------------------------------------------------
+
+-spec muc_subscribers_new() -> #muc_subscribers{}.
+muc_subscribers_new() ->
+ #muc_subscribers{}.
+
+-spec muc_subscribers_get(ljid(), #muc_subscribers{}) -> #subscriber{}.
+muc_subscribers_get({_, _, _} = LJID, MUCSubscribers) ->
+ maps:get(LJID, MUCSubscribers#muc_subscribers.subscribers).
+
+-spec muc_subscribers_find(ljid(), #muc_subscribers{}) ->
+ {ok, #subscriber{}} | error.
+muc_subscribers_find({_, _, _} = LJID, MUCSubscribers) ->
+ maps:find(LJID, MUCSubscribers#muc_subscribers.subscribers).
+
+-spec muc_subscribers_is_key(ljid(), #muc_subscribers{}) -> boolean().
+muc_subscribers_is_key({_, _, _} = LJID, MUCSubscribers) ->
+ maps:is_key(LJID, MUCSubscribers#muc_subscribers.subscribers).
+
+-spec muc_subscribers_size(#muc_subscribers{}) -> integer().
+muc_subscribers_size(MUCSubscribers) ->
+ maps:size(MUCSubscribers#muc_subscribers.subscribers).
+
+-spec muc_subscribers_fold(Fun, Acc, #muc_subscribers{}) -> Acc when
+ Fun :: fun((ljid(), #subscriber{}, Acc) -> Acc).
+muc_subscribers_fold(Fun, Init, MUCSubscribers) ->
+ maps:fold(Fun, Init, MUCSubscribers#muc_subscribers.subscribers).
+
+-spec muc_subscribers_get_by_nick(binary(), #muc_subscribers{}) -> [#subscriber{}].
+muc_subscribers_get_by_nick(Nick, MUCSubscribers) ->
+ maps:get(Nick, MUCSubscribers#muc_subscribers.subscriber_nicks, []).
+
+-spec muc_subscribers_get_by_node(binary(), #muc_subscribers{}) -> subscribers().
+muc_subscribers_get_by_node(Node, MUCSubscribers) ->
+ maps:get(Node, MUCSubscribers#muc_subscribers.subscriber_nodes, #{}).
+
+-spec muc_subscribers_remove_exn(ljid(), #muc_subscribers{}) ->
+ {#muc_subscribers{}, #subscriber{}}.
+muc_subscribers_remove_exn({_, _, _} = LJID, MUCSubscribers) ->
+ #muc_subscribers{subscribers = Subs,
+ subscriber_nicks = SubNicks,
+ subscriber_nodes = SubNodes} = MUCSubscribers,
+ Subscriber = maps:get(LJID, Subs),
+ #subscriber{nick = Nick, nodes = Nodes} = Subscriber,
+ NewSubNicks = maps:remove(Nick, SubNicks),
+ NewSubs = maps:remove(LJID, Subs),
+ NewSubNodes =
+ lists:foldl(
+ fun(Node, Acc) ->
+ NodeSubs = maps:get(Node, Acc, #{}),
+ NodeSubs2 = maps:remove(LJID, NodeSubs),
+ maps:put(Node, NodeSubs2, Acc)
+ end, SubNodes, Nodes),
+ {#muc_subscribers{subscribers = NewSubs,
+ subscriber_nicks = NewSubNicks,
+ subscriber_nodes = NewSubNodes}, Subscriber}.
+
+-spec muc_subscribers_put(#subscriber{}, #muc_subscribers{}) ->
+ #muc_subscribers{}.
+muc_subscribers_put(Subscriber, MUCSubscribers) ->
+ #subscriber{jid = JID,
+ nick = Nick,
+ nodes = Nodes} = Subscriber,
+ #muc_subscribers{subscribers = Subs,
+ subscriber_nicks = SubNicks,
+ subscriber_nodes = SubNodes} = MUCSubscribers,
+ LJID = jid:tolower(JID),
+ NewSubs = maps:put(LJID, Subscriber, Subs),
+ NewSubNicks = maps:put(Nick, [LJID], SubNicks),
+ NewSubNodes =
+ lists:foldl(
+ fun(Node, Acc) ->
+ NodeSubs = maps:get(Node, Acc, #{}),
+ NodeSubs2 = maps:put(LJID, Subscriber, NodeSubs),
+ maps:put(Node, NodeSubs2, Acc)
+ end, SubNodes, Nodes),
+ #muc_subscribers{subscribers = NewSubs,
+ subscriber_nicks = NewSubNicks,
+ subscriber_nodes = NewSubNodes}.
+
+
+
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Detect messange stanzas that don't have meaningful content
-spec has_body_or_subject(message()) -> boolean().
diff --git a/src/mod_muc_sql.erl b/src/mod_muc_sql.erl
index 569cfac49..1310cde7b 100644
--- a/src/mod_muc_sql.erl
+++ b/src/mod_muc_sql.erl
@@ -29,7 +29,8 @@
-behaviour(mod_muc_room).
%% API
--export([init/2, store_room/5, restore_room/3, forget_room/3,
+-export([init/2, store_room/5, store_changes/4,
+ restore_room/3, forget_room/3,
can_use_nick/4, get_rooms/2, get_nick/3, set_nick/4,
import/3, export/1]).
-export([register_online_room/4, unregister_online_room/4, find_online_room/3,
@@ -83,6 +84,12 @@ store_room(LServer, Host, Name, Opts, ChangesHints) ->
end,
ejabberd_sql:sql_transaction(LServer, F).
+store_changes(LServer, Host, Name, Changes) ->
+ F = fun () ->
+ [change_room(Host, Name, Change) || Change <- Changes]
+ end,
+ ejabberd_sql:sql_transaction(LServer, F).
+
change_room(Host, Room, {add_subscription, JID, Nick, Nodes}) ->
SJID = jid:encode(JID),
SNodes = misc:term_to_expr(Nodes),
@@ -185,13 +192,20 @@ get_rooms(LServer, Host) ->
{selected, Subs} ->
SubsD = lists:foldl(
fun({Room, Jid, Nick, Nodes}, Dict) ->
- dict:append(Room, {jid:decode(Jid),
- Nick, ejabberd_sql:decode_term(Nodes)}, Dict)
- end, dict:new(), Subs),
+ Sub = {jid:decode(Jid),
+ Nick, ejabberd_sql:decode_term(Nodes)},
+ maps:update_with(
+ Room,
+ fun(SubAcc) ->
+ [Sub | SubAcc]
+ end,
+ [Sub],
+ Dict)
+ end, maps:new(), Subs),
lists:map(
fun({Room, Opts}) ->
OptsD = ejabberd_sql:decode_term(Opts),
- OptsD2 = case {dict:find(Room, SubsD), lists:keymember(subscribers, 1, OptsD)} of
+ OptsD2 = case {maps:find(Room, SubsD), lists:keymember(subscribers, 1, OptsD)} of
{_, true} ->
store_room(LServer, Host, Room, mod_muc:opts_to_binary(OptsD), undefined),
OptsD;
diff --git a/src/mod_multicast.erl b/src/mod_multicast.erl
index 161d3a4c4..fa076da70 100644
--- a/src/mod_multicast.erl
+++ b/src/mod_multicast.erl
@@ -35,7 +35,7 @@
%% API
-export([start/2, stop/1, reload/3,
- user_send_packet/1]).
+ user_send_packet/1]).
%% gen_server callbacks
-export([init/1, handle_info/2, handle_call/3,
@@ -51,11 +51,6 @@
response,
ts :: integer()}).
--record(dest, {jid_string :: binary() | none,
- jid_jid :: jid() | undefined,
- type :: bcc | cc | noreply | ofrom | replyroom | replyto | to,
- address :: address()}).
-
-type limit_value() :: {default | custom, integer()}.
-record(limits, {message :: limit_value(),
presence :: limit_value()}).
@@ -63,14 +58,6 @@
-record(service_limits, {local :: #limits{},
remote :: #limits{}}).
--type routing() :: route_single | {route_multicast, binary(), #service_limits{}}.
-
--record(group, {server :: binary(),
- dests :: [#dest{}],
- multicast :: routing() | undefined,
- others :: [address()],
- addresses :: [address()]}).
-
-record(state, {lserver :: binary(),
lservice :: binary(),
access :: atom(),
@@ -117,7 +104,7 @@ reload(LServerS, NewOpts, OldOpts) ->
user_send_packet({#presence{} = Packet, C2SState} = Acc) ->
case xmpp:get_subtag(Packet, #addresses{}) of
#addresses{list = Addresses} ->
- {ToDeliver, _Delivereds} = split_addresses_todeliver(Addresses),
+ {CC, BCC, _Invalid, _Delivered} = partition_addresses(Addresses),
NewState =
lists:foldl(
fun(Address, St) ->
@@ -138,7 +125,7 @@ user_send_packet({#presence{} = Packet, C2SState} = Acc) ->
undefined ->
St
end
- end, C2SState, ToDeliver),
+ end, C2SState, CC ++ BCC),
{Packet, NewState};
false ->
Acc
@@ -308,19 +295,10 @@ iq_vcard(Lang, State) ->
%%%-------------------------
-spec route_trusted(binary(), binary(), jid(), [jid()], stanza()) -> 'ok'.
-route_trusted(LServiceS, LServerS, FromJID,
- Destinations, Packet) ->
- Packet_stripped = Packet,
- Delivereds = [],
- Dests2 = lists:map(
- fun(D) ->
- #dest{jid_string = jid:encode(D),
- jid_jid = D, type = bcc,
- address = #address{type = bcc, jid = D}}
- end, Destinations),
- Groups = group_dests(Dests2),
- route_common(LServerS, LServiceS, FromJID, Groups,
- Delivereds, Packet_stripped).
+route_trusted(LServiceS, LServerS, FromJID, Destinations, Packet) ->
+ Addresses = [#address{type = bcc, jid = D} || D <- Destinations],
+ Groups = group_by_destinations(Addresses, #{}),
+ route_grouped(LServerS, LServiceS, FromJID, Groups, [], Packet).
-spec route_untrusted(binary(), binary(), atom(), #service_limits{}, stanza()) -> 'ok'.
route_untrusted(LServiceS, LServerS, Access, SLimits, Packet) ->
@@ -356,50 +334,88 @@ route_untrusted(LServiceS, LServerS, Access, SLimits, Packet) ->
route_untrusted2(LServiceS, LServerS, Access, SLimits, Packet) ->
FromJID = xmpp:get_from(Packet),
ok = check_access(LServerS, Access, FromJID),
- {ok, Packet_stripped, Addresses} = strip_addresses_element(Packet),
- {To_deliver, Delivereds} = split_addresses_todeliver(Addresses),
- Dests = convert_dest_record(To_deliver),
- {Dests2, Not_jids} = split_dests_jid(Dests),
- report_not_jid(FromJID, Packet, Not_jids),
- ok = check_limit_dests(SLimits, FromJID, Packet, Dests2),
- Groups = group_dests(Dests2),
+ {ok, PacketStripped, Addresses} = strip_addresses_element(Packet),
+ {CC, BCC, NotJids, Rest} = partition_addresses(Addresses),
+ report_not_jid(FromJID, Packet, NotJids),
+ ok = check_limit_dests(SLimits, FromJID, Packet, length(CC) + length(BCC)),
+ Groups0 = group_by_destinations(CC, #{}),
+ Groups = group_by_destinations(BCC, Groups0),
ok = check_relay(FromJID#jid.server, LServerS, Groups),
- route_common(LServerS, LServiceS, FromJID, Groups,
- Delivereds, Packet_stripped).
-
--spec route_common(binary(), binary(), jid(), [#group{}],
- [address()], stanza()) -> 'ok'.
-route_common(LServerS, LServiceS, FromJID, Groups,
- Delivereds, Packet_stripped) ->
- Groups2 = look_cached_servers(LServerS, LServiceS, Groups),
- Groups3 = build_others_xml(Groups2),
- Groups4 = add_addresses(Delivereds, Groups3),
- AGroups = decide_action_groups(Groups4),
- act_groups(FromJID, Packet_stripped, LServiceS,
- AGroups).
-
--spec act_groups(jid(), stanza(), binary(), [{routing(), #group{}}]) -> 'ok'.
-act_groups(FromJID, Packet_stripped, LServiceS, AGroups) ->
+ route_grouped(LServerS, LServiceS, FromJID, Groups, Rest, PacketStripped).
+
+-spec mark_as_delivered([address()]) -> [address()].
+mark_as_delivered(Addresses) ->
+ [A#address{delivered = true} || A <- Addresses].
+
+-spec route_individual(jid(), [address()], [address()], [address()], stanza()) -> ok.
+route_individual(From, CC, BCC, Other, Packet) ->
+ CCDelivered = mark_as_delivered(CC),
+ Addresses = CCDelivered ++ Other,
+ PacketWithAddresses = xmpp:append_subtags(Packet, [#addresses{list = Addresses}]),
lists:foreach(
- fun(AGroup) ->
- perform(FromJID, Packet_stripped, LServiceS,
- AGroup)
- end, AGroups).
-
--spec perform(jid(), stanza(), binary(),
- {routing(), #group{}}) -> 'ok'.
-perform(From, Packet, _,
- {route_single, Group}) ->
+ fun(#address{jid = To}) ->
+ ejabberd_router:route(xmpp:set_from_to(PacketWithAddresses, From, To))
+ end, CC),
lists:foreach(
- fun(ToUser) ->
- Group_others = strip_other_bcc(ToUser, Group#group.others),
- route_packet(From, ToUser, Packet,
- Group_others, Group#group.addresses)
- end, Group#group.dests);
-perform(From, Packet, _,
- {{route_multicast, JID, RLimits}, Group}) ->
- route_packet_multicast(From, JID, Packet,
- Group#group.dests, Group#group.addresses, RLimits).
+ fun(#address{jid = To} = Address) ->
+ Packet2 = case Addresses of
+ [] ->
+ Packet;
+ _ ->
+ xmpp:append_subtags(Packet, [#addresses{list = [Address | Addresses]}])
+ end,
+ ejabberd_router:route(xmpp:set_from_to(Packet2, From, To))
+ end, BCC).
+
+-spec route_chunk(jid(), jid(), stanza(), [address()]) -> ok.
+route_chunk(From, To, Packet, Addresses) ->
+ PacketWithAddresses = xmpp:append_subtags(Packet, [#addresses{list = Addresses}]),
+ ejabberd_router:route(xmpp:set_from_to(PacketWithAddresses, From, To)).
+
+-spec route_in_chunks(jid(), jid(), stanza(), integer(), [address()], [address()], [address()]) -> ok.
+route_in_chunks(_From, _To, _Packet, _Limit, [], [], _) ->
+ ok;
+route_in_chunks(From, To, Packet, Limit, CC, BCC, RestOfAddresses) when length(CC) > Limit ->
+ {Chunk, Rest} = lists:split(Limit, CC),
+ route_chunk(From, To, Packet, Chunk ++ RestOfAddresses),
+ route_in_chunks(From, To, Packet, Limit, Rest, BCC, RestOfAddresses);
+route_in_chunks(From, To, Packet, Limit, [], BCC, RestOfAddresses) when length(BCC) > Limit ->
+ {Chunk, Rest} = lists:split(Limit, BCC),
+ route_chunk(From, To, Packet, Chunk ++ RestOfAddresses),
+ route_in_chunks(From, To, Packet, Limit, [], Rest, RestOfAddresses);
+route_in_chunks(From, To, Packet, Limit, CC, BCC, RestOfAddresses) when length(BCC) + length(CC) > Limit ->
+ {Chunk, Rest} = lists:split(Limit - length(CC), BCC),
+ route_chunk(From, To, Packet, CC ++ Chunk ++ RestOfAddresses),
+ route_in_chunks(From, To, Packet, Limit, [], Rest, RestOfAddresses);
+route_in_chunks(From, To, Packet, _Limit, CC, BCC, RestOfAddresses) ->
+ route_chunk(From, To, Packet, CC ++ BCC ++ RestOfAddresses).
+
+-spec route_multicast(jid(), jid(), [address()], [address()], [address()], stanza(), #limits{}) -> ok.
+route_multicast(From, To, CC, BCC, RestOfAddresses, Packet, Limits) ->
+ {_Type, Limit} = get_limit_number(element(1, Packet),
+ Limits),
+ route_in_chunks(From, To, Packet, Limit, CC, BCC, RestOfAddresses).
+
+-spec route_grouped(binary(), binary(), jid(), #{}, [address()], stanza()) -> ok.
+route_grouped(LServer, LService, From, Groups, RestOfAddresses, Packet) ->
+ maps:fold(
+ fun(Server, {CC, BCC}, _) ->
+ OtherCC = maps:fold(
+ fun(Server2, _, Res) when Server2 == Server ->
+ Res;
+ (_, {CC2, _}, Res) ->
+ mark_as_delivered(CC2) ++ Res
+ end, [], Groups),
+ case search_server_on_cache(Server,
+ LServer, LService,
+ {?MAXTIME_CACHE_POSITIVE,
+ ?MAXTIME_CACHE_NEGATIVE}) of
+ route_single ->
+ route_individual(From, CC, BCC, OtherCC ++ RestOfAddresses, Packet);
+ {route_multicast, Service, Limits} ->
+ route_multicast(From, Service, CC, BCC, OtherCC ++ RestOfAddresses, Packet, Limits)
+ end
+ end, ok, Groups).
%%%-------------------------
%%% Check access permission
@@ -426,244 +442,88 @@ strip_addresses_element(Packet) ->
end.
%%%-------------------------
-%%% Strip third-party bcc 'addresses'
-%%%-------------------------
-
-strip_other_bcc(#dest{jid_jid = ToUserJid}, Group_others) ->
- lists:filter(
- fun(#address{jid = JID, type = Type}) ->
- case {JID, Type} of
- {ToUserJid, bcc} -> true;
- {_, bcc} -> false;
- _ -> true
- end
- end,
- Group_others).
-
-%%%-------------------------
%%% Split Addresses
%%%-------------------------
--spec split_addresses_todeliver([address()]) -> {[address()], [address()]}.
-split_addresses_todeliver(Addresses) ->
- lists:partition(
- fun(#address{delivered = true}) ->
- false;
- (#address{type = Type}) ->
- case Type of
- to -> true;
- cc -> true;
- bcc -> true;
- _ -> false
- end
- end, Addresses).
+partition_addresses(Addresses) ->
+ lists:foldl(
+ fun(#address{delivered = true} = A, {C, B, I, D}) ->
+ {C, B, I, [A | D]};
+ (#address{type = T, jid = undefined} = A, {C, B, I, D})
+ when T == to; T == cc; T == bcc ->
+ {C, B, [A | I], D};
+ (#address{type = T} = A, {C, B, I, D})
+ when T == to; T == cc ->
+ {[A | C], B, I, D};
+ (#address{type = bcc} = A, {C, B, I, D}) ->
+ {C, [A | B], I, D};
+ (A, {C, B, I, D}) ->
+ {C, B, I, [A | D]}
+ end, {[], [], [], []}, Addresses).
%%%-------------------------
%%% Check does not exceed limit of destinations
%%%-------------------------
--spec check_limit_dests(#service_limits{}, jid(), stanza(), [address()]) -> ok.
-check_limit_dests(SLimits, FromJID, Packet,
- Addresses) ->
+-spec check_limit_dests(#service_limits{}, jid(), stanza(), integer()) -> ok.
+check_limit_dests(SLimits, FromJID, Packet, NumOfAddresses) ->
SenderT = sender_type(FromJID),
Limits = get_slimit_group(SenderT, SLimits),
- Type_of_stanza = type_of_stanza(Packet),
- {_Type, Limit_number} = get_limit_number(Type_of_stanza,
- Limits),
- case length(Addresses) > Limit_number of
+ StanzaType = type_of_stanza(Packet),
+ {_Type, Limit} = get_limit_number(StanzaType,
+ Limits),
+ case NumOfAddresses > Limit of
false -> ok;
true -> throw(etoorec)
end.
-%%%-------------------------
-%%% Convert Destination XML to record
-%%%-------------------------
-
--spec convert_dest_record([address()]) -> [#dest{}].
-convert_dest_record(Addrs) ->
- lists:map(
- fun(#address{jid = undefined, type = Type} = Addr) ->
- #dest{jid_string = none,
- type = Type, address = Addr};
- (#address{jid = JID, type = Type} = Addr) ->
- #dest{jid_string = jid:encode(JID), jid_jid = JID,
- type = Type, address = Addr}
- end, Addrs).
-
-%%%-------------------------
-%%% Split destinations by existence of JID
-%%% and send error messages for other dests
-%%%-------------------------
--spec split_dests_jid([#dest{}]) -> {[#dest{}], [#dest{}]}.
-split_dests_jid(Dests) ->
- lists:partition(fun (Dest) ->
- case Dest#dest.jid_string of
- none -> false;
- _ -> true
- end
- end,
- Dests).
-
--spec report_not_jid(jid(), stanza(), [#dest{}]) -> any().
-report_not_jid(From, Packet, Dests) ->
- Dests2 = [fxml:element_to_binary(xmpp:encode(Dest#dest.address))
- || Dest <- Dests],
- [route_error(
- xmpp:set_from_to(Packet, From, From), jid_malformed,
- str:format(?T("This service can not process the address: ~s"), [D]))
- || D <- Dests2].
+-spec report_not_jid(jid(), stanza(), [address()]) -> any().
+report_not_jid(From, Packet, Addresses) ->
+ lists:foreach(
+ fun(Address) ->
+ route_error(
+ xmpp:set_from_to(Packet, From, From), jid_malformed,
+ str:format(?T("This service can not process the address: ~s"),
+ [fxml:element_to_binary(xmpp:encode(Address))]))
+ end, Addresses).
%%%-------------------------
%%% Group destinations by their servers
%%%-------------------------
--spec group_dests([#dest{}]) -> [#group{}].
-group_dests(Dests) ->
- D = lists:foldl(fun (Dest, Dict) ->
- ServerS = (Dest#dest.jid_jid)#jid.server,
- dict:append(ServerS, Dest, Dict)
- end,
- dict:new(), Dests),
- Keys = dict:fetch_keys(D),
- [#group{server = Key, dests = dict:fetch(Key, D),
- addresses = [], others = []}
- || Key <- Keys].
-
-%%%-------------------------
-%%% Look for cached responses
-%%%-------------------------
-
-look_cached_servers(LServerS, LServiceS, Groups) ->
- [look_cached(LServerS, LServiceS, Group) || Group <- Groups].
-
-look_cached(LServerS, LServiceS, G) ->
- Maxtime_positive = (?MAXTIME_CACHE_POSITIVE),
- Maxtime_negative = (?MAXTIME_CACHE_NEGATIVE),
- Cached_response = search_server_on_cache(G#group.server,
- LServerS, LServiceS,
- {Maxtime_positive,
- Maxtime_negative}),
- G#group{multicast = Cached_response}.
-
-%%%-------------------------
-%%% Build delivered XML element
-%%%-------------------------
-
-build_others_xml(Groups) ->
- [Group#group{others =
- build_other_xml(Group#group.dests)}
- || Group <- Groups].
-
-build_other_xml(Dests) ->
- lists:foldl(fun (Dest, R) ->
- XML = Dest#dest.address,
- case Dest#dest.type of
- to -> [add_delivered(XML) | R];
- cc -> [add_delivered(XML) | R];
- _ -> [XML | R]
- end
- end,
- [], Dests).
-
--spec add_delivered(address()) -> address().
-add_delivered(Addr) ->
- Addr#address{delivered = true}.
-
-%%%-------------------------
-%%% Add preliminary packets
-%%%-------------------------
-
-add_addresses(Delivereds, Groups) ->
- Ps = [Group#group.others || Group <- Groups],
- add_addresses2(Delivereds, Groups, [], [], Ps).
-
-add_addresses2(_, [], Res, _, []) -> Res;
-add_addresses2(Delivereds, [Group | Groups], Res, Pa,
- [Pi | Pz]) ->
- Addresses = lists:append([Delivereds] ++ Pa ++ Pz),
- Group2 = Group#group{addresses = Addresses},
- add_addresses2(Delivereds, Groups, [Group2 | Res],
- [Pi | Pa], Pz).
-
-%%%-------------------------
-%%% Decide action groups
-%%%-------------------------
-
--spec decide_action_groups([#group{}]) -> [{routing(), #group{}}].
-decide_action_groups(Groups) ->
- [{Group#group.multicast, Group}
- || Group <- Groups].
+group_by_destinations(Addrs, Map) ->
+ lists:foldl(
+ fun
+ (#address{type = Type, jid = #jid{lserver = Server}} = Addr, Map2) when Type == to; Type == cc ->
+ maps:update_with(Server,
+ fun({CC, BCC}) ->
+ {[Addr | CC], BCC}
+ end, {[Addr], []}, Map2);
+ (#address{type = bcc, jid = #jid{lserver = Server}} = Addr, Map2) ->
+ maps:update_with(Server,
+ fun({CC, BCC}) ->
+ {CC, [Addr | BCC]}
+ end, {[], [Addr]}, Map2)
+ end, Map, Addrs).
%%%-------------------------
%%% Route packet
%%%-------------------------
--spec route_packet(jid(), #dest{}, stanza(), [addresses()], [addresses()]) -> 'ok'.
-route_packet(From, ToDest, Packet, Others, Addresses) ->
- Dests = case ToDest#dest.type of
- bcc -> [];
- _ -> [ToDest]
- end,
- route_packet2(From, ToDest#dest.jid_string, Dests,
- Packet, {Others, Addresses}).
-
--spec route_packet_multicast(jid(), binary(), stanza(), [#dest{}], [address()], #limits{}) -> 'ok'.
-route_packet_multicast(From, ToS, Packet, Dests,
- Addresses, Limits) ->
- Type_of_stanza = type_of_stanza(Packet),
- {_Type, Limit_number} = get_limit_number(Type_of_stanza,
- Limits),
- Fragmented_dests = fragment_dests(Dests, Limit_number),
- lists:foreach(fun(DFragment) ->
- route_packet2(From, ToS, DFragment, Packet,
- Addresses)
- end, Fragmented_dests).
-
--spec route_packet2(jid(), binary(), [#dest{}], stanza(), {[address()], [address()]} | [address()]) -> 'ok'.
-route_packet2(From, ToS, Dests, Packet, Addresses) ->
- Els = case append_dests(Dests, Addresses) of
- [] ->
- xmpp:get_els(Packet);
- ACs ->
- [#addresses{list = ACs}|xmpp:get_els(Packet)]
- end,
- Packet2 = xmpp:set_els(Packet, Els),
- ToJID = stj(ToS),
- ejabberd_router:route(xmpp:set_from_to(Packet2, From, ToJID)).
-
--spec append_dests([#dest{}], {[address()], [address()]} | [address()]) -> [address()].
-append_dests(_Dests, {Others, Addresses}) ->
- Addresses ++ Others;
-append_dests([], Addresses) -> Addresses;
-append_dests([Dest | Dests], Addresses) ->
- append_dests(Dests, [Dest#dest.address | Addresses]).
-
%%%-------------------------
%%% Check relay
%%%-------------------------
--spec check_relay(binary(), binary(), [#group{}]) -> ok.
+-spec check_relay(binary(), binary(), #{}) -> ok.
check_relay(RS, LS, Gs) ->
- case check_relay_required(RS, LS, Gs) of
- false -> ok;
- true -> throw(edrelay)
- end.
-
--spec check_relay_required(binary(), binary(), [#group{}]) -> boolean().
-check_relay_required(RServer, LServerS, Groups) ->
- case lists:suffix(str:tokens(LServerS, <<".">>),
- str:tokens(RServer, <<".">>)) of
- true -> false;
- false -> check_relay_required(LServerS, Groups)
+ case lists:suffix(str:tokens(LS, <<".">>),
+ str:tokens(RS, <<".">>)) orelse
+ (maps:is_key(LS, Gs) andalso maps:size(Gs) == 1) of
+ true -> ok;
+ _ -> throw(edrelay)
end.
--spec check_relay_required(binary(), [#group{}]) -> boolean().
-check_relay_required(LServerS, Groups) ->
- lists:any(fun (Group) -> Group#group.server /= LServerS
- end,
- Groups).
-
%%%-------------------------
%%% Check protocol support: Send request
%%%-------------------------
@@ -1060,20 +920,6 @@ get_slimit_group(local, SLimits) ->
get_slimit_group(remote, SLimits) ->
SLimits#service_limits.remote.
-fragment_dests(Dests, Limit_number) ->
- {R, _} = lists:foldl(fun (Dest, {Res, Count}) ->
- case Count of
- Limit_number ->
- Head2 = [Dest], {[Head2 | Res], 0};
- _ ->
- [Head | Tail] = Res,
- Head2 = [Dest | Head],
- {[Head2 | Tail], Count + 1}
- end
- end,
- {[[]], 0}, Dests),
- R.
-
%%%-------------------------
%%% Limits: XEP-0128 Service Discovery Extensions
%%%-------------------------
diff --git a/src/mod_offline.erl b/src/mod_offline.erl
index c3fda25db..1d367eb72 100644
--- a/src/mod_offline.erl
+++ b/src/mod_offline.erl
@@ -572,6 +572,16 @@ check_event(#message{from = From, to = To, id = ID, type = Type} = Msg) ->
sub_els = [#xevent{id = ID, offline = true}]},
ejabberd_router:route(NewMsg),
true;
+ % Don't store composing events
+ #xevent{id = V, composing = true} when V /= undefined ->
+ false;
+ % Nor composing stopped events
+ #xevent{id = V, composing = false, delivered = false,
+ displayed = false, offline = false} when V /= undefined ->
+ false;
+ % But store other received notifications
+ #xevent{id = V} when V /= undefined ->
+ true;
_ ->
false
end.
@@ -1315,19 +1325,19 @@ mod_doc() ->
{db_type,
#{value => "mnesia | sql",
desc =>
- ?T("Same as top-level 'default_db' option, but applied to this module only.")}},
+ ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}},
{use_cache,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'use_cache' option, but applied to this module only.")}},
+ ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}},
{cache_size,
#{value => "pos_integer() | infinity",
desc =>
- ?T("Same as top-level 'cache_size' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}},
{cache_life_time,
#{value => "timeout()",
desc =>
- ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}}],
+ ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}],
example =>
[{?T("This example allows power users to have as much as 5000 "
"offline messages, administrators up to 2000, and all the "
diff --git a/src/mod_ping.erl b/src/mod_ping.erl
index 483f2e834..f233b2ae8 100644
--- a/src/mod_ping.erl
+++ b/src/mod_ping.erl
@@ -154,7 +154,7 @@ handle_info({iq_reply, timeout, JID}, State) ->
{noreply, State#state{timers = Timers}};
handle_info({timeout, _TRef, {ping, JID}}, State) ->
Host = State#state.host,
- From = jid:remove_resource(JID),
+ From = jid:make(Host),
IQ = #iq{from = From, to = JID, type = get, sub_els = [#ping{}]},
ejabberd_router:route_iq(IQ, JID,
gen_mod:get_module_proc(Host, ?MODULE),
@@ -300,7 +300,7 @@ mod_doc() ->
desc =>
?T("How long to wait before deeming that a client "
"has not answered a given server ping request. "
- "The default value is '32' seconds.")}},
+ "The default value is 'undefined'.")}},
{send_pings,
#{value => "true | false",
desc =>
@@ -317,8 +317,8 @@ mod_doc() ->
"server ping request in less than period defined "
"in 'ping_ack_timeout' option: "
"'kill' means destroying the underlying connection, "
- "'none' means to do nothing. NOTE: when 'mod_stream_mgmt' "
- "module is loaded and stream management is enabled by "
+ "'none' means to do nothing. NOTE: when _`mod_stream_mgmt`_ "
+ "is loaded and stream management is enabled by "
"a client, killing the client connection doesn't mean "
"killing the client session - the session will be kept "
"alive in order to give the client a chance to resume it. "
diff --git a/src/mod_privacy.erl b/src/mod_privacy.erl
index 4f15b80c4..5ac26c2f5 100644
--- a/src/mod_privacy.erl
+++ b/src/mod_privacy.erl
@@ -887,25 +887,25 @@ mod_doc() ->
"https://xmpp.org/extensions/xep-0191.html"
"[XEP-0191: Blocking Command] which is implemented by "
"'mod_blocking' module. However, you still need "
- "'mod_privacy' loaded in order for 'mod_blocking' to work.")],
+ "'mod_privacy' loaded in order for _`mod_blocking`_ to work.")],
opts =>
[{db_type,
#{value => "mnesia | sql",
desc =>
- ?T("Same as top-level 'default_db' option, but applied to this module only.")}},
+ ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}},
{use_cache,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'use_cache' option, but applied to this module only.")}},
+ ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}},
{cache_size,
#{value => "pos_integer() | infinity",
desc =>
- ?T("Same as top-level 'cache_size' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}},
{cache_missed,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'cache_missed' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}},
{cache_life_time,
#{value => "timeout()",
desc =>
- ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}}]}.
+ ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}.
diff --git a/src/mod_private.erl b/src/mod_private.erl
index ad36c8494..436aae222 100644
--- a/src/mod_private.erl
+++ b/src/mod_private.erl
@@ -66,7 +66,7 @@ start(Host, Opts) ->
ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, get_sm_features, 50),
ejabberd_hooks:add(pubsub_publish_item, Host, ?MODULE, pubsub_publish_item, 50),
gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_PRIVATE, ?MODULE, process_sm_iq),
- ejabberd_commands:register_commands(get_commands_spec()).
+ ejabberd_commands:register_commands(?MODULE, get_commands_spec()).
stop(Host) ->
ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 50),
@@ -128,23 +128,23 @@ mod_doc() ->
[{db_type,
#{value => "mnesia | sql",
desc =>
- ?T("Same as top-level 'default_db' option, but applied to this module only.")}},
+ ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}},
{use_cache,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'use_cache' option, but applied to this module only.")}},
+ ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}},
{cache_size,
#{value => "pos_integer() | infinity",
desc =>
- ?T("Same as top-level 'cache_size' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}},
{cache_missed,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'cache_missed' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}},
{cache_life_time,
#{value => "timeout()",
desc =>
- ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}}]}.
+ ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}.
-spec get_sm_features({error, stanza_error()} | empty | {result, [binary()]},
jid(), jid(), binary(), binary()) ->
diff --git a/src/mod_privilege.erl b/src/mod_privilege.erl
index a6d4ba446..353a8da27 100644
--- a/src/mod_privilege.erl
+++ b/src/mod_privilege.erl
@@ -106,7 +106,7 @@ mod_doc() ->
?T("WARNING: Security issue: Privileged access gives components "
"access to sensitive data, so permission should be granted "
"carefully, only if you trust a component."), "",
- ?T("NOTE: This module is complementary to 'mod_delegation', "
+ ?T("NOTE: This module is complementary to _`mod_delegation`_, "
"but can also be used separately.")],
opts =>
[{roster,
diff --git a/src/mod_pubsub.erl b/src/mod_pubsub.erl
index e64b73083..43344986e 100644
--- a/src/mod_pubsub.erl
+++ b/src/mod_pubsub.erl
@@ -45,6 +45,7 @@
-include("mod_roster.hrl").
-include("translate.hrl").
-include("ejabberd_stacktrace.hrl").
+-include("ejabberd_commands.hrl").
-define(STDTREE, <<"tree">>).
-define(STDNODE, <<"flat">>).
@@ -93,6 +94,9 @@
handle_call/3, handle_cast/2, handle_info/2, mod_doc/0,
terminate/2, code_change/3, depends/2, mod_opt_type/1, mod_options/1]).
+%% ejabberd commands
+-export([get_commands_spec/0, delete_old_items/1, delete_expired_items/0]).
+
-export([route/1]).
%%====================================================================
@@ -210,7 +214,7 @@
pep_mapping :: [{binary(), binary()}],
ignore_pep_from_offline :: boolean(),
last_item_cache :: boolean(),
- max_items_node :: non_neg_integer(),
+ max_items_node :: non_neg_integer()|unlimited,
max_subscriptions_node :: non_neg_integer()|undefined,
default_node_config :: [{atom(), binary()|boolean()|integer()|atom()}],
nodetree :: binary(),
@@ -337,6 +341,7 @@ init([ServerHost|_]) ->
false ->
ok
end,
+ ejabberd_commands:register_commands(?MODULE, get_commands_spec()),
NodeTree = config(ServerHost, nodetree),
Plugins = config(ServerHost, plugins),
PepMapping = config(ServerHost, pep_mapping),
@@ -806,7 +811,13 @@ terminate(_Reason,
gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_COMMANDS),
terminate_plugins(Host, ServerHost, Plugins, TreePlugin),
ejabberd_router:unregister_route(Host)
- end, Hosts).
+ end, Hosts),
+ case gen_mod:is_loaded_elsewhere(ServerHost, ?MODULE) of
+ false ->
+ ejabberd_commands:unregister_commands(get_commands_spec());
+ true ->
+ ok
+ end.
%%--------------------------------------------------------------------
%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState}
@@ -3399,14 +3410,14 @@ node_config(_, _, []) ->
%% @doc <p>Return the maximum number of items for a given node.</p>
%% <p>Unlimited means that there is no limit in the number of items that can
%% be stored.</p>
--spec max_items(host(), [{atom(), any()}]) -> non_neg_integer().
+-spec max_items(host(), [{atom(), any()}]) -> non_neg_integer() | unlimited.
max_items(Host, Options) ->
case get_option(Options, persist_items) of
true ->
case get_option(Options, max_items) of
I when is_integer(I), I < 0 -> 0;
I when is_integer(I) -> I;
- _ -> ?MAXITEMS
+ _ -> get_max_items_node(Host)
end;
false ->
case get_option(Options, send_last_published_item) of
@@ -3420,6 +3431,14 @@ max_items(Host, Options) ->
end
end.
+-spec item_expire(host(), [{atom(), any()}]) -> non_neg_integer() | infinity.
+item_expire(Host, Options) ->
+ case get_option(Options, item_expire) of
+ I when is_integer(I), I < 0 -> 0;
+ I when is_integer(I) -> I;
+ _ -> get_max_item_expire_node(Host)
+ end.
+
-spec get_configure_xfields(_, pubsub_node_config:result(),
binary(), [binary()]) -> [xdata_field()].
get_configure_xfields(_Type, Options, Lang, Groups) ->
@@ -3548,16 +3567,23 @@ decode_get_pending(#xdata{fields = Fs}, Lang) ->
{error, xmpp:err_resource_constraint(Txt, Lang)}
end.
--spec check_opt_range(atom(), [proplists:property()], non_neg_integer()) -> boolean().
-check_opt_range(_Opt, _Opts, undefined) ->
+-spec check_opt_range(atom(), [proplists:property()],
+ non_neg_integer() | unlimited) -> boolean().
+check_opt_range(_Opt, _Opts, unlimited) ->
true;
check_opt_range(Opt, Opts, Max) ->
- Val = proplists:get_value(Opt, Opts, Max),
- Val =< Max.
+ case proplists:get_value(Opt, Opts, Max) of
+ max -> true;
+ Val -> Val =< Max
+ end.
--spec get_max_items_node(host()) -> undefined | non_neg_integer().
+-spec get_max_items_node(host()) -> unlimited | non_neg_integer().
get_max_items_node(Host) ->
- config(Host, max_items_node, undefined).
+ config(Host, max_items_node, ?MAXITEMS).
+
+-spec get_max_item_expire_node(host()) -> infinity | non_neg_integer().
+get_max_item_expire_node(Host) ->
+ config(Host, max_item_expire_node, infinity).
-spec get_max_subscriptions_node(host()) -> undefined | non_neg_integer().
get_max_subscriptions_node(Host) ->
@@ -3707,6 +3733,7 @@ features() ->
<<"access-whitelist">>, % OPTIONAL
<<"collections">>, % RECOMMENDED
<<"config-node">>, % RECOMMENDED
+ <<"config-node-max">>,
<<"create-and-configure">>, % RECOMMENDED
<<"item-ids">>, % RECOMMENDED
<<"last-published">>, % RECOMMENDED
@@ -4136,6 +4163,91 @@ purge_offline(Host, LJID, Node) ->
{error, xmpp:err_internal_server_error(Txt, Lang)}
end.
+-spec delete_old_items(non_neg_integer()) -> ok | error.
+delete_old_items(N) ->
+ Results = lists:flatmap(
+ fun(Host) ->
+ case tree_action(Host, get_all_nodes, [Host]) of
+ Nodes when is_list(Nodes) ->
+ lists:map(
+ fun(#pubsub_node{id = Nidx, type = Type}) ->
+ case node_action(Host, Type,
+ remove_extra_items,
+ [Nidx , N]) of
+ {result, _} ->
+ ok;
+ {error, _} ->
+ error
+ end
+ end, Nodes);
+ _ ->
+ error
+ end
+ end, ejabberd_option:hosts()),
+ case lists:member(error, Results) of
+ true ->
+ error;
+ false ->
+ ok
+ end.
+
+-spec delete_expired_items() -> ok | error.
+delete_expired_items() ->
+ Results = lists:flatmap(
+ fun(Host) ->
+ case tree_action(Host, get_all_nodes, [Host]) of
+ Nodes when is_list(Nodes) ->
+ lists:map(
+ fun(#pubsub_node{id = Nidx, type = Type,
+ options = Options}) ->
+ case item_expire(Host, Options) of
+ infinity ->
+ ok;
+ Seconds ->
+ case node_action(
+ Host, Type,
+ remove_expired_items,
+ [Nidx, Seconds]) of
+ {result, []} ->
+ ok;
+ {result, [_|_]} ->
+ unset_cached_item(
+ Host, Nidx);
+ {error, _} ->
+ error
+ end
+ end
+ end, Nodes);
+ _ ->
+ error
+ end
+ end, ejabberd_option:hosts()),
+ case lists:member(error, Results) of
+ true ->
+ error;
+ false ->
+ ok
+ end.
+
+-spec get_commands_spec() -> [ejabberd_commands()].
+get_commands_spec() ->
+ [#ejabberd_commands{name = delete_old_pubsub_items, tags = [purge],
+ desc = "Keep only NUMBER of PubSub items per node",
+ module = ?MODULE, function = delete_old_items,
+ args_desc = ["Number of items to keep per node"],
+ args = [{number, integer}],
+ result = {res, rescode},
+ result_desc = "0 if command failed, 1 when succeeded",
+ args_example = [1000],
+ result_example = ok},
+ #ejabberd_commands{name = delete_expired_pubsub_items, tags = [purge],
+ desc = "Delete expired PubSub items",
+ module = ?MODULE, function = delete_expired_items,
+ args = [],
+ result = {res, rescode},
+ result_desc = "0 if command failed, 1 when succeeded",
+ result_example = ok}].
+
-spec mod_opt_type(atom()) -> econf:validator().
mod_opt_type(access_createnode) ->
econf:acl();
@@ -4146,7 +4258,9 @@ mod_opt_type(ignore_pep_from_offline) ->
mod_opt_type(last_item_cache) ->
econf:bool();
mod_opt_type(max_items_node) ->
- econf:non_neg_int();
+ econf:non_neg_int(unlimited);
+mod_opt_type(max_item_expire_node) ->
+ econf:timeout(second, infinity);
mod_opt_type(max_nodes_discoitems) ->
econf:non_neg_int(infinity);
mod_opt_type(max_subscriptions_node) ->
@@ -4194,6 +4308,7 @@ mod_options(Host) ->
{ignore_pep_from_offline, true},
{last_item_cache, false},
{max_items_node, ?MAXITEMS},
+ {max_item_expire_node, infinity},
{max_nodes_discoitems, 100},
{nodetree, ?STDTREE},
{pep_mapping, []},
@@ -4212,7 +4327,7 @@ mod_doc() ->
"(https://xmpp.org/extensions/xep-0163.html"
"[XEP-0163: Personal Eventing via Pubsub]) "
"is enabled in the default ejabberd configuration file, "
- "and it requires 'mod_caps'.")],
+ "and it requires _`mod_caps`_.")],
opts =>
[{access_createnode,
#{value => "AccessName",
@@ -4225,7 +4340,7 @@ mod_doc() ->
{db_type,
#{value => "mnesia | sql",
desc =>
- ?T("Same as top-level 'default_db' option, but applied to "
+ ?T("Same as top-level _`default_db`_ option, but applied to "
"this module only.")}},
{default_node_config,
#{value => "List of Key:Value",
@@ -4272,8 +4387,13 @@ mod_doc() ->
" so many nodes, caching last items speeds up pubsub "
"and allows to raise user connection rate. The cost "
"is memory usage, as every item is stored in memory.")}},
+ {max_item_expire_node,
+ #{value => "timeout() | infinity",
+ desc =>
+ ?T("Specify the maximum item epiry time. Default value "
+ "is: 'infinity'.")}},
{max_items_node,
- #{value => "MaxItems",
+ #{value => "non_neg_integer() | infinity",
desc =>
?T("Define the maximum number of items that can be "
"stored in a node. Default value is: '1000'.")}},
diff --git a/src/mod_pubsub_opt.erl b/src/mod_pubsub_opt.erl
index 8db5532f6..cb3c014b9 100644
--- a/src/mod_pubsub_opt.erl
+++ b/src/mod_pubsub_opt.erl
@@ -11,6 +11,7 @@
-export([hosts/1]).
-export([ignore_pep_from_offline/1]).
-export([last_item_cache/1]).
+-export([max_item_expire_node/1]).
-export([max_items_node/1]).
-export([max_nodes_discoitems/1]).
-export([max_subscriptions_node/1]).
@@ -68,7 +69,13 @@ last_item_cache(Opts) when is_map(Opts) ->
last_item_cache(Host) ->
gen_mod:get_module_opt(Host, mod_pubsub, last_item_cache).
--spec max_items_node(gen_mod:opts() | global | binary()) -> non_neg_integer().
+-spec max_item_expire_node(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer().
+max_item_expire_node(Opts) when is_map(Opts) ->
+ gen_mod:get_opt(max_item_expire_node, Opts);
+max_item_expire_node(Host) ->
+ gen_mod:get_module_opt(Host, mod_pubsub, max_item_expire_node).
+
+-spec max_items_node(gen_mod:opts() | global | binary()) -> 'unlimited' | non_neg_integer().
max_items_node(Opts) when is_map(Opts) ->
gen_mod:get_opt(max_items_node, Opts);
max_items_node(Host) ->
diff --git a/src/mod_push.erl b/src/mod_push.erl
index b4dd48234..5477c5792 100644
--- a/src/mod_push.erl
+++ b/src/mod_push.erl
@@ -98,7 +98,7 @@ start(Host, Opts) ->
init_cache(Mod, Host, Opts),
register_iq_handlers(Host),
register_hooks(Host),
- ejabberd_commands:register_commands(get_commands_spec()).
+ ejabberd_commands:register_commands(?MODULE, get_commands_spec()).
-spec stop(binary()) -> ok.
stop(Host) ->
@@ -191,23 +191,23 @@ mod_doc() ->
{db_type,
#{value => "mnesia | sql",
desc =>
- ?T("Same as top-level 'default_db' option, but applied to this module only.")}},
+ ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}},
{use_cache,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'use_cache' option, but applied to this module only.")}},
+ ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}},
{cache_size,
#{value => "pos_integer() | infinity",
desc =>
- ?T("Same as top-level 'cache_size' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}},
{cache_missed,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'cache_missed' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}},
{cache_life_time,
#{value => "timeout()",
desc =>
- ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}}]}.
+ ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}.
%%--------------------------------------------------------------------
%% ejabberd command callback.
diff --git a/src/mod_push_keepalive.erl b/src/mod_push_keepalive.erl
index aba4fbfae..e0e83f1e1 100644
--- a/src/mod_push_keepalive.erl
+++ b/src/mod_push_keepalive.erl
@@ -94,13 +94,13 @@ mod_options(_Host) ->
mod_doc() ->
#{desc =>
[?T("This module tries to keep the stream management "
- "session (see 'mod_stream_mgmt') of a disconnected "
+ "session (see _`mod_stream_mgmt`_) of a disconnected "
"mobile client alive if the client enabled push "
"notifications for that session. However, the normal "
"session resumption timeout is restored once a push "
"notification is issued, so the session will be closed "
"if the client doesn't respond to push notifications."), "",
- ?T("The module depends on 'mod_push'.")],
+ ?T("The module depends on _`mod_push`_.")],
opts =>
[{resume_timeout,
#{value => "timeout()",
@@ -109,8 +109,8 @@ mod_doc() ->
"the session of a disconnected push client times out. "
"This timeout is only in effect as long as no push "
"notification is issued. Once that happened, the "
- "resumption timeout configured for the 'mod_stream_mgmt' "
- "module is restored. "
+ "resumption timeout configured for _`mod_stream_mgmt`_ "
+ "is restored. "
"The default value is '72' hours.")}},
{wake_on_start,
#{value => "true | false",
diff --git a/src/mod_push_sql.erl b/src/mod_push_sql.erl
index f89b904c2..c024a12d1 100644
--- a/src/mod_push_sql.erl
+++ b/src/mod_push_sql.erl
@@ -52,7 +52,7 @@ store_session(LUser, LServer, NowTS, PushJID, Node, XData) ->
case ?SQL_UPSERT(LServer, "push_session",
["!username=%(LUser)s",
"!server_host=%(LServer)s",
- "!timestamp=%(TS)d",
+ "timestamp=%(TS)d",
"!service=%(Service)s",
"!node=%(Node)s",
"xml=%(XML)s"]) of
diff --git a/src/mod_register.erl b/src/mod_register.erl
index a864c0df2..0bb0978ef 100644
--- a/src/mod_register.erl
+++ b/src/mod_register.erl
@@ -32,11 +32,13 @@
-behaviour(gen_mod).
-export([start/2, stop/1, reload/3, stream_feature_register/2,
- c2s_unauthenticated_packet/2, try_register/4,
+ c2s_unauthenticated_packet/2, try_register/4, try_register/5,
process_iq/1, send_registration_notifications/3,
mod_opt_type/1, mod_options/1, depends/2,
format_error/1, mod_doc/0]).
+-deprecated({try_register, 4}).
+
-include("logger.hrl").
-include_lib("xmpp/include/xmpp.hrl").
-include("translate.hrl").
@@ -283,7 +285,7 @@ try_register_or_set_password(User, Server, Password,
_ when CaptchaSucceed ->
case check_from(From, Server) of
allow ->
- case try_register(User, Server, Password, Source, Lang) of
+ case try_register(User, Server, Password, Source, ?MODULE, Lang) of
ok ->
xmpp:make_iq_result(IQ);
{error, Error} ->
@@ -328,6 +330,13 @@ try_set_password(User, Server, Password, #iq{lang = Lang, meta = M} = IQ) ->
xmpp:make_error(IQ, xmpp:err_internal_server_error(format_error(Why), Lang))
end.
+try_register(User, Server, Password, SourceRaw, Module) ->
+ Modules = mod_register_opt:allow_modules(Server),
+ case (Modules == all) orelse lists:member(Module, Modules) of
+ true -> try_register(User, Server, Password, SourceRaw);
+ false -> {error, eaccess}
+ end.
+
try_register(User, Server, Password, SourceRaw) ->
case jid:is_nodename(User) of
false ->
@@ -363,8 +372,8 @@ try_register(User, Server, Password, SourceRaw) ->
end
end.
-try_register(User, Server, Password, SourceRaw, Lang) ->
- case try_register(User, Server, Password, SourceRaw) of
+try_register(User, Server, Password, SourceRaw, Module, Lang) ->
+ case try_register(User, Server, Password, SourceRaw, Module) of
ok ->
JID = jid:make(User, Server),
Source = may_remove_resource(SourceRaw),
@@ -597,6 +606,8 @@ mod_opt_type(access_from) ->
econf:acl();
mod_opt_type(access_remove) ->
econf:acl();
+mod_opt_type(allow_modules) ->
+ econf:either(all, econf:list(econf:atom()));
mod_opt_type(captcha_protected) ->
econf:bool();
mod_opt_type(ip_access) ->
@@ -623,6 +634,7 @@ mod_options(_Host) ->
[{access, all},
{access_from, none},
{access_remove, all},
+ {allow_modules, all},
{captcha_protected, false},
{ip_access, all},
{password_strength, 0},
@@ -638,9 +650,9 @@ mod_doc() ->
?T("* Register a new account on the server."), "",
?T("* Change the password from an existing account on the server."), "",
?T("* Delete an existing account on the server."), "",
- ?T("This module reads also another option defined globally for the "
- "server: 'registration_timeout'. Please check that option "
- "documentation in the section with top-level options.")],
+ ?T("This module reads also the top-level _`registration_timeout`_ "
+ "option defined globally for the server, "
+ "so please check that option documentation too.")],
opts =>
[{access,
#{value => ?T("AccessName"),
@@ -661,12 +673,17 @@ mod_doc() ->
desc =>
?T("Specify rules to restrict access for user unregistration. "
"By default any user is able to unregister their account.")}},
+ {allow_modules,
+ #{value => "all | [Module, ...]",
+ desc =>
+ ?T("List of modules that can register accounts, or 'all'. "
+ "The default value is 'all', which is equivalent to "
+ "something like '[mod_register, mod_register_web]'.")}},
{captcha_protected,
#{value => "true | false",
desc =>
- ?T("Protect registrations with CAPTCHA (see section "
- "https://docs.ejabberd.im/admin/configuration/basic/#captcha[CAPTCHA] "
- "of the Configuration Guide). The default is 'false'.")}},
+ ?T("Protect registrations with http://../basic/#captcha[CAPTCHA]. "
+ "The default is 'false'.")}},
{ip_access,
#{value => ?T("AccessName"),
desc =>
@@ -680,7 +697,7 @@ mod_doc() ->
"https://en.wikipedia.org/wiki/Entropy_(information_theory)"
"[Shannon entropy] for passwords. The value 'Entropy' is a "
"number of bits of entropy. The recommended minimum is 32 bits. "
- "The default is 0, i.e. no checks are performed.")}},
+ "The default is '0', i.e. no checks are performed.")}},
{registration_watchers,
#{value => "[JID, ...]",
desc =>
diff --git a/src/mod_register_opt.erl b/src/mod_register_opt.erl
index 53c6ca6ea..e7236424c 100644
--- a/src/mod_register_opt.erl
+++ b/src/mod_register_opt.erl
@@ -6,6 +6,7 @@
-export([access/1]).
-export([access_from/1]).
-export([access_remove/1]).
+-export([allow_modules/1]).
-export([captcha_protected/1]).
-export([ip_access/1]).
-export([password_strength/1]).
@@ -31,6 +32,12 @@ access_remove(Opts) when is_map(Opts) ->
access_remove(Host) ->
gen_mod:get_module_opt(Host, mod_register, access_remove).
+-spec allow_modules(gen_mod:opts() | global | binary()) -> 'all' | [atom()].
+allow_modules(Opts) when is_map(Opts) ->
+ gen_mod:get_opt(allow_modules, Opts);
+allow_modules(Host) ->
+ gen_mod:get_module_opt(Host, mod_register, allow_modules).
+
-spec captcha_protected(gen_mod:opts() | global | binary()) -> boolean().
captcha_protected(Opts) when is_map(Opts) ->
gen_mod:get_opt(captcha_protected, Opts);
diff --git a/src/mod_register_web.erl b/src/mod_register_web.erl
index 9c2179302..0cf4bcff8 100644
--- a/src/mod_register_web.erl
+++ b/src/mod_register_web.erl
@@ -23,32 +23,6 @@
%%%
%%%----------------------------------------------------------------------
-%%% IDEAS:
-%%%
-%%% * Implement those options, already present in mod_register:
-%%% + access
-%%% + captcha_protected
-%%% + password_strength
-%%% + welcome_message
-%%% + registration_timeout
-%%%
-%%% * Improve this module to allow each virtual host to have different
-%%% options. See http://support.process-one.net/browse/EJAB-561
-%%%
-%%% * Check that all the text is translatable.
-%%%
-%%% * Add option to use a custom CSS file, or custom CSS lines.
-%%%
-%%% * Don't hardcode the "register" path in URL.
-%%%
-%%% * Allow private email during register, and store in custom table.
-%%% * Optionally require private email to register.
-%%% * Optionally require email confirmation to register.
-%%% * Allow to set a private email address anytime.
-%%% * Allow to recover password using private email to confirm (mod_passrecover)
-%%% * Optionally require invitation
-%%% * Optionally register request is forwarded to admin, no account created.
-
-module(mod_register_web).
-author('badlop@process-one.net').
@@ -111,7 +85,7 @@ process([Section],
process([<<"new">>],
#request{method = 'POST', q = Q, ip = {Ip, _Port},
lang = Lang, host = _HTTPHost}) ->
- case form_new_post(Q) of
+ case form_new_post(Q, Ip) of
{success, ok, {Username, Host, _Password}} ->
Jid = jid:make(Username, Host),
mod_register:send_registration_notifications(?MODULE, Jid, Ip),
@@ -316,10 +290,10 @@ form_new_get2(Host, Lang, CaptchaEls) ->
%%% Formulary new POST
%%%----------------------------------------------------------------------
-form_new_post(Q) ->
+form_new_post(Q, Ip) ->
case catch get_register_parameters(Q) of
[Username, Host, Password, Password, Id, Key] ->
- form_new_post(Username, Host, Password, {Id, Key});
+ form_new_post(Username, Host, Password, {Id, Key}, Ip);
[_Username, _Host, _Password, _Password2, false, false] ->
{error, passwords_not_identical};
[_Username, _Host, _Password, _Password2, Id, Key] ->
@@ -338,13 +312,12 @@ get_register_parameters(Q) ->
[<<"username">>, <<"host">>, <<"password">>, <<"password2">>,
<<"id">>, <<"key">>]).
-form_new_post(Username, Host, Password,
- {false, false}) ->
- register_account(Username, Host, Password);
-form_new_post(Username, Host, Password, {Id, Key}) ->
+form_new_post(Username, Host, Password, {false, false}, Ip) ->
+ register_account(Username, Host, Password, Ip);
+form_new_post(Username, Host, Password, {Id, Key}, Ip) ->
case ejabberd_captcha:check_captcha(Id, Key) of
captcha_valid ->
- register_account(Username, Host, Password);
+ register_account(Username, Host, Password, Ip);
captcha_non_valid -> {error, captcha_non_valid};
captcha_not_found -> {error, captcha_non_valid}
end.
@@ -528,24 +501,27 @@ form_del_get(Host, Lang) ->
{<<"Content-Type">>, <<"text/html">>}],
ejabberd_web:make_xhtml(HeadEls, Els)}.
-%% @spec(Username, Host, Password) -> {success, ok, {Username, Host, Password} |
+%% @spec(Username, Host, Password, Ip) -> {success, ok, {Username, Host, Password} |
%% {success, exists, {Username, Host, Password}} |
%% {error, not_allowed} |
%% {error, invalid_jid}
-register_account(Username, Host, Password) ->
- Access = mod_register_opt:access(Host),
- case jid:make(Username, Host) of
- error -> {error, invalid_jid};
- JID ->
- case acl:match_rule(Host, Access, JID) of
- deny -> {error, not_allowed};
- allow -> register_account2(Username, Host, Password)
- end
+register_account(Username, Host, Password, Ip) ->
+ try mod_register_opt:access(Host) of
+ Access ->
+ case jid:make(Username, Host) of
+ error -> {error, invalid_jid};
+ JID ->
+ case acl:match_rule(Host, Access, JID) of
+ deny -> {error, not_allowed};
+ allow -> register_account2(Username, Host, Password, Ip)
+ end
+ end
+ catch _:{module_not_loaded, mod_register, _Host} ->
+ {error, host_unknown}
end.
-register_account2(Username, Host, Password) ->
- case ejabberd_auth:try_register(Username, Host,
- Password)
+register_account2(Username, Host, Password, Ip) ->
+ case mod_register:try_register(Username, Host, Password, Ip, ?MODULE)
of
ok ->
{success, ok, {Username, Host, Password}};
@@ -601,10 +577,8 @@ get_error_text({error, exists}) ->
?T("The account already exists");
get_error_text({error, password_incorrect}) ->
?T("Incorrect password");
-get_error_text({error, invalid_jid}) ->
- ?T("The username is not valid");
-get_error_text({error, not_allowed}) ->
- ?T("Not allowed");
+get_error_text({error, host_unknown}) ->
+ ?T("Host unknown");
get_error_text({error, account_doesnt_exist}) ->
?T("Account doesn't exist");
get_error_text({error, account_exists}) ->
@@ -614,7 +588,9 @@ get_error_text({error, password_not_changed}) ->
get_error_text({error, passwords_not_identical}) ->
?T("The passwords are different");
get_error_text({error, wrong_parameters}) ->
- ?T("Wrong parameters in the web formulary").
+ ?T("Wrong parameters in the web formulary");
+get_error_text({error, Why}) ->
+ mod_register:format_error(Why).
mod_options(_) ->
[].
@@ -625,13 +601,27 @@ mod_doc() ->
?T("- Register a new account on the server."), "",
?T("- Change the password from an existing account on the server."), "",
?T("- Unregister an existing account on the server."), "",
- ?T("This module supports CAPTCHA image to register a new account. "
- "To enable this feature, configure the options 'captcha\_cmd' "
- "and 'captcha\_url', which are documented in the section with "
- "top-level options."), "",
- ?T("As an example usage, the users of the host 'example.org' can "
- "visit the page: 'https://example.org:5281/register/' It is "
+ ?T("This module supports http://../basic/#captcha[CAPTCHA] "
+ "to register a new account. "
+ "To enable this feature, configure the "
+ "top-level _`captcha_cmd`_ and "
+ "top-level _`captcha_url`_ options."), "",
+ ?T("As an example usage, the users of the host 'localhost' can "
+ "visit the page: 'https://localhost:5280/register/' It is "
"important to include the last / character in the URL, "
"otherwise the subpages URL will be incorrect."), "",
- ?T("The module depends on 'mod_register' where all the configuration "
- "is performed.")]}.
+ ?T("This module is enabled in 'listen' -> 'ejabberd_http' -> "
+ "http://../listen-options/#request-handlers[request_handlers], "
+ "no need to enable in 'modules'."),
+ ?T("The module depends on _`mod_register`_ where all the "
+ "configuration is performed.")],
+ example =>
+ ["listen:",
+ " -",
+ " port: 5280",
+ " module: ejabberd_http",
+ " request_handlers:",
+ " /register: mod_register_web",
+ "",
+ "modules:",
+ " mod_register: {}"]}.
diff --git a/src/mod_roster.erl b/src/mod_roster.erl
index f204c9211..94cae4950 100644
--- a/src/mod_roster.erl
+++ b/src/mod_roster.erl
@@ -1345,29 +1345,29 @@ mod_doc() ->
"This option does not affect the client in any way. "
"This option is only useful if option 'versioning' is "
"set to 'true'. The default value is 'false'. "
- "IMPORTANT: if you use 'mod_shared_roster' or "
- "'mod_shared_roster_ldap', you must set the value "
+ "IMPORTANT: if you use _`mod_shared_roster`_ or "
+ " _`mod_shared_roster_ldap`_, you must set the value "
"of the option to 'false'.")}},
{db_type,
#{value => "mnesia | sql",
desc =>
- ?T("Same as top-level 'default_db' option, but applied to this module only.")}},
+ ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}},
{use_cache,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'use_cache' option, but applied to this module only.")}},
+ ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}},
{cache_size,
#{value => "pos_integer() | infinity",
desc =>
- ?T("Same as top-level 'cache_size' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}},
{cache_missed,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'cache_missed' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}},
{cache_life_time,
#{value => "timeout()",
desc =>
- ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}}],
+ ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}],
example =>
["modules:",
" ...",
diff --git a/src/mod_roster_sql.erl b/src/mod_roster_sql.erl
index 76ddb29dd..ebfcde463 100644
--- a/src/mod_roster_sql.erl
+++ b/src/mod_roster_sql.erl
@@ -80,9 +80,10 @@ get_roster(LUser, LServer) ->
[]
end,
GroupsDict = lists:foldl(fun({J, G}, Acc) ->
- dict:append(J, G, Acc)
+ Gs = maps:get(J, Acc, []),
+ maps:put(J, [G | Gs], Acc)
end,
- dict:new(), JIDGroups),
+ maps:new(), JIDGroups),
{ok, lists:flatmap(
fun(I) ->
case raw_to_record(LServer, I) of
@@ -90,10 +91,7 @@ get_roster(LUser, LServer) ->
error -> [];
R ->
SJID = jid:encode(R#roster.jid),
- Groups = case dict:find(SJID, GroupsDict) of
- {ok, Gs} -> Gs;
- error -> []
- end,
+ Groups = maps:get(SJID, GroupsDict, []),
[R#roster{groups = Groups}]
end
end, Items)};
diff --git a/src/mod_shared_roster.erl b/src/mod_shared_roster.erl
index 16cc96a75..13ff90466 100644
--- a/src/mod_shared_roster.erl
+++ b/src/mod_shared_roster.erl
@@ -1266,7 +1266,7 @@ mod_doc() ->
?T("- Displayed: A list of groups that will be in the "
"rosters of this group's members. A group of other vhost can "
"be identified with 'groupid@vhost'."), "",
- ?T("This module depends on 'mod_roster'. "
+ ?T("This module depends on _`mod_roster`_. "
"If not enabled, roster queries will return 503 errors.")],
opts =>
[{db_type,
@@ -1274,25 +1274,25 @@ mod_doc() ->
desc =>
?T("Define the type of storage where the module will create "
"the tables and store user information. The default is "
- "the storage defined by the global option 'default_db', "
+ "the storage defined by the top-level _`default_db`_ option, "
"or 'mnesia' if omitted. If 'sql' value is defined, "
"make sure you have defined the database.")}},
- {use_cache,
+ {use_cache,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'use_cache' option, but applied to this module only.")}},
+ ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}},
{cache_size,
#{value => "pos_integer() | infinity",
desc =>
- ?T("Same as top-level 'cache_size' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}},
{cache_missed,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'cache_missed' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}},
{cache_life_time,
#{value => "timeout()",
desc =>
- ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}}],
+ ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}],
example =>
[{?T("Take the case of a computer club that wants all its members "
"seeing each other in their rosters. To achieve this, they "
diff --git a/src/mod_shared_roster_ldap.erl b/src/mod_shared_roster_ldap.erl
index 93c08e0c3..08fbe8793 100644
--- a/src/mod_shared_roster_ldap.erl
+++ b/src/mod_shared_roster_ldap.erl
@@ -796,7 +796,7 @@ mod_doc() ->
"disable the check. Default value is 'true'.")}}] ++
[{Opt,
#{desc =>
- {?T("Same as top-level '~s' option, but "
+ {?T("Same as top-level _`~s`_ option, but "
"applied to this module only."), [Opt]}}}
|| Opt <- [ldap_backups, ldap_base, ldap_uids, ldap_deref_aliases,
ldap_encrypt, ldap_password, ldap_port, ldap_rootdn,
diff --git a/src/mod_stream_mgmt.erl b/src/mod_stream_mgmt.erl
index b9443e5d2..f60f6722b 100644
--- a/src/mod_stream_mgmt.erl
+++ b/src/mod_stream_mgmt.erl
@@ -962,12 +962,14 @@ mod_doc() ->
{queue_type,
#{value => "ram | file",
desc =>
- ?T("Same as top-level 'queue_type' option, but applied to this module only.")}},
+ ?T("Same as top-level _`queue_type`_ option, but applied to this module only.")}},
{cache_size,
#{value => "pos_integer() | infinity",
desc =>
- ?T("Same as top-level 'cache_size' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}},
{cache_life_time,
#{value => "timeout()",
desc =>
- ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}}]}.
+ ?T("Same as top-level _`cache_life_time`_ option, "
+ "but applied to this module only. "
+ "The default value is '48 hours'.")}}]}.
diff --git a/src/mod_stun_disco.erl b/src/mod_stun_disco.erl
index bb701b96b..6e7592453 100644
--- a/src/mod_stun_disco.erl
+++ b/src/mod_stun_disco.erl
@@ -176,7 +176,7 @@ mod_doc() ->
"clients. If ejabberd's built-in TURN service is used, "
"TURN relays allocated using temporary credentials will "
"be terminated shortly after the credentials expired. The "
- "default value is '12' hours. Note that restarting the "
+ "default value is '12 hours'. Note that restarting the "
"ejabberd node invalidates any temporary credentials "
"offered before the restart unless a 'secret' is "
"specified (see below).")}},
diff --git a/src/mod_vcard.erl b/src/mod_vcard.erl
index e7cfff819..8e0d32a4a 100644
--- a/src/mod_vcard.erl
+++ b/src/mod_vcard.erl
@@ -640,23 +640,23 @@ mod_doc() ->
{db_type,
#{value => "mnesia | sql | ldap",
desc =>
- ?T("Same as top-level 'default_db' option, but applied to this module only.")}},
+ ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}},
{use_cache,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'use_cache' option, but applied to this module only.")}},
+ ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}},
{cache_size,
#{value => "pos_integer() | infinity",
desc =>
- ?T("Same as top-level 'cache_size' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}},
{cache_missed,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'cache_missed' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}},
{cache_life_time,
#{value => "timeout()",
desc =>
- ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}},
{vcard,
#{value => ?T("vCard"),
desc =>
diff --git a/src/mod_vcard_ldap.erl b/src/mod_vcard_ldap.erl
index c81c058f5..bc6e7ebca 100644
--- a/src/mod_vcard_ldap.erl
+++ b/src/mod_vcard_ldap.erl
@@ -571,7 +571,7 @@ mod_doc() ->
}]}}] ++
[{Opt,
#{desc =>
- {?T("Same as top-level '~s' option, but "
+ {?T("Same as top-level _`~s`_ option, but "
"applied to this module only."), [Opt]}}}
|| Opt <- [ldap_base, ldap_servers, ldap_uids,
ldap_deref_aliases, ldap_encrypt, ldap_password,
diff --git a/src/mod_vcard_xupdate.erl b/src/mod_vcard_xupdate.erl
index ab8df2c60..59ebc7f71 100644
--- a/src/mod_vcard_xupdate.erl
+++ b/src/mod_vcard_xupdate.erl
@@ -228,26 +228,26 @@ mod_doc() ->
"frequently their presence. However, the overhead is significantly "
"reduced by the use of caching, so you probably don't want "
"to set 'use_cache' to 'false'."), "",
- ?T("The module depends on 'mod_vcard'."), "",
+ ?T("The module depends on _`mod_vcard`_."), "",
?T("NOTE: Nowadays https://xmpp.org/extensions/xep-0153.html"
"[XEP-0153] is used mostly as \"read-only\", i.e. modern "
"clients don't publish their avatars inside vCards. Thus "
"in the majority of cases the module is only used along "
- "with 'mod_avatar' module for providing backward compatibility.")],
+ "with _`mod_avatar`_ for providing backward compatibility.")],
opts =>
[{use_cache,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'use_cache' option, but applied to this module only.")}},
+ ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}},
{cache_size,
#{value => "pos_integer() | infinity",
desc =>
- ?T("Same as top-level 'cache_size' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}},
{cache_missed,
#{value => "true | false",
desc =>
- ?T("Same as top-level 'cache_missed' option, but applied to this module only.")}},
+ ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}},
{cache_life_time,
#{value => "timeout()",
desc =>
- ?T("Same as top-level 'cache_life_time' option, but applied to this module only.")}}]}.
+ ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}.
diff --git a/src/node_flat.erl b/src/node_flat.erl
index 4a2a60971..55dea0d8d 100644
--- a/src/node_flat.erl
+++ b/src/node_flat.erl
@@ -39,7 +39,8 @@
-export([init/3, terminate/2, options/0, features/0,
create_node_permission/6, create_node/2, delete_node/1,
purge_node/2, subscribe_node/8, unsubscribe_node/4,
- publish_item/7, delete_item/4, remove_extra_items/3,
+ publish_item/7, delete_item/4,
+ remove_extra_items/2, remove_extra_items/3, remove_expired_items/2,
get_entity_affiliations/2, get_node_affiliations/1,
get_affiliation/2, set_affiliation/3,
get_entity_subscriptions/2, get_node_subscriptions/1,
@@ -375,7 +376,8 @@ publish_item(Nidx, Publisher, PublishModel, MaxItems, ItemId, Payload,
or (Subscribed == true)) ->
{error, xmpp:err_forbidden()};
true ->
- if MaxItems > 0 ->
+ if MaxItems > 0;
+ MaxItems == unlimited ->
Now = erlang:timestamp(),
case get_item(Nidx, ItemId) of
{result, #pubsub_item{creation = {_, GenKey}} = OldItem} ->
@@ -402,6 +404,16 @@ publish_item(Nidx, Publisher, PublishModel, MaxItems, ItemId, Payload,
end
end.
+remove_extra_items(Nidx, MaxItems) ->
+ {result, States} = get_states(Nidx),
+ Records = States ++ mnesia:read({pubsub_orphan, Nidx}),
+ ItemIds = lists:flatmap(fun(#pubsub_state{items = Is}) ->
+ Is;
+ (#pubsub_orphan{items = Is}) ->
+ Is
+ end, Records),
+ remove_extra_items(Nidx, MaxItems, ItemIds).
+
%% @doc <p>This function is used to remove extra items, most notably when the
%% maximum number of items has been reached.</p>
%% <p>This function is used internally by the core PubSub module, as no
@@ -420,6 +432,22 @@ remove_extra_items(Nidx, MaxItems, ItemIds) ->
del_items(Nidx, OldItems),
{result, {NewItems, OldItems}}.
+remove_expired_items(_Nidx, infinity) ->
+ {result, []};
+remove_expired_items(Nidx, Seconds) ->
+ Items = mnesia:index_read(pubsub_item, Nidx, #pubsub_item.nodeidx),
+ ExpT = misc:usec_to_now(
+ erlang:system_time(microsecond) - (Seconds * 1000000)),
+ ExpItems = lists:filtermap(
+ fun(#pubsub_item{itemid = {ItemId, _},
+ modification = {ModT, _}}) when ModT < ExpT ->
+ {true, ItemId};
+ (#pubsub_item{}) ->
+ false
+ end, Items),
+ del_items(Nidx, ExpItems),
+ {result, ExpItems}.
+
%% @doc <p>Triggers item deletion.</p>
%% <p>Default plugin: The user performing the deletion must be the node owner
%% or a publisher, or PublishModel being open.</p>
@@ -945,15 +973,12 @@ rsm_page(Count, Index, Offset, Items) ->
last = Last}.
encode_stamp(Stamp) ->
- case catch xmpp_util:decode_timestamp(Stamp) of
- {MS,S,US} -> {MS,S,US};
- _ -> Stamp
+ try xmpp_util:decode_timestamp(Stamp)
+ catch _:{bad_timestamp, _} ->
+ Stamp % We should return a proper error to the client instead.
end.
decode_stamp(Stamp) ->
- case catch xmpp_util:encode_timestamp(Stamp) of
- TimeStamp when is_binary(TimeStamp) -> TimeStamp;
- _ -> Stamp
- end.
+ xmpp_util:encode_timestamp(Stamp).
transform({pubsub_state, {Id, Nidx}, Is, A, Ss}) ->
{pubsub_state, {Id, Nidx}, Nidx, Is, A, Ss};
diff --git a/src/node_flat_sql.erl b/src/node_flat_sql.erl
index 1e197a51d..f9c8a209d 100644
--- a/src/node_flat_sql.erl
+++ b/src/node_flat_sql.erl
@@ -40,9 +40,10 @@
-include("translate.hrl").
-export([init/3, terminate/2, options/0, features/0,
- create_node_permission/6, create_node/2, delete_node/1,
- purge_node/2, subscribe_node/8, unsubscribe_node/4,
- publish_item/7, delete_item/4, remove_extra_items/3,
+ create_node_permission/6, create_node/2, delete_node/1, purge_node/2,
+ subscribe_node/8, unsubscribe_node/4,
+ publish_item/7, delete_item/4,
+ remove_extra_items/2, remove_extra_items/3, remove_expired_items/2,
get_entity_affiliations/2, get_node_affiliations/1,
get_affiliation/2, set_affiliation/3,
get_entity_subscriptions/2, get_node_subscriptions/1,
@@ -247,7 +248,8 @@ publish_item(Nidx, Publisher, PublishModel, MaxItems, ItemId, Payload,
or (Subscribed == true)) ->
{error, xmpp:err_forbidden()};
true ->
- if MaxItems > 0 ->
+ if MaxItems > 0;
+ MaxItems == unlimited ->
Now = erlang:timestamp(),
case get_item(Nidx, ItemId) of
{result, #pubsub_item{creation = {_, GenKey}} = OldItem} ->
@@ -258,20 +260,23 @@ publish_item(Nidx, Publisher, PublishModel, MaxItems, ItemId, Payload,
{result, _} ->
{error, xmpp:err_forbidden()};
_ ->
- Items = [ItemId | itemids(Nidx, GenKey)],
- {result, {_NI, OI}} = remove_extra_items(Nidx, MaxItems, Items),
+ OldIds = maybe_remove_extra_items(Nidx, MaxItems,
+ GenKey, ItemId),
set_item(#pubsub_item{
itemid = {ItemId, Nidx},
creation = {Now, GenKey},
modification = {Now, SubKey},
payload = Payload}),
- {result, {default, broadcast, OI}}
+ {result, {default, broadcast, OldIds}}
end;
true ->
{result, {default, broadcast, []}}
end
end.
+remove_extra_items(Nidx, MaxItems) ->
+ remove_extra_items(Nidx, MaxItems, itemids(Nidx)).
+
remove_extra_items(_Nidx, unlimited, ItemIds) ->
{result, {ItemIds, []}};
remove_extra_items(Nidx, MaxItems, ItemIds) ->
@@ -280,6 +285,23 @@ remove_extra_items(Nidx, MaxItems, ItemIds) ->
del_items(Nidx, OldItems),
{result, {NewItems, OldItems}}.
+remove_expired_items(_Nidx, infinity) ->
+ {result, []};
+remove_expired_items(Nidx, Seconds) ->
+ ExpT = encode_now(
+ misc:usec_to_now(
+ erlang:system_time(microsecond) - (Seconds * 1000000))),
+ case ejabberd_sql:sql_query_t(
+ ?SQL("select @(itemid)s from pubsub_item where nodeid=%(Nidx)d "
+ "and creation < %(ExpT)s")) of
+ {selected, RItems} ->
+ ItemIds = [ItemId || {ItemId} <- RItems],
+ del_items(Nidx, ItemIds),
+ {result, ItemIds};
+ _ ->
+ {result, []}
+ end.
+
delete_item(Nidx, Publisher, PublishModel, ItemId) ->
SubKey = jid:tolower(Publisher),
GenKey = jid:remove_resource(SubKey),
@@ -862,6 +884,18 @@ first_in_list(Pred, [H | T]) ->
_ -> first_in_list(Pred, T)
end.
+itemids(Nidx) ->
+ case catch
+ ejabberd_sql:sql_query_t(
+ ?SQL("select @(itemid)s from pubsub_item where "
+ "nodeid=%(Nidx)d order by modification desc"))
+ of
+ {selected, RItems} ->
+ [ItemId || {ItemId} <- RItems];
+ _ ->
+ []
+ end.
+
itemids(Nidx, {_U, _S, _R} = JID) ->
SJID = encode_jid(JID),
SJIDLike = <<(encode_jid_like(JID))/binary, "/%">>,
@@ -933,6 +967,16 @@ update_subscription(Nidx, JID, Subscription) ->
"-affiliation='n'"
]).
+-spec maybe_remove_extra_items(mod_pubsub:nodeIdx(),
+ non_neg_integer() | unlimited, ljid(),
+ mod_pubsub:itemId()) -> [mod_pubsub:itemId()].
+maybe_remove_extra_items(_Nidx, unlimited, _GenKey, _ItemId) ->
+ [];
+maybe_remove_extra_items(Nidx, MaxItems, GenKey, ItemId) ->
+ ItemIds = [ItemId | itemids(Nidx, GenKey)],
+ {result, {_NewIds, OldIds}} = remove_extra_items(Nidx, MaxItems, ItemIds),
+ OldIds.
+
-spec decode_jid(SJID :: binary()) -> ljid().
decode_jid(SJID) ->
jid:tolower(jid:decode(SJID)).
@@ -1037,15 +1081,14 @@ rsm_page(Count, Index, Offset, Items) ->
last = Last}.
encode_stamp(Stamp) ->
- case catch xmpp_util:decode_timestamp(Stamp) of
- {MS,S,US} -> encode_now({MS,S,US});
- _ -> Stamp
+ try xmpp_util:decode_timestamp(Stamp) of
+ Now ->
+ encode_now(Now)
+ catch _:{bad_timestamp, _} ->
+ Stamp % We should return a proper error to the client instead.
end.
decode_stamp(Stamp) ->
- case catch xmpp_util:encode_timestamp(decode_now(Stamp)) of
- TimeStamp when is_binary(TimeStamp) -> TimeStamp;
- _ -> Stamp
- end.
+ xmpp_util:encode_timestamp(decode_now(Stamp)).
encode_now({T1, T2, T3}) ->
<<(misc:i2l(T1, 6))/binary, ":",
diff --git a/src/node_pep.erl b/src/node_pep.erl
index 58c3050a0..a30f8cb91 100644
--- a/src/node_pep.erl
+++ b/src/node_pep.erl
@@ -35,7 +35,8 @@
-export([init/3, terminate/2, options/0, features/0,
create_node_permission/6, create_node/2, delete_node/1,
purge_node/2, subscribe_node/8, unsubscribe_node/4,
- publish_item/7, delete_item/4, remove_extra_items/3,
+ publish_item/7, delete_item/4,
+ remove_extra_items/2, remove_extra_items/3, remove_expired_items/2,
get_entity_affiliations/2, get_node_affiliations/1,
get_affiliation/2, set_affiliation/3,
get_entity_subscriptions/2, get_node_subscriptions/1,
@@ -135,9 +136,15 @@ publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload, PubOpts) ->
node_flat:publish_item(Nidx, Publisher, Model, MaxItems, ItemId,
Payload, PubOpts).
+remove_extra_items(Nidx, MaxItems) ->
+ node_flat:remove_extra_items(Nidx, MaxItems).
+
remove_extra_items(Nidx, MaxItems, ItemIds) ->
node_flat:remove_extra_items(Nidx, MaxItems, ItemIds).
+remove_expired_items(Nidx, Seconds) ->
+ node_flat:remove_expired_items(Nidx, Seconds).
+
delete_item(Nidx, Publisher, PublishModel, ItemId) ->
node_flat:delete_item(Nidx, Publisher, PublishModel, ItemId).
diff --git a/src/node_pep_sql.erl b/src/node_pep_sql.erl
index 7b21aa901..3bb66bc4c 100644
--- a/src/node_pep_sql.erl
+++ b/src/node_pep_sql.erl
@@ -37,7 +37,8 @@
-export([init/3, terminate/2, options/0, features/0,
create_node_permission/6, create_node/2, delete_node/1,
purge_node/2, subscribe_node/8, unsubscribe_node/4,
- publish_item/7, delete_item/4, remove_extra_items/3,
+ publish_item/7, delete_item/4,
+ remove_extra_items/2, remove_extra_items/3, remove_expired_items/2,
get_entity_affiliations/2, get_node_affiliations/1,
get_affiliation/2, set_affiliation/3,
get_entity_subscriptions/2, get_node_subscriptions/1,
@@ -92,9 +93,15 @@ publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload, PubOpts) ->
node_flat_sql:publish_item(Nidx, Publisher, Model, MaxItems, ItemId,
Payload, PubOpts).
+remove_extra_items(Nidx, MaxItems) ->
+ node_flat_sql:remove_extra_items(Nidx, MaxItems).
+
remove_extra_items(Nidx, MaxItems, ItemIds) ->
node_flat_sql:remove_extra_items(Nidx, MaxItems, ItemIds).
+remove_expired_items(Nidx, Seconds) ->
+ node_flat_sql:remove_expired_items(Nidx, Seconds).
+
delete_item(Nidx, Publisher, PublishModel, ItemId) ->
node_flat_sql:delete_item(Nidx, Publisher, PublishModel, ItemId).
diff --git a/src/nodetree_tree.erl b/src/nodetree_tree.erl
index fe15f3323..853c1fb93 100644
--- a/src/nodetree_tree.erl
+++ b/src/nodetree_tree.erl
@@ -46,7 +46,8 @@
-export([init/3, terminate/2, options/0, set_node/1,
get_node/3, get_node/2, get_node/1, get_nodes/2,
- get_nodes/1, get_parentnodes/3, get_parentnodes_tree/3,
+ get_nodes/1, get_all_nodes/1,
+ get_parentnodes/3, get_parentnodes_tree/3,
get_subnodes/3, get_subnodes_tree/3, create_node/6,
delete_node/2]).
@@ -98,6 +99,14 @@ get_nodes(Host, Limit) ->
{Nodes, _} -> Nodes
end.
+get_all_nodes({_U, _S, _R} = Owner) ->
+ Host = jid:tolower(jid:remove_resource(Owner)),
+ mnesia:match_object(#pubsub_node{nodeid = {Host, '_'}, _ = '_'});
+get_all_nodes(Host) ->
+ mnesia:match_object(#pubsub_node{nodeid = {Host, '_'}, _ = '_'})
+ ++ mnesia:match_object(#pubsub_node{nodeid = {{'_', Host, '_'}, '_'},
+ _ = '_'}).
+
get_parentnodes(Host, Node, _From) ->
case catch mnesia:read({pubsub_node, {Host, Node}}) of
[Record] when is_record(Record, pubsub_node) ->
diff --git a/src/nodetree_tree_sql.erl b/src/nodetree_tree_sql.erl
index d68355202..402c50901 100644
--- a/src/nodetree_tree_sql.erl
+++ b/src/nodetree_tree_sql.erl
@@ -45,7 +45,8 @@
-export([init/3, terminate/2, options/0, set_node/1,
get_node/3, get_node/2, get_node/1, get_nodes/2,
- get_nodes/1, get_parentnodes/3, get_parentnodes_tree/3,
+ get_nodes/1, get_all_nodes/1,
+ get_parentnodes/3, get_parentnodes_tree/3,
get_subnodes/3, get_subnodes_tree/3, create_node/6,
delete_node/2]).
@@ -165,6 +166,34 @@ get_nodes(Host, Limit) ->
[]
end.
+get_all_nodes({_U, _S, _R} = JID) ->
+ SubKey = jid:tolower(JID),
+ GenKey = jid:remove_resource(SubKey),
+ EncKey = node_flat_sql:encode_jid(GenKey),
+ Pattern = <<(node_flat_sql:encode_jid_like(GenKey))/binary, "/%">>,
+ case ejabberd_sql:sql_query_t(
+ ?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d "
+ "from pubsub_node where host=%(EncKey)s "
+ "or host like %(Pattern)s %ESCAPE")) of
+ {selected, RItems} ->
+ [raw_to_node(GenKey, Item) || Item <- RItems];
+ _ ->
+ []
+ end;
+get_all_nodes(Host) ->
+ Pattern1 = <<"%@", Host/binary>>,
+ Pattern2 = <<"%@", Host/binary, "/%">>,
+ case ejabberd_sql:sql_query_t(
+ ?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d "
+ "from pubsub_node where host=%(Host)s "
+ "or host like %(Pattern1)s "
+ "or host like %(Pattern2)s %ESCAPE")) of
+ {selected, RItems} ->
+ [raw_to_node(Host, Item) || Item <- RItems];
+ _ ->
+ []
+ end.
+
get_parentnodes(Host, Node, _From) ->
case get_node(Host, Node) of
Record when is_record(Record, pubsub_node) ->
diff --git a/src/nodetree_virtual.erl b/src/nodetree_virtual.erl
index 9cf7a80ca..c0274a795 100644
--- a/src/nodetree_virtual.erl
+++ b/src/nodetree_virtual.erl
@@ -38,7 +38,8 @@
-export([init/3, terminate/2, options/0, set_node/1,
get_node/3, get_node/2, get_node/1, get_nodes/2,
- get_nodes/1, get_parentnodes/3, get_parentnodes_tree/3,
+ get_nodes/1, get_all_nodes/1,
+ get_parentnodes/3, get_parentnodes_tree/3,
get_subnodes/3, get_subnodes_tree/3, create_node/6,
delete_node/2]).
@@ -71,6 +72,9 @@ get_nodes(Host) ->
get_nodes(_Host, _Limit) ->
[].
+get_all_nodes(_Host) ->
+ [].
+
get_parentnodes(_Host, _Node, _From) ->
[].
diff --git a/test/offline_tests.erl b/test/offline_tests.erl
index 1021c86e8..b5a90e7ee 100644
--- a/test/offline_tests.erl
+++ b/test/offline_tests.erl
@@ -489,6 +489,14 @@ wait_for_complete(Config, N) ->
end
end, error, [0, 100, 200, 2000, 5000, 10000]).
+xevent_stored(#message{body = [], subject = []}, _) -> false;
+xevent_stored(#message{type = T}, _) when T /= chat, T /= normal -> false;
+xevent_stored(_, #xevent{id = undefined}) -> true;
+xevent_stored(_, #xevent{offline = true}) -> true;
+xevent_stored(_, #xevent{delivered = true}) -> true;
+xevent_stored(_, #xevent{displayed = true}) -> true;
+xevent_stored(_, _) -> false.
+
message_iterator(Config) ->
ServerJID = server_jid(Config),
ChatStates = [[#chatstate{type = composing}]],
@@ -511,8 +519,14 @@ message_iterator(Config) ->
fun(#message{type = error}) -> true;
(#message{type = groupchat}) -> false;
(#message{sub_els = [#hint{type = store}|_]}) when MamEnabled -> true;
+ (#message{sub_els = [#hint{type = 'no-store'}|_]}) -> false;
(#message{sub_els = [#offline{}|_]}) when not MamEnabled -> false;
- (#message{sub_els = [_, #xevent{id = I}]}) when I /= undefined, not MamEnabled -> false;
+ (#message{sub_els = [#hint{type = store}, #xevent{} = Event | _]} = Msg) when not MamEnabled ->
+ xevent_stored(Msg#message{body = body, type = chat}, Event);
+ (#message{sub_els = [#xevent{} = Event]} = Msg) when not MamEnabled ->
+ xevent_stored(Msg, Event);
+ (#message{sub_els = [_, #xevent{} = Event | _]} = Msg) when not MamEnabled ->
+ xevent_stored(Msg, Event);
(#message{sub_els = [#xevent{id = I}]}) when I /= undefined, not MamEnabled -> false;
(#message{sub_els = [#hint{type = store}|_]}) -> true;
(#message{sub_els = [#hint{type = 'no-store'}|_]}) -> false;
diff --git a/test/roster_tests.erl b/test/roster_tests.erl
index a3b6009c9..3092b8cd8 100644
--- a/test/roster_tests.erl
+++ b/test/roster_tests.erl
@@ -224,13 +224,21 @@ get_items(Config, Version) ->
sub_els = [#roster_query{ver = Version}]}) of
#iq{type = result,
sub_els = [#roster_query{ver = NewVersion, items = Items}]} ->
- {NewVersion, Items};
+ {NewVersion, normalize_items(Items)};
#iq{type = result, sub_els = []} ->
{empty, []};
#iq{type = error} = Err ->
xmpp:get_error(Err)
end.
+normalize_items(Items) ->
+ Items2 =
+ lists:map(
+ fun(I) ->
+ I#roster_item{groups = lists:sort(I#roster_item.groups)}
+ end, Items),
+ lists:sort(Items2).
+
get_item(Config, JID) ->
case get_items(Config) of
{_Ver, Items} when is_list(Items) ->
diff --git a/tools/captcha-ng.sh b/tools/captcha-ng.sh
index cbcb95407..bb57385c4 100755
--- a/tools/captcha-ng.sh
+++ b/tools/captcha-ng.sh
@@ -42,7 +42,8 @@ INTRUDER()
{
NUMBERS=$(echo "$INPUT" | grep -o . | tr '\n' ' ')
SORTED_UNIQ_NUM=$(echo "${NUMBERS[@]}" | sort -u | tr '\n' ' ')
-RANDOM_DIGITS=$(echo 123456789 | grep -o . | sort -R | tr '\n' ' ')
+SORT_RANDOM_CMD="$( ( echo x|sort -R >&/dev/null && echo "sort -R" ) || ( echo x|shuf >&/dev/null && echo shuf ) || echo cat)"
+RANDOM_DIGITS=$(echo 123456789 | grep -o . | eval "$SORT_RANDOM_CMD" | tr '\n' ' ')
INTRUDER=-1
for i in $RANDOM_DIGITS