diff options
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()]}. @@ -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 @@ -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 |