diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d498760be..8cd9a38c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,12 +25,12 @@ jobs: strategy: fail-fast: false matrix: - otp: ['19.3', '24.0'] + otp: ['19.3', '24'] include: - otp: '19.3' rebar: 2 os: ubuntu-18.04 - - otp: '24.0' + - otp: '24' rebar: 3 os: ubuntu-20.04 runs-on: ${{ matrix.os }} @@ -44,9 +44,17 @@ jobs: - uses: actions/checkout@v2 + - name: Test shell scripts + if: matrix.otp == 24 + run: | + shellcheck test/ejabberd_SUITE_data/gencerts.sh + shellcheck tools/captcha.sh + shellcheck ejabberd.init.template + shellcheck -x ejabberdctl.template + - name: Get previous Erlang/OTP uses: ErlGang/setup-erlang@master - if: matrix.otp != 24.0 + if: matrix.otp != 24 with: otp-version: ${{ matrix.otp }} @@ -81,7 +89,6 @@ jobs: libsqlite3-dev libwebp-dev libyaml-dev - name: Prepare rebar - id: rebar run: | echo '{xref_ignores, [{eldap_filter_yecc, return_error, 2} ]}.' >>rebar.config @@ -93,23 +100,26 @@ jobs: mqtree, p1_acme, p1_mysql, p1_oauth2, p1_pgsql, p1_utils, pkix, sqlite3, stringprep, stun, xmpp, yconf]} ]}.' >>rebar.config echo '{ct_extra_params, "-verbosity 20"}.' >>rebar.config + echo "{ct_opts, [{verbosity, 20}, {keep_logs, 20}]}." >>rebar.config - - name: Cache rebar2 - if: matrix.rebar == 2 + - name: Cache rebar uses: actions/cache@v2 with: path: | deps/ dialyzer/ ebin/ + ~/.cache/rebar3/ key: ${{matrix.otp}}-${{matrix.rebar}}-${{hashFiles('rebar.config')}} - - name: Cache rebar3 - if: matrix.rebar == 3 - uses: actions/cache@v2 - with: - path: ~/.cache/rebar3/ - key: ${{matrix.otp}}-${{matrix.rebar}}-${{hashFiles('rebar.config')}} + - name: Download test logs + if: matrix.otp == 24 && github.repository == 'processone/ejabberd' + continue-on-error: true + run: | + mkdir -p _build/test + curl -sSL https://github.com/processone/ecil/tarball/gh-pages | + tar -C _build/test --strip-components=1 --wildcards -xzf - + rm -rf _build/test/logs/last/ - name: Compile run: | @@ -128,46 +138,50 @@ jobs: - run: make hooks - run: make options - run: make xref - - run: make dialyzer - - run: make test + - run: | + make dialyzer + [ ${{ matrix.rebar }} = 3 ] && true \ + || { cat dialyzer/error.log ; test ! -s dialyzer/error.log ; } + + - name: Run tests + if: matrix.otp != 24 + run: make test + - name: Run tests (OTP 24) + if: matrix.otp == 24 + id: ct + run: | + (cd priv && ln -sf ../sql) + COMMIT=`echo $GITHUB_SHA | cut -c 1-7` + DATE=`date +%s` + REF_NAME=`echo $GITHUB_REF_NAME | tr "/" "_"` + NODENAME=$DATE@$GITHUB_RUN_NUMBER-$GITHUB_ACTOR-$REF_NAME-$COMMIT + LABEL=`git show -s --format=%s | cut -c 1-30` + rebar3 ct --name $NODENAME --label "$LABEL" + rebar3 cover - name: Check results if: always() run: | - [[ -d _build ]] && ln -s _build/test/logs/ logs \ - && ln `find _build/ -name "*dialyzer_warnings"` \ - logs/dialyzer.log \ - || ln dialyzer/error.log logs/dialyzer.log + [[ -d _build ]] && ln -s _build/test/logs/last/ logs || true 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 dialyzer report - run: cat logs/dialyzer.log - - - name: View full suite.log - run: cat logs/suite.log - - - name: View suite.log failures + - name: View logs 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 exunit.log - if: failure() - run: find logs/ -name exunit.log -exec cat '{}' ';' + run: | + cat logs/suite.log | awk \ + 'BEGIN{RS="\n=case";FS="\n"} /=result\s*failed/ {print "=case" $0}' + find logs/ -name error.log -exec cat '{}' ';' + find logs/ -name exunit.log -exec cat '{}' ';' - name: Send to coveralls - if: matrix.otp == 24.0 + if: matrix.otp == 24 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 +189,61 @@ jobs: "payload":{"build_num":$GITHUB_RUN_ID, "status":"done"}}' + - name: Upload test logs + if: always() && steps.ct.outcome == 'failure' && github.repository == 'processone/ejabberd' + uses: peaceiris/actions-gh-pages@v3 + with: + publish_dir: _build/test + exclude_assets: '.github,lib,plugins' + external_repository: processone/ecil + deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }} + keep_files: true + + - name: View ECIL address + if: always() && steps.ct.outcome == 'failure' && github.repository == 'processone/ejabberd' + run: | + CTRUN=`ls -la _build/test/logs/last | sed 's|.*-> ||'` + echo "::notice::View CT results: https://processone.github.io/ecil/logs/$CTRUN/" + + - name: Prepare new schema + run: | + [[ -d logs ]] && rm -rf logs + [[ -d _build/test/logs ]] && rm -rf _build/test/logs || true + 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 + id: ctnewschema + - name: Check results + if: always() && steps.ctnewschema.outcome != 'skipped' + run: | + [[ -d _build ]] && ln -s _build/test/logs/last/ logs || true + 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 logs failures + if: failure() && steps.ctnewschema.outcome != 'skipped' + run: | + cat logs/suite.log | awk \ + 'BEGIN{RS="\n=case";FS="\n"} /=result\s*failed/ {print "=case" $0}' + find logs/ -name error.log -exec cat '{}' ';' + find logs/ -name exunit.log -exec cat '{}' ';' + binaries: name: Binaries needs: [tests] diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 000000000..0b7131a2e --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1,4 @@ +disable=SC2016,SC2086,SC2089,SC2090 +external-sources=true +source=ejabberdctl.cfg.example +shell=sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 13385b864..288931b5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,50 @@ +# Version 21.12 + +Commands +- `create_room_with_opts`: Fixed when using SQL storage +- `change_room_option`: Add missing fields from config inside `mod_muc_admin:change_options` +- piefxis: Fixed arguments of all commands + +Modules +- mod_caps: Don't forget caps on XEP-0198 resumption +- mod_conversejs: New module to serve a simple page for Converse.js +- mod_http_upload_quota: Avoid `max_days` race +- mod_muc: Support MUC hats (XEP-0317, conversejs/prosody compatible) +- mod_muc: Optimize MucSub processing +- mod_muc: Fix exception in mucsub {un}subscription events multicast handler +- mod_multicast: Improve and optimize multicast routing code +- mod_offline: Allow storing non-composing x:events in offline +- mod_ping: Send ping from server, not bare user JID +- mod_push: Fix handling of MUC/Sub messages +- mod_register: New allow_modules option to restrict registration modules +- mod_register_web: Handle unknown host gracefully +- mod_register_web: Use mod_register configured restrictions + +PubSub +- Add `delete_expired_pubsub_items` command +- Add `delete_old_pubsub_items` command +- Optimize publishing on large nodes (SQL) +- Support unlimited number of items +- Support `max_items=max` node configuration +- Bump default value for `max_items` limit from 10 to 1000 +- Use configured `max_items` by default +- node_flat: Avoid catch-all clauses for RSM +- node_flat_sql: Avoid catch-all clauses for RSM + +SQL +- Use `INSERT ... ON CONFLICT` in SQL_UPSERT for PostgreSQL >= 9.5 +- mod_mam export: assign MUC entries to the MUC service +- MySQL: Fix typo when creating index +- PgSQL: Add SASL auth support, PostgreSQL 14 +- PgSQL: Add missing SQL migration for table `push_session` +- PgSQL: Fix `vcard_search` definition in pgsql new schema + +Other +- `captcha-ng.sh`: "sort -R" command not POSIX, added "shuf" and "cat" as fallback +- Make s2s connection table cleanup more robust +- Update export/import of scram password to XEP-0227 1.1 +- Update Jose to 1.11.1 (the last in hex.pm correctly versioned) + # Version 21.07 Compilation diff --git a/Makefile.in b/Makefile.in index 6549665f7..3066488f0 100644 --- a/Makefile.in +++ b/Makefile.in @@ -68,12 +68,6 @@ LUADIR = $(PRIVDIR)/lua # /var/lib/ejabberd/ 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 @@ -100,8 +94,10 @@ endif ifeq "$(MIX)" "mix" REBAR_VER:=6 +REBAR_VER_318:=0 else REBAR_VER:=$(shell $(REBAR) --version | awk -F '[ .]' '/rebar / {print $$2}') +REBAR_VER_318:=$(shell $(REBAR) --version | awk -F '[ .]' '/rebar / {print ($$2 == 3 && $$3 >= 18 ? 1 : 0)}') endif ifeq "$(REBAR_VER)" "6" @@ -121,7 +117,11 @@ else ifeq "$(REBAR_VER)" "3" SKIPDEPS= LISTDEPS=tree +ifeq "$(REBAR_VER_318)" "1" + UPDATEDEPS=upgrade --all +else UPDATEDEPS=upgrade +endif DEPSPATTERN="s/ (.*//; /^ / s/.* \([a-z0-9_]*\).*/\1/p;" DEPSBASE=_build DEPSDIR=$(DEPSBASE)/default/lib @@ -249,7 +249,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 +306,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,12 +315,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) - $(CHOWN_COMMAND) -R @INSTALLUSER@ $(CTLLOCKDIR) >$(CHOWN_OUTPUT) - chmod -R 750 $(CTLLOCKDIR) # # Log directory $(INSTALL) -d -m 750 $(O_USER) $(LOGDIR) @@ -361,7 +364,6 @@ uninstall-all: uninstall-binary rm -rf $(ETCDIR) rm -rf $(EJABBERDDIR) rm -rf $(SPOOLDIR) - rm -rf $(CTLLOCKDIR) rm -rf $(LOGDIR) clean: 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/ejabberdctl.template b/ejabberdctl.template index 4cab19977..408a4fe6d 100755 --- a/ejabberdctl.template +++ b/ejabberdctl.template @@ -12,6 +12,8 @@ ERLANG_NODE=ejabberd@localhost # define default environment variables [ -z "$SCRIPT" ] && SCRIPT=$0 SCRIPT_DIR="$(cd "$(dirname "$SCRIPT")" && pwd -P)" +# shellcheck disable=SC2034 +ERTS_VSN="{{erts_vsn}}" ERL="{{erl}}" IEX="{{bindir}}/iex" EPMD="{{epmd}}" @@ -62,6 +64,7 @@ done : "${EJABBERDCTL_CONFIG_PATH:="$ETC_DIR/ejabberdctl.cfg"}" # Allows passing extra Erlang command-line arguments in vm.args file : "${VMARGS:="$ETC_DIR/vm.args"}" +# shellcheck source=ejabberdctl.cfg.example [ -f "$EJABBERDCTL_CONFIG_PATH" ] && . "$EJABBERDCTL_CONFIG_PATH" [ -n "$ERLANG_NODE_ARG" ] && ERLANG_NODE="$ERLANG_NODE_ARG" [ "$ERLANG_NODE" = "${ERLANG_NODE%.*}" ] && S="-s" @@ -81,7 +84,7 @@ if [ -n "$INET_DIST_INTERFACE" ] ; then fi # if vm.args file exists in config directory, pass it to Erlang VM [ -f "$VMARGS" ] && ERLANG_OPTS="$ERLANG_OPTS -args_file $VMARGS" -ERL_LIBS={{libdir}} +ERL_LIBS='{{libdir}}' ERL_CRASH_DUMP="$LOGS_DIR"/erl_crash_$(date "+%Y%m%d-%H%M%S").dump ERL_INETRC="$ETC_DIR"/inetrc @@ -105,6 +108,7 @@ export ERL_MAX_ETS_TABLES export CONTRIB_MODULES_PATH export CONTRIB_MODULES_CONF_DIR export ERL_LIBS +export SCRIPT_DIR # run command either directly or via su $INSTALLUSER exec_cmd() @@ -128,14 +132,6 @@ exec_iex() # usage debugwarning() { - if [ "$OSTYPE" != "cygwin" ] && [ "$OSTYPE" != "win32" ] ; then - if [ "a$TERM" = "a" ] || [ "$TERM" = "dumb" ] ; then - echo "Terminal type not supported." - echo "You may have to set the TERM environment variable to fix this." - exit 8 - fi - fi - if [ "$EJABBERD_BYPASS_WARNINGS" != "true" ] ; then echo "--------------------------------------------------------------------" echo "" @@ -153,7 +149,7 @@ debugwarning() echo "To bypass permanently this warning, add to ejabberdctl.cfg the line:" echo " EJABBERD_BYPASS_WARNINGS=true" echo "Press return to continue" - read -r input + read -r _ echo "" fi } @@ -176,7 +172,7 @@ livewarning() echo "To bypass permanently this warning, add to ejabberdctl.cfg the line:" echo " EJABBERD_BYPASS_WARNINGS=true" echo "Press return to continue" - read -r input + read -r _ echo "" fi } @@ -206,8 +202,9 @@ help() uid() { uuid=$(uuidgen 2>/dev/null) + random=$(awk 'BEGIN { srand(); print int(rand()*32768) }' /dev/null) [ -z "$uuid" ] && [ -f /proc/sys/kernel/random/uuid ] && uuid=$(cat /proc/sys/kernel/random/uuid) - [ -z "$uuid" ] && uuid=$(printf "%X" "${RANDOM:-$$}$(date +%M%S)") + [ -z "$uuid" ] && uuid=$(printf "%X" "${random:-$$}$(date +%M%S)") uuid=$(printf '%s' $uuid | sed 's/^\(...\).*$/\1/') [ $# -eq 0 ] && echo "${uuid}-${ERLANG_NODE}" [ $# -eq 1 ] && echo "${uuid}-${1}-${ERLANG_NODE}" diff --git a/include/mod_muc_room.hrl b/include/mod_muc_room.hrl index bbe656575..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() }). @@ -124,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(), diff --git a/include/pubsub.hrl b/include/pubsub.hrl index 8496b95ac..da919e9e2 100644 --- a/include/pubsub.hrl +++ b/include/pubsub.hrl @@ -23,7 +23,7 @@ -define(ERR_EXTENDED(E, C), mod_pubsub:extended_error(E, C)). %% The actual limit can be configured with mod_pubsub's option max_items_node --define(MAXITEMS, 10). +-define(MAXITEMS, 1000). %% this is currently a hard limit. %% Would be nice to have it configurable. diff --git a/man/ejabberd.yml.5 b/man/ejabberd.yml.5 index f25f57250..8fb59c381 100644 --- a/man/ejabberd.yml.5 +++ b/man/ejabberd.yml.5 @@ -2,12 +2,12 @@ .\" Title: ejabberd.yml .\" Author: [see the "AUTHOR" section] .\" Generator: DocBook XSL Stylesheets v1.79.1 -.\" Date: 07/21/2021 +.\" Date: 12/08/2021 .\" Manual: \ \& .\" Source: \ \& .\" Language: English .\" -.TH "EJABBERD\&.YML" "5" "07/21/2021" "\ \&" "\ \&" +.TH "EJABBERD\&.YML" "5" "12/08/2021" "\ \&" "\ \&" .\" ----------------------------------------------------------------- .\" * Define some portability stuff .\" ----------------------------------------------------------------- @@ -82,7 +82,7 @@ All options can be changed in runtime by running \fIejabberdctl reload\-config\f .sp Some options can be specified for particular virtual host(s) only using \fIhost_config\fR or \fIappend_host_config\fR options\&. Such options are called \fIlocal\fR\&. Examples are \fImodules\fR, \fIauth_method\fR and \fIdefault_db\fR\&. The options that cannot be defined per virtual host are called \fIglobal\fR\&. Examples are \fIloglevel\fR, \fIcertfiles\fR and \fIlisten\fR\&. It is a configuration mistake to put \fIglobal\fR options under \fIhost_config\fR or \fIappend_host_config\fR section \- ejabberd will refuse to load such configuration\&. .sp -It is not recommended to write ejabberd\&.yml from scratch\&. Instead it is better to start from "default" configuration file available at https://github\&.com/processone/ejabberd/blob/21\&.07/ejabberd\&.yml\&.example\&. Once you get ejabberd running you can start changing configuration options to meet your requirements\&. +It is not recommended to write ejabberd\&.yml from scratch\&. Instead it is better to start from "default" configuration file available at https://github\&.com/processone/ejabberd/blob/21\&.12/ejabberd\&.yml\&.example\&. Once you get ejabberd running you can start changing configuration options to meet your requirements\&. .sp Note that this document is intended to provide comprehensive description of all configuration options that can be consulted to understand the meaning of a particular option, its format and possible values\&. It will be quite hard to understand how to configure ejabberd by reading this document only \- for this purpose the reader is recommended to read online Configuration Guide available at https://docs\&.ejabberd\&.im/admin/configuration\&. .SH "TOP LEVEL OPTIONS" @@ -316,14 +316,46 @@ means that the same username can be taken multiple times in anonymous login mode .PP \fBanonymous_protocol\fR: \fIlogin_anon | sasl_anon | both\fR .RS 4 +Define what anonymous protocol will be used: +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} \fIlogin_anon\fR means that the anonymous login method will be used\&. +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} \fIsasl_anon\fR means that the SASL Anonymous method will be used\&. -\fIboth\fR -means that SASL Anonymous and login anonymous are both enabled\&. The default value is -\fIsasl_anon\fR\&. .RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +\fIboth\fR +means that SASL Anonymous and login anonymous are both enabled\&. +.RE +.RE +.sp +The default value is \fIsasl_anon\fR\&. .PP \fBapi_permissions\fR: \fI[Permission, \&.\&.\&.]\fR .RS 4 @@ -375,7 +407,7 @@ A list of authentication methods to use\&. If several methods are defined, authe This is used by the contributed module \fIejabberd_auth_http\fR that can be installed from the -\fIejabberd\-contrib\fR +ejabberd\-contrib Git repository\&. Please refer to that module\(cqs README file for details\&. .RE .sp @@ -383,14 +415,36 @@ Git repository\&. Please refer to that module\(cqs README file for details\&. .PP \fBauth_password_format\fR: \fIplain | scram\fR .RS 4 -The option defines in what format the users passwords are stored\&. +The option defines in what format the users passwords are stored: +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} \fIplain\fR: 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\&. -\fIscram\fR: 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\&. +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +\fIscram\fR: 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\&. The default value is +\fIplain\fR\&. +.RE .RE .PP \fBauth_scram_hash\fR: \fIsha | sha256 | sha512\fR .RS 4 -Hash algorith that should be used to store password in SCRAM format\&. You shouldn\(cqt change this if you already have passwords generated with a different algorithm \- users that have such passwords will not be able to authenticate\&. +Hash algorith that should be used to store password in SCRAM format\&. You shouldn\(cqt change this if you already have passwords generated with a different algorithm \- users that have such passwords will not be able to authenticate\&. The default value is +\fIsha\fR\&. .RE .PP \fBauth_use_cache\fR: \fItrue | false\fR @@ -476,7 +530,7 @@ For server conections, this \fIca_file\fR option is overriden by the s2s_cafile \fBcache_life_time\fR: \fItimeout()\fR .RS 4 The time of a cached item to keep in cache\&. Once it\(cqs expired, the corresponding item is erased from cache\&. The default value is -\fIone hour\fR\&. Several modules have a similar option; and some core ejabberd parts support similar options too, see +\fI1 hour\fR\&. Several modules have a similar option; and some core ejabberd parts support similar options too, see \fIauth_cache_life_time\fR, \fIoauth_cache_life_time\fR, \fIrouter_cache_life_time\fR, and @@ -507,7 +561,9 @@ A maximum number of items (not memory!) in cache\&. The rule of thumb, for all t .PP \fBcaptcha_cmd\fR: \fIPath\fR .RS 4 -Full path to a script that generates CAPTCHA images\&. There is no default value: when this option is not set, CAPTCHA functionality is completely disabled\&. +Full path to a script that generates +CAPTCHA +images\&. There is no default value: when this option is not set, CAPTCHA functionality is completely disabled\&. .RE .PP \fBcaptcha_host\fR: \fIString\fR @@ -519,13 +575,17 @@ instead\&. .PP \fBcaptcha_limit\fR: \fIpos_integer() | infinity\fR .RS 4 -Maximum number of CAPTCHA generated images per minute for any given JID\&. The option is intended to protect the server from CAPTCHA DoS\&. The default value is +Maximum number of +CAPTCHA +generated images per minute for any given JID\&. The option is intended to protect the server from CAPTCHA DoS\&. The default value is \fIinfinity\fR\&. .RE .PP \fBcaptcha_url\fR: \fIURL\fR .RS 4 -An URL where CAPTCHA requests should be sent\&. NOTE: you need to configure +An URL where +CAPTCHA +requests should be sent\&. NOTE: you need to configure \fIrequest_handlers\fR for \fIejabberd_http\fR @@ -815,7 +875,8 @@ Path to the file that contains the JWK Key\&. The default value is .RS 4 The option defines the default language of server strings that can be seen by XMPP clients\&. If an XMPP client does not possess \fIxml:lang\fR -attribute, the specified language is used\&. +attribute, the specified language is used\&. The default value is +\fI"en"\fR\&. .RE .PP \fBldap_backups\fR: \fI[Host, \&.\&.\&.]\fR @@ -948,7 +1009,11 @@ section for details\&. \fBlog_rotate_count\fR: \fINumber\fR .RS 4 The number of rotated log files to keep\&. The default value is -\fI1\fR\&. +\fI1\fR, which means that only keeps +ejabberd\&.log\&.0, +error\&.log\&.0 +and +crash\&.log\&.0\&. .RE .PP \fBlog_rotate_size\fR: \fIpos_integer() | infinity\fR @@ -989,8 +1054,7 @@ seconds\&. .RS 4 This option can be used to tune tick time parameter of \fInet_kernel\fR\&. It tells Erlang VM how often nodes should check if intra\-node communication was not interrupted\&. This option 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 -\fI1\fR -minute\&. +\fI1 minute\fR\&. .RE .PP \fBnew_sql_schema\fR: \fItrue | false\fR @@ -998,7 +1062,7 @@ minute\&. Whether to use \fInew\fR SQL schema\&. All schemas are located at -https://github\&.com/processone/ejabberd/tree/21\&.07/sql\&. There are two schemas available\&. The default legacy schema allows to store one XMPP domain into one ejabberd database\&. The +https://github\&.com/processone/ejabberd/tree/21\&.12/sql\&. There are two schemas available\&. The default legacy schema allows to store one XMPP domain into one ejabberd database\&. The \fInew\fR schema allows to handle several XMPP domains in a single ejabberd database\&. Using this \fInew\fR @@ -1392,8 +1456,7 @@ if the latter is not set\&. \fBs2s_timeout\fR: \fItimeout()\fR .RS 4 A time to wait before closing an idle s2s connection\&. The default value is -\fI10\fR -minutes\&. +\fI10 minutes\fR\&. .RE .PP \fBs2s_tls_compression\fR: \fItrue | false\fR @@ -1795,24 +1858,7 @@ Details for some commands: \fIsrg\-create\fR: If you want to put a group Name with blankspaces, use the characters "\*(Aq and \*(Aq" to define when the Name starts and ends\&. See an example below\&. .RE .sp -.it 1 an-trap -.nr an-no-space-flag 1 -.nr an-break-flag 1 -.br -.ps +1 -\fBAvailable options:\fR -.RS 4 -.PP -\fBmodule_resource\fR: \fIResource\fR -.RS 4 -Indicate the resource that the XMPP stanzas must use in the FROM or TO JIDs\&. This is only useful in the -\fIget_vcard*\fR -and -\fIset_vcard*\fR -commands\&. The default value is -\fImod_admin_extra\fR\&. -.RE -.RE +The module has no options\&. .sp .it 1 an-trap .nr an-no-space-flag 1 @@ -1835,8 +1881,7 @@ access_rules: vcard_set: \- allow: adminextraresource modules: - mod_admin_extra: - module_resource: "modadminextraf8x,31ad" + mod_admin_extra: {} mod_vcard: access_set: vcard_set .fi @@ -1884,7 +1929,7 @@ ejabberdctl srg\-create g1 example\&.org "\*(AqGroup number 1\*(Aq" this_is_g1 g .RE .SS "mod_admin_update_sql" .sp -This module can be used to update existing SQL database from the default to the new schema\&. Check the section Default and New Schemas for details\&. Please note that only PostgreSQL is supported\&. When the module is loaded use \fIupdate_sql\fR ejabberdctl command\&. +This module can be used to update existing SQL database from the default to the new schema\&. Check the section Default and New Schemas for details\&. Please note that only PostgreSQL is supported\&. When the module is loaded use \fIupdate_sql\fR API\&. .sp The module has no options\&. .SS "mod_announce" @@ -2321,6 +2366,78 @@ While a client is inactive, queue presence stanzas that indicate (un)availabilit The module provides server configuration functionality via XEP\-0050: Ad\-Hoc Commands\&. This module requires \fImod_adhoc\fR to be loaded\&. .sp The module has no options\&. +.SS "mod_conversejs" +.sp +This module serves a simple page for the Converse XMPP web browser client\&. +.sp +To use this module, in addition to adding it to the \fImodules\fR section, you must also enable it in \fIlisten\fR → \fIejabberd_http\fR → request_handlers\&. +.sp +You must also setup either the option \fIwebsocket_url\fR or \fIbosh_service_url\fR\&. +.sp +By default, the options \fIconversejs_css\fR and \fIconversejs_script\fR point to the public Converse\&.js client\&. Alternatively, you can host the client locally using \fImod_http_fileserver\fR\&. +.sp +.it 1 an-trap +.nr an-no-space-flag 1 +.nr an-break-flag 1 +.br +.ps +1 +\fBAvailable options:\fR +.RS 4 +.PP +\fBbosh_service_url\fR: \fIBoshURL\fR +.RS 4 +BOSH service URL to which Converse\&.js can connect to\&. +.RE +.PP +\fBconversejs_css\fR: \fIURL\fR +.RS 4 +Converse\&.js CSS URL\&. +.RE +.PP +\fBconversejs_script\fR: \fIURL\fR +.RS 4 +Converse\&.js main script URL\&. +.RE +.PP +\fBdefault_domain\fR: \fIDomain\fR +.RS 4 +Specify a domain to act as the default for user JIDs\&. The default value is the first domain defined in the ejabberd configuration file\&. +.RE +.PP +\fBwebsocket_url\fR: \fIWebsocketURL\fR +.RS 4 +A websocket URL to which Converse\&.js can connect to\&. +.RE +.RE +.sp +.it 1 an-trap +.nr an-no-space-flag 1 +.nr an-break-flag 1 +.br +.ps +1 +\fBExample:\fR +.RS 4 +.sp +.if n \{\ +.RS 4 +.\} +.nf +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" +.fi +.if n \{\ +.RE +.\} +.RE .SS "mod_delegation" .sp This module is an implementation of XEP\-0355: Namespace Delegation\&. Only admin mode has been implemented by now\&. Namespace delegation allows external services to handle IQ using specific namespace\&. This may be applied for external PEP service\&. @@ -2479,11 +2596,11 @@ server_info: \- 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 @@ -2563,13 +2680,40 @@ The number of C2S authentication failures to trigger the IP ban\&. The default v .sp This module provides a ReST API to call ejabberd commands using JSON data\&. .sp -To use this module, in addition to adding it to the \fImodules\fR section, you must also add it to \fIrequest_handlers\fR of some listener\&. +To use this module, in addition to adding it to the \fImodules\fR section, you must also enable it in \fIlisten\fR → \fIejabberd_http\fR → request_handlers\&. .sp To use a specific API version N, when defining the URL path in the request_handlers, add a \fIvN\fR\&. For example: \fI/api/v2: mod_http_api\fR .sp To run a command, send a POST request to the corresponding URL: \fIhttp://localhost:5280/api/\fR .sp The module has no options\&. +.sp +.it 1 an-trap +.nr an-no-space-flag 1 +.nr an-break-flag 1 +.br +.ps +1 +\fBExample:\fR +.RS 4 +.sp +.if n \{\ +.RS 4 +.\} +.nf +listen: + \- + port: 5280 + module: ejabberd_http + request_handlers: + /api: mod_http_api + +modules: + mod_http_api: {} +.fi +.if n \{\ +.RE +.\} +.RE .SS "mod_http_fileserver" .sp This simple module serves files from the local disk over HTTP\&. @@ -2697,7 +2841,7 @@ modules: .sp This module allows for requesting permissions to upload a file via HTTP as described in 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\&. .sp -In order to use this module, it must be configured as a \fIrequest_handler\fR for \fIejabberd_http\fR listener\&. +In order to use this module, it must be enabled in \fIlisten\fR → \fIejabberd_http\fR → request_handlers\&. .sp .it 1 an-trap .nr an-no-space-flag 1 @@ -2746,7 +2890,9 @@ This option defines the permission bits of uploaded files\&. The bits are specif .PP \fBget_url\fR: \fIURL\fR .RS 4 -This option specifies the initial part of the GET URLs used for downloading the files\&. By default, it is set to the same value as +This option specifies the initial part of the GET URLs used for downloading the files\&. The default value is +\fIundefined\fR\&. When this option is +\fIundefined\fR, this option is set to the same value as \fIput_url\fR\&. The keyword @HOST@ is replaced with the virtual host name\&. NOTE: if GET requests are handled by \fImod_http_upload\fR, the \fIget_url\fR @@ -2795,7 +2941,7 @@ A name of the service in the Service Discovery\&. This will only be displayed by .PP \fBput_url\fR: \fIURL\fR .RS 4 -This option specifies the initial part of the PUT URLs 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"\&. +This option specifies the initial part of the PUT URLs 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/upload"\&. .RE .PP \fBrm_on_unregister\fR: \fItrue | false\fR @@ -3530,21 +3676,24 @@ This option specifies who is allowed to administrate the Multi\-User Chat servic .PP \fBaccess_create\fR: \fIAccessName\fR .RS 4 -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 allowed to create rooms\&. +To configure who is allowed to create new rooms at the Multi\-User Chat service, this option can be used\&. The default value is +\fIall\fR, which means everyone is allowed to create rooms\&. .RE .PP \fBaccess_mam\fR: \fIAccessName\fR .RS 4 To configure who is allowed to modify the \fImam\fR -room option\&. By default any account in the local ejabberd server is allowed to modify that option\&. +room option\&. The default value is +\fIall\fR, which means everyone is allowed to modify that option\&. .RE .PP \fBaccess_persistent\fR: \fIAccessName\fR .RS 4 To configure who is allowed to modify the \fIpersistent\fR -room option\&. By default any account in the local ejabberd server is allowed to modify that option\&. +room option\&. The default value is +\fIall\fR, which means everyone is allowed to modify that option\&. .RE .PP \fBaccess_register\fR: \fIAccessName\fR @@ -3825,7 +3974,8 @@ This option defines the number of service admins or room owners allowed to enter .PP \fBmax_users_presence\fR: \fINumber\fR .RS 4 -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\&. +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\&. The default value is +\fI1000\fR\&. .RE .PP \fBmin_message_interval\fR: \fINumber\fR @@ -4498,8 +4648,7 @@ This module implements support for XEP\-0199: XMPP Ping and periodic keepalives\ \fBping_ack_timeout\fR: \fItimeout()\fR .RS 4 How long to wait before deeming that a client has not answered a given server ping request\&. The default value is -\fI32\fR -seconds\&. +\fIundefined\fR\&. .RE .PP \fBping_interval\fR: \fItimeout()\fR @@ -4531,7 +4680,7 @@ means destroying the underlying connection, \fInone\fR means to do nothing\&. NOTE: when \fImod_stream_mgmt\fR -module is loaded and stream management is enabled by a client, killing the client connection doesn\(cqt mean killing the client session \- the session will be kept alive in order to give the client a chance to resume it\&. The default value is +is loaded and stream management is enabled by a client, killing the client connection doesn\(cqt mean killing the client session \- the session will be kept alive in order to give the client a chance to resume it\&. The default value is \fInone\fR\&. .RE .RE @@ -5136,10 +5285,16 @@ or \fIfalse\fR\&. If not defined, pubsub does not cache last items\&. On systems with not 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\&. .RE .PP -\fBmax_items_node\fR: \fIMaxItems\fR +\fBmax_item_expire_node\fR: \fItimeout() | infinity\fR +.RS 4 +Specify the maximum item epiry time\&. Default value is: +\fIinfinity\fR\&. +.RE +.PP +\fBmax_items_node\fR: \fInon_neg_integer() | infinity\fR .RS 4 Define the maximum number of items that can be stored in a node\&. Default value is: -\fI10\fR\&. +\fI1000\fR\&. .RE .PP \fBmax_nodes_discoitems\fR: \fIpos_integer() | infinity\fR @@ -5437,9 +5592,9 @@ The module depends on \fImod_push\fR\&. .PP \fBresume_timeout\fR: \fItimeout()\fR .RS 4 -This option specifies the period of time until 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 +This option specifies the period of time until 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 \fImod_stream_mgmt\fR -module is restored\&. The default value is +is restored\&. The default value is \fI72\fR hours\&. .RE @@ -5499,7 +5654,7 @@ Change the password from an existing account on the server\&. Delete an existing account on the server\&. .RE .sp -This module reads also another option defined globally for the server: \fIregistration_timeout\fR\&. Please check that option documentation in the section with top\-level options\&. +This module reads also the top\-level \fIregistration_timeout\fR option defined globally for the server, so please check that option documentation too\&. .sp .it 1 an-trap .nr an-no-space-flag 1 @@ -5528,11 +5683,18 @@ doesn\(cqt allow to register new accounts from s2s or existing c2s sessions\&. Y Specify rules to restrict access for user unregistration\&. By default any user is able to unregister their account\&. .RE .PP +\fBallow_modules\fR: \fIall | [Module, \&.\&.\&.]\fR +.RS 4 +List of modules that can register accounts, or +\fIall\fR\&. The default value is +\fIall\fR, which is equivalent to something like +\fI[mod_register, mod_register_web]\fR\&. +.RE +.PP \fBcaptcha_protected\fR: \fItrue | false\fR .RS 4 -Protect registrations with CAPTCHA (see section -CAPTCHA -of the Configuration Guide)\&. The default is +Protect registrations with +CAPTCHA\&. The default is \fIfalse\fR\&. .RE .PP @@ -5551,7 +5713,8 @@ This option sets the minimum Shannon entropy for passwords\&. The value \fIEntropy\fR -is a number of bits of entropy\&. The recommended minimum is 32 bits\&. The default is 0, i\&.e\&. no checks are performed\&. +is a number of bits of entropy\&. The recommended minimum is 32 bits\&. The default is +\fI0\fR, i\&.e\&. no checks are performed\&. .RE .PP \fBredirect_url\fR: \fIURL\fR @@ -5610,13 +5773,40 @@ Change the password from an existing account on the server\&. Unregister an existing account on the server\&. .RE .sp -This module supports CAPTCHA image to register a new account\&. To enable this feature, configure the options \fIcaptcha_cmd\fR and \fIcaptcha_url\fR, which are documented in the section with top\-level options\&. +This module supports CAPTCHA to register a new account\&. To enable this feature, configure the top\-level \fIcaptcha_cmd\fR and top\-level \fIcaptcha_url\fR options\&. .sp -As an example usage, the users of the host \fIexample\&.org\fR can visit the page: \fIhttps://example\&.org:5281/register/\fR It is important to include the last / character in the URL, otherwise the subpages URL will be incorrect\&. +As an example usage, the users of the host \fIlocalhost\fR can visit the page: \fIhttps://localhost:5280/register/\fR It is important to include the last / character in the URL, otherwise the subpages URL will be incorrect\&. .sp -The module depends on \fImod_register\fR where all the configuration is performed\&. +This module is enabled in \fIlisten\fR → \fIejabberd_http\fR → request_handlers, no need to enable in \fImodules\fR\&. The module depends on \fImod_register\fR where all the configuration is performed\&. .sp The module has no options\&. +.sp +.it 1 an-trap +.nr an-no-space-flag 1 +.nr an-break-flag 1 +.br +.ps +1 +\fBExample:\fR +.RS 4 +.sp +.if n \{\ +.RS 4 +.\} +.nf +listen: + \- + port: 5280 + module: ejabberd_http + request_handlers: + /register: mod_register_web + +modules: + mod_register: {} +.fi +.if n \{\ +.RE +.\} +.RE .SS "mod_roster" .sp This module implements roster management as defined in RFC6121 Section 2\&. The module also adds support for XEP\-0237: Roster Versioning\&. @@ -5910,8 +6100,9 @@ option, but applied to this module only\&. .PP \fBdb_type\fR: \fImnesia | sql\fR .RS 4 -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 -\fIdefault_db\fR, or +Define the type of storage where the module will create the tables and store user information\&. The default is the storage defined by the top\-level +\fIdefault_db\fR +option, or \fImnesia\fR if omitted\&. If \fIsql\fR @@ -6521,7 +6712,8 @@ minute\&. .RS 4 Same as top\-level \fIcache_life_time\fR -option, but applied to this module only\&. +option, but applied to this module only\&. The default value is +\fI48 hours\fR\&. .RE .PP \fBcache_size\fR: \fIpos_integer() | infinity\fR @@ -6594,8 +6786,7 @@ This option defines which access rule will be used to control who is allowed to \fBcredentials_lifetime\fR: \fItimeout()\fR .RS 4 The lifetime of temporary credentials offered to clients\&. If ejabberd\(cqs built\-in TURN service is used, TURN relays allocated using temporary credentials will be terminated shortly after the credentials expired\&. The default value is -\fI12\fR -hours\&. Note that restarting the ejabberd node invalidates any temporary credentials offered before the restart unless a +\fI12 hours\fR\&. Note that restarting the ejabberd node invalidates any temporary credentials offered before the restart unless a \fIsecret\fR is specified (see below)\&. .RE @@ -7121,7 +7312,7 @@ The module depends on \fImod_vcard\fR\&. .ps -1 .br .sp -Nowadays XEP\-0153 is used mostly as "read\-only", i\&.e\&. modern clients don\(cqt publish their avatars inside vCards\&. Thus in the majority of cases the module is only used along with \fImod_avatar\fR module for providing backward compatibility\&. +Nowadays XEP\-0153 is used mostly as "read\-only", i\&.e\&. modern clients don\(cqt publish their avatars inside vCards\&. Thus in the majority of cases the module is only used along with \fImod_avatar\fR for providing backward compatibility\&. .sp .5v .RE .sp @@ -7189,13 +7380,13 @@ TODO ProcessOne\&. .SH "VERSION" .sp -This document describes the configuration file of ejabberd 21\&.04\&.131\&. Configuration options of other ejabberd versions may differ significantly\&. +This document describes the configuration file of ejabberd 21\&.12\&. Configuration options of other ejabberd versions may differ significantly\&. .SH "REPORTING BUGS" .sp Report bugs to https://github\&.com/processone/ejabberd/issues .SH "SEE ALSO" .sp -Default configuration file: https://github\&.com/processone/ejabberd/blob/21\&.07/ejabberd\&.yml\&.example +Default configuration file: https://github\&.com/processone/ejabberd/blob/21\&.12/ejabberd\&.yml\&.example .sp Main site: https://ejabberd\&.im .sp diff --git a/mix.exs b/mix.exs index 0b5d40b09..64f94345c 100644 --- a/mix.exs +++ b/mix.exs @@ -24,6 +24,7 @@ defmodule Ejabberd.MixProject do case config(:vsn) do :false -> "0.0.0" # ./configure wasn't run: vars.config not created '0.0' -> "0.0.0" # the full git repository wasn't downloaded + 'latest.0' -> "0.0.0" # running 'docker-ejabberd/ecs/build.sh latest' [_, _, ?., _, _] = x -> head = String.replace(:erlang.list_to_binary(x), ~r/0+([0-9])/, "\\1") <> @@ -87,6 +88,7 @@ defmodule Ejabberd.MixProject do if_version_below('23', [{:d, :USE_OLD_PG2}]) ++ if_version_below('24', [{:d, :COMPILER_REPORTS_ONLY_LINES}]) ++ if_version_below('24', [{:d, :SYSTOOLS_APP_DEF_WITHOUT_OPTIONAL}]) ++ + if_function_exported(:uri_string, :normalize, 1, [{:d, :HAVE_URI_STRING}]) if_function_exported(:erl_error, :format_exception, 6, [{:d, :HAVE_ERL_ERROR}]) defines = for {:d, value} <- result, do: {:d, value} result ++ [{:d, :ALL_DEFS, defines}] @@ -102,7 +104,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 +115,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 +126,7 @@ defmodule Ejabberd.MixProject do {:pkix, "~> 1.0"}, {:stringprep, ">= 1.0.26"}, {:stun, "~> 1.0"}, - {:xmpp, git: "https://github.com/processone/xmpp", ref: "e943c0285aa85e3cbd4bfb9259f6b7de32b00395", override: true}, + {:xmpp, "~> 1.5"}, {:yconf, "~> 1.0"}] ++ cond_deps() end @@ -145,7 +147,7 @@ defmodule Ejabberd.MixProject do for {:true, dep} <- [{config(:pam), {:epam, "~> 1.0"}}, {config(:redis), {:eredis, "~> 1.2.0"}}, {config(:zlib), {:ezlib, "~> 1.0"}}, - {config(:lua), {:luerl, "~> 0.3.1"}}, + {config(:lua), {:luerl, "~> 1.0"}}, {config(:sqlite), {:sqlite3, "~> 1.1"}}], do: dep end @@ -213,6 +215,7 @@ defmodule Ejabberd.MixProject do epmd: config(:epmd), bindir: Path.join([config(:release_dir), "releases", version()]), release_dir: config(:release_dir), + erts_dir: config(:erts_dir), erts_vsn: "erts-#{release.erts_version}" ] ro = "rel/overlays" @@ -238,7 +241,9 @@ defmodule Ejabberd.MixProject do execute.("sed -e 's|{{\\(\[_a-z\]*\\)}}|<%= @\\1 %>|g' ejabberdctl.template > ejabberdctl.example1") Mix.Generator.copy_template("ejabberdctl.example1", "ejabberdctl.example2", assigns) - execute.("sed -e 's|{{\\(\[_a-z\]*\\)}}|<%= @\\1 %>|g' ejabberdctl.example2 > ejabberdctl.example3") + execute.("sed -e 's|{{\\(\[_a-z\]*\\)}}|<%= @\\1 %>|g' ejabberdctl.example2> ejabberdctl.example2a") + Mix.Generator.copy_template("ejabberdctl.example2a", "ejabberdctl.example2b", assigns) + execute.("sed -e 's|{{\\(\[_a-z\]*\\)}}|<%= @\\1 %>|g' ejabberdctl.example2b > ejabberdctl.example3") execute.("sed -e 's|ERLANG_NODE=ejabberd@localhost|ERLANG_NODE=ejabberd|g' ejabberdctl.example3 > ejabberdctl.example4") execute.("sed -e 's|INSTALLUSER=|ERL_OPTIONS=\"-setcookie \\$\\(cat \"\\${SCRIPT_DIR%/*}/releases/COOKIE\")\"\\nINSTALLUSER=|g' ejabberdctl.example4 > ejabberdctl.example5") Mix.Generator.copy_template("ejabberdctl.example5", "#{ro}/bin/ejabberdctl", assigns) @@ -246,6 +251,8 @@ defmodule Ejabberd.MixProject do File.rm("ejabberdctl.example1") File.rm("ejabberdctl.example2") + File.rm("ejabberdctl.example2a") + File.rm("ejabberdctl.example2b") File.rm("ejabberdctl.example3") File.rm("ejabberdctl.example4") File.rm("ejabberdctl.example5") diff --git a/mix.lock b/mix.lock index a5b158e0f..d0edb619d 100644 --- a/mix.lock +++ b/mix.lock @@ -1,36 +1,36 @@ %{ "artificery": {:hex, :artificery, "0.4.3", "0bc4260f988dcb9dda4b23f9fc3c6c8b99a6220a331534fdf5bf2fd0d4333b02", [:mix], [], "hexpm", "12e95333a30e20884e937abdbefa3e7f5e05609c2ba8cf37b33f000b9ffc0504"}, - "base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm", "fab09b20e3f5db886725544cbcf875b8e73ec93363954eb8a1a9ed834aa8c1f9"}, + "base64url": {:hex, :base64url, "1.0.1", "f8c7f2da04ca9a5d0f5f50258f055e1d699f0e8bf4cfdb30b750865368403cf6", [:rebar3], [], "hexpm", "f9b3add4731a02a9b0410398b475b33e7566a695365237a6bdee1bb447719f5c"}, "cache_tab": {:hex, :cache_tab, "1.0.29", "6c161988620b788d8df28c8f6af557571609c8e4b671dbadab295a4722cd501b", [:rebar3], [{:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "a02a638021cce91ed1a8628dcbb4795bf5c01c9d11db8c613065923142824ce9"}, "distillery": {:hex, :distillery, "2.1.1", "f9332afc2eec8a1a2b86f22429e068ef35f84a93ea1718265e740d90dd367814", [:mix], [{:artificery, "~> 0.2", [hex: :artificery, repo: "hexpm", optional: false]}], "hexpm", "bbc7008b0161a6f130d8d903b5b3232351fccc9c31a991f8fcbf2a12ace22995"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.13", "0c98163e7d04a15feb62000e1a891489feb29f3d10cb57d4f845c405852bbef8", [:mix], [], "hexpm", "d602c26af3a0af43d2f2645613f65841657ad6efc9f0e361c3b6c06b578214ba"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.17", "6f3c7e94170377ba45241d394389e800fb15adc5de51d0a3cd52ae766aafd63f", [:mix], [], "hexpm", "f93ac89c9feca61c165b264b5837bf82344d13bebc634cd575cb711e2e342023"}, "eimp": {:hex, :eimp, "1.0.21", "2e918a5dc9a1959ef8713a2360499e3baeee64cfd7881bd9d1f361ca9ddf07e8", [:rebar3], [{:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "998f58538f58aa0cff103414994d7ce56dc253e6576cd6fb40c1ead64aa73a28"}, "epam": {:hex, :epam, "1.0.12", "2a5625d4133bca4b3943791a3f723ba764455a461ae9b6ba5debb262efcf4b40", [:rebar3], [], "hexpm", "54c166c4459cef72f2990a3d89a8f0be27180fe0ab0f24b28ddcc3b815f49f7f"}, - "esip": {:hex, :esip, "1.0.43", "1cbdc073073f80b9b50e2759f66ca13a353eb4f874bcf92501bd4cd767e34d46", [:rebar3], [{:fast_tls, "1.1.13", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}, {:stun, "1.0.44", [hex: :stun, repo: "hexpm", optional: false]}], "hexpm", "b2c758ae52c4588e0399c0b4ce550bfa56551a5a2f828a28389f2614797e4f4b"}, - "ex_doc": {:hex, :ex_doc, "0.25.0", "4070a254664ee5495c2f7cce87c2f43064a8752f7976f2de4937b65871b05223", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2d90883bd4f3d826af0bde7fea733a4c20adba1c79158e2330f7465821c8949b"}, + "esip": {:hex, :esip, "1.0.45", "2f21fb9750f7a505e6bbd43f6d48b0e879b808aba6c2224686c83f2bcd7a34bf", [: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.47", [hex: :stun, repo: "hexpm", optional: false]}], "hexpm", "1f1eae69f2bd8d75f42c048409eabb4e3dc71ab6412fc5d998edbdade6ad5f75"}, + "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.47", "fae94c0dc7415263297e8f07f286f3355d327d8bf78b1b0743c9a5a492381f71", [: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", "377d8487f4add85f6bc6ecdebdb4dcbcbe890e9462f27d6d31f3db1cf9b2cc9b"}, "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/priv/msgs/fr.msg b/priv/msgs/fr.msg index f33d6115d..d35a2fbd5 100644 --- a/priv/msgs/fr.msg +++ b/priv/msgs/fr.msg @@ -5,11 +5,20 @@ {" (Add * to the end of field to match substring)"," (Ajouter * à la fin du champ pour correspondre à la sous-chaîne)"}. {" has set the subject to: "," a défini le sujet sur : "}. +{"# participants","# participants"}. {"A description of the node","Une description du nœud"}. {"A friendly name for the node","Un nom convivial pour le nœud"}. {"A password is required to enter this room","Un mot de passe est nécessaire pour accéder à ce salon"}. +{"A Web Page","Une page Web"}. {"Accept","Accepter"}. {"Access denied by service policy","L'accès au service est refusé"}. +{"Access model of authorize","Modèle d’accès de « autoriser »"}. +{"Access model of open","Modèle d’accès de « ouvrir »"}. +{"Access model of presence","Modèle d’accès de « présence »"}. +{"Access model of roster","Modèle d’accès de « liste »"}. +{"Access model of whitelist","Modèle d’accès de « liste blanche »"}. +{"Access model","Modèle d’accès"}. +{"Account doesn't exist","Le compte n’existe pas"}. {"Action on user","Action sur l'utilisateur"}. {"Add Jabber ID","Ajouter un Jabber ID"}. {"Add New","Ajouter"}. @@ -19,7 +28,9 @@ {"Administrator privileges required","Les droits d'administrateur sont nécessaires"}. {"All activity","Toute activité"}. {"All Users","Tous les utilisateurs"}. +{"Allow subscription","Autoriser l’abonnement"}. {"Allow this Jabber ID to subscribe to this pubsub node?","Autoriser ce Jabber ID à s'abonner à ce nœud PubSub ?"}. +{"Allow this person to register with the room?","Autoriser cette personne à enregistrer ce salon ?"}. {"Allow users to change the subject","Autoriser les utilisateurs à changer le sujet"}. {"Allow users to query other users","Autoriser les utilisateurs à envoyer des requêtes aux autres utilisateurs"}. {"Allow users to send invites","Autoriser les utilisateurs à envoyer des invitations"}. @@ -28,8 +39,25 @@ {"Allow visitors to send private messages to","Autoriser les visiteurs à envoyer des messages privés"}. {"Allow visitors to send status text in presence updates","Autoriser les visiteurs à envoyer un message d'état avec leur présence"}. {"Allow visitors to send voice requests","Permettre aux visiteurs d'envoyer des demandes de 'voice'"}. +{"An associated LDAP group that defines room membership; this should be an LDAP Distinguished Name according to an implementation-specific or deployment-specific definition of a group.","Un groupe LDAP associé qui définit l’adhésion à un salon ; cela devrait être un nom distingué LDAP selon la définition spécifique à l’implémentation ou au déploiement d’un groupe."}. {"Announcements","Annonces"}. +{"Answer associated with a picture","Réponse associée à une image"}. +{"Answer associated with a video","Réponse associée à une vidéo"}. +{"Answer associated with speech","Réponse associée à un discours"}. +{"Answer to a question","Réponse à une question"}. +{"Anyone in the specified roster group(s) may subscribe and retrieve items","N’importe qui dans le groupe de la liste spécifiée peut s’abonner et récupérer les items"}. +{"Anyone may associate leaf nodes with the collection","N’importe qui peut associer les feuilles avec la collection"}. +{"Anyone may publish","N’importe qui peut publier"}. +{"Anyone may subscribe and retrieve items","N’importe qui peut s’abonner et récupérer les items"}. +{"Anyone with a presence subscription of both or from may subscribe and retrieve items","N’importe qui avec un abonnement de présence peut s’abonner et récupérer les items"}. +{"Anyone with Voice","N’importe qui avec Voice"}. +{"Anyone","N’importe qui"}. {"April","Avril"}. +{"Attribute 'channel' is required for this request","L’attribut « channel » est requis pour la requête"}. +{"Attribute 'id' is mandatory for MIX messages","L’attribut « id » est obligatoire pour les messages MIX"}. +{"Attribute 'jid' is not allowed here","L’attribut « jid » n’est pas autorisé ici"}. +{"Attribute 'node' is not allowed here","L’attribut « node » n’est pas autorisé ici"}. +{"Attribute 'to' of stanza that triggered challenge","L’attribut « to » de la strophe qui a déclenché le défi"}. {"August","Août"}. {"Automatic node creation is not enabled","La creation implicite de nœud n'est pas disponible"}. {"Backup Management","Gestion des sauvegardes"}. @@ -43,10 +71,14 @@ {"Cannot remove active list","La liste active ne peut être supprimée"}. {"Cannot remove default list","La liste par défaut ne peut être supprimée"}. {"CAPTCHA web page","Page web de CAPTCHA"}. +{"Challenge ID","Identifiant du défi"}. {"Change Password","Modifier le mot de passe"}. {"Change User Password","Changer le mot de passe de l'utilisateur"}. {"Changing password is not allowed","La modification du mot de passe n'est pas autorisée"}. {"Changing role/affiliation is not allowed","La modification role/affiliation n'est pas autorisée"}. +{"Channel already exists","Ce canal existe déjà"}. +{"Channel does not exist","Le canal n’existe pas"}. +{"Channels","Canaux"}. {"Characters not allowed:","Caractères non autorisés :"}. {"Chatroom configuration modified","Configuration du salon modifiée"}. {"Chatroom is created","Le salon de discussion est créé"}. @@ -58,30 +90,39 @@ {"Choose storage type of tables","Choisissez un type de stockage pour les tables"}. {"Choose whether to approve this entity's subscription.","Choisissez d'approuver ou non l'abonnement de cette entité."}. {"City","Ville"}. +{"Client acknowledged more stanzas than sent by server","Le client accuse réception de plus de strophes que ce qui est envoyé par le serveur"}. {"Commands","Commandes"}. {"Conference room does not exist","Le salon de discussion n'existe pas"}. {"Configuration of room ~s","Configuration pour le salon ~s"}. {"Configuration","Configuration"}. {"Connected Resources:","Ressources connectées :"}. +{"Contact Addresses (normally, room owner or owners)","Adresses de contact (normalement les administrateurs du salon)"}. {"Country","Pays"}. {"CPU Time:","Temps CPU :"}. +{"Current Discussion Topic","Sujet de discussion courant"}. {"Database failure","Échec sur la base de données"}. {"Database Tables at ~p","Tables de base de données sur ~p"}. {"Database Tables Configuration at ","Configuration des tables de base de données sur "}. {"Database","Base de données"}. {"December","Décembre"}. {"Default users as participants","Les utilisateurs sont participant par défaut"}. +{"Delete content","Supprimer le contenu"}. {"Delete message of the day on all hosts","Supprimer le message du jour sur tous les domaines"}. {"Delete message of the day","Supprimer le message du jour"}. {"Delete Selected","Suppression des éléments sélectionnés"}. +{"Delete table","Supprimer la table"}. {"Delete User","Supprimer l'utilisateur"}. {"Deliver event notifications","Envoyer les notifications d'événement"}. {"Deliver payloads with event notifications","Inclure le contenu du message avec la notification"}. {"Description:","Description :"}. {"Disc only copy","Copie sur disque uniquement"}. +{"'Displayed groups' not added (they do not exist!): ","« Groupes affichés » non ajoutés (ils n’existent pas !) : "}. {"Displayed:","Affichés :"}. +{"Don't tell your password to anybody, not even the administrators of the XMPP server.","Ne révélez votre mot de passe à personne, pas même aux administrateurs du serveur XMPP."}. {"Dump Backup to Text File at ","Enregistrer la sauvegarde dans un fichier texte sur "}. {"Dump to Text File","Sauvegarder dans un fichier texte"}. +{"Duplicated groups are not allowed by RFC6121","Les groupes dupliqués ne sont pas autorisés par la RFC6121"}. +{"Dynamically specify a replyto of the item publisher","Spécifie dynamiquement un « réponse à » de l’item de l’éditeur"}. {"Edit Properties","Modifier les propriétés"}. {"Either approve or decline the voice request.","Accepter ou refuser la demande de voix."}. {"ejabberd MUC module","Module MUC ejabberd"}. @@ -116,6 +157,7 @@ {"Failed to parse HTTP response","Échec de lecture de la réponse HTTP"}. {"Failed to process option '~s'","Échec de traitement de l'option '~s'"}. {"Family Name","Nom de famille"}. +{"FAQ Entry","Entrée FAQ"}. {"February","Février"}. {"File larger than ~w bytes","Taille de fichier suppérieur à ~w octets"}. {"Fill in the form to search for any matching XMPP User","Complétez le formulaire pour rechercher un utilisateur XMPP correspondant"}. @@ -176,6 +218,7 @@ {"July","Juillet"}. {"June","Juin"}. {"Just created","Vient d'être créé"}. +{"Label:","Étiquette :"}. {"Last Activity","Dernière activité"}. {"Last login","Dernière connexion"}. {"Last month","Dernier mois"}. @@ -192,7 +235,6 @@ {"Make room public searchable","Rendre le salon public"}. {"Malformed username","Nom d'utilisateur invalide"}. {"March","Mars"}. -{"Max # of items to persist","Nombre maximum d'éléments à stocker"}. {"Max payload size in bytes","Taille maximum pour le contenu du message en octet"}. {"Maximum file size","Taille maximale du fichier"}. {"Maximum Number of History Messages Returned by Room","Nombre maximal de messages d'historique renvoyés par salle"}. @@ -274,6 +316,7 @@ {"Online Users:","Utilisateurs connectés :"}. {"Online Users","Utilisateurs en ligne"}. {"Online","En ligne"}. +{"Only admins can see this","Seuls les administrateurs peuvent voir cela"}. {"Only deliver notifications to available users","Envoyer les notifications uniquement aux utilisateurs disponibles"}. {"Only or tags are allowed","Seul le tag ou est autorisé"}. {"Only element is allowed in this query","Seul l'élément est autorisé dans cette requête"}. @@ -283,6 +326,7 @@ {"Only moderators can approve voice requests","Seuls les modérateurs peuvent accépter les requêtes voix"}. {"Only occupants are allowed to send messages to the conference","Seuls les occupants peuvent envoyer des messages à la conférence"}. {"Only occupants are allowed to send queries to the conference","Seuls les occupants sont autorisés à envoyer des requêtes à la conférence"}. +{"Only publishers may publish","Seuls les éditeurs peuvent publier"}. {"Only service administrators are allowed to send service messages","Seuls les administrateurs du service sont autoriser à envoyer des messages de service"}. {"Organization Name","Nom de l'organisation"}. {"Organization Unit","Unité de l'organisation"}. @@ -290,6 +334,7 @@ {"Outgoing s2s Connections:","Connexions s2s sortantes :"}. {"Owner privileges required","Les droits de propriétaire sont nécessaires"}. {"Packet","Paquet"}. +{"Participant","Participant"}. {"Password Verification","Vérification du mot de passe"}. {"Password Verification:","Vérification du mot de passe :"}. {"Password","Mot de passe"}. @@ -323,6 +368,9 @@ {"Remove User","Supprimer l'utilisateur"}. {"Remove","Supprimer"}. {"Replaced by new connection","Remplacé par une nouvelle connexion"}. +{"Request has timed out","La demande a expiré"}. +{"Request is ignored","La demande est ignorée"}. +{"Requested role","Rôle demandé"}. {"Resources","Ressources"}. {"Restart Service","Redémarrer le service"}. {"Restart","Redémarrer"}. diff --git a/priv/msgs/zh.msg b/priv/msgs/zh.msg index 8f099b608..a3a400afb 100644 --- a/priv/msgs/zh.msg +++ b/priv/msgs/zh.msg @@ -4,7 +4,7 @@ %% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ {" (Add * to the end of field to match substring)"," (在字段末添加*来匹配子串)"}. -{" has set the subject to: ","已将标题设置为: "}. +{" has set the subject to: "," 已将标题设置为: "}. {"# participants","# 个参与人"}. {"A description of the node","该节点的描述"}. {"A friendly name for the node","该节点的友好名称"}. @@ -23,7 +23,7 @@ {"Add Jabber ID","添加Jabber ID"}. {"Add New","添加新用户"}. {"Add User","添加用户"}. -{"Administration of ","管理"}. +{"Administration of ","管理 "}. {"Administration","管理"}. {"Administrator privileges required","需要管理员权限"}. {"All activity","所有活动"}. @@ -62,7 +62,7 @@ {"Automatic node creation is not enabled","未启用自动节点创建"}. {"Backup Management","备份管理"}. {"Backup of ~p","~p的备份"}. -{"Backup to File at ","备份文件位于"}. +{"Backup to File at ","备份文件位于 "}. {"Backup","备份"}. {"Bad format","格式错误"}. {"Birthday","出生日期"}. @@ -102,7 +102,7 @@ {"Current Discussion Topic","当前讨论话题"}. {"Database failure","数据库失败"}. {"Database Tables at ~p","位于~p的数据库表"}. -{"Database Tables Configuration at ","数据库表格配置位于"}. +{"Database Tables Configuration at ","数据库表格配置位于 "}. {"Database","数据库"}. {"December","十二月"}. {"Default users as participants","用户默认被视为参与人"}. @@ -119,12 +119,12 @@ {"'Displayed groups' not added (they do not exist!): ","'显示的群组' 未被添加 (它们不存在!): "}. {"Displayed:","已显示:"}. {"Don't tell your password to anybody, not even the administrators of the XMPP server.","不要将密码告诉任何人, 就算是XMPP服务器的管理员也不可以."}. -{"Dump Backup to Text File at ","转储备份到文本文件于"}. +{"Dump Backup to Text File at ","将备份转储到位于以下位置的文本文件 "}. {"Dump to Text File","转储到文本文件"}. {"Duplicated groups are not allowed by RFC6121","按照RFC6121的规则,不允许有重复的群组"}. {"Dynamically specify a replyto of the item publisher","为项目发布者动态指定一个 replyto"}. {"Edit Properties","编辑属性"}. -{"Either approve or decline the voice request.","接受或拒绝声音请求"}. +{"Either approve or decline the voice request.","接受或拒绝声音请求."}. {"ejabberd HTTP Upload service","ejabberd HTTP 上传服务"}. {"ejabberd MUC module","ejabberd MUC 模块"}. {"ejabberd Multicast service","ejabberd多重映射服务"}. @@ -194,10 +194,10 @@ {"Import Directory","导入目录"}. {"Import File","导入文件"}. {"Import user data from jabberd14 spool file:","从 jabberd14 Spool 文件导入用户数据:"}. -{"Import User from File at ","导入用户的文件位于"}. +{"Import User from File at ","从以下位置的文件导入用户 "}. {"Import users data from a PIEFXIS file (XEP-0227):","从 PIEFXIS 文件 (XEP-0227) 导入用户数据:"}. {"Import users data from jabberd14 spool directory:","从jabberd14 Spool目录导入用户数据:"}. -{"Import Users from Dir at ","导入用户的目录位于"}. +{"Import Users from Dir at ","从以下位置目录导入用户 "}. {"Import Users From jabberd14 Spool Files","从 jabberd14 Spool 文件导入用户"}. {"Improper domain part of 'from' attribute","不恰当的'from'属性域名部分"}. {"Improper message type","不恰当的消息类型"}. @@ -249,7 +249,6 @@ {"Malformed username","用户名无效"}. {"MAM preference modification denied by service policy","MAM偏好被服务策略拒绝"}. {"March","三月"}. -{"Max # of items to persist","允许持久化的最大内容条目数"}. {"Max payload size in bytes","最大有效负载字节数"}. {"Maximum file size","最大文件大小"}. {"Maximum Number of History Messages Returned by Room","房间返回的历史消息最大值"}. @@ -288,7 +287,7 @@ {"Never","从未"}. {"New Password:","新密码:"}. {"Nickname can't be empty","昵称不能为空"}. -{"Nickname Registration at ","昵称注册于"}. +{"Nickname Registration at ","昵称注册于 "}. {"Nickname ~s does not exist in the room","昵称~s不在该房间"}. {"Nickname","昵称"}. {"No address elements found","没有找到地址的各元素"}. @@ -326,6 +325,7 @@ {"Node ~p","节点~p"}. {"Nodeprep has failed","Nodeprep 已失效"}. {"Nodes","节点"}. +{"Node","节点"}. {"None","无"}. {"Not allowed","不允许"}. {"Not Found","没有找到"}. @@ -339,7 +339,6 @@ {"Number of Offline Messages","离线消息数量"}. {"Number of online users","在线用户数"}. {"Number of registered users","注册用户数"}. -{"Number of seconds after which to automatically purge items","自动清除项目要等待的秒数"}. {"Occupants are allowed to invite others","允许成员邀请其他人"}. {"Occupants May Change the Subject","成员可以修改主题"}. {"October","十月"}. @@ -428,7 +427,7 @@ {"Resources","资源"}. {"Restart Service","重启服务"}. {"Restart","重启"}. -{"Restore Backup from File at ","要恢复的备份文件位于"}. +{"Restore Backup from File at ","从以下位置的文件恢复备份 "}. {"Restore binary backup after next ejabberd restart (requires less memory):","在下次 ejabberd 重启后恢复二进制备份(需要的内存更少):"}. {"Restore binary backup immediately:","立即恢复二进制备份:"}. {"Restore plain text backup immediately:","立即恢复普通文本备份:"}. @@ -455,7 +454,7 @@ {"Search Results for ","搜索结果属于关键词 "}. {"Search the text","搜索文本"}. {"Search until the date","搜索截至日期"}. -{"Search users in ","搜索用户于"}. +{"Search users in ","在以下位置搜索用户 "}. {"Select All","全选"}. {"Send announcement to all online users on all hosts","发送通知给所有主机的在线用户"}. {"Send announcement to all online users","发送通知给所有在线用户"}. @@ -519,7 +518,6 @@ {"The JIDs of those with an affiliation of owner","隶属所有人的JID"}. {"The JIDs of those with an affiliation of publisher","隶属发布人的JID"}. {"The list of JIDs that may associate leaf nodes with a collection","可以将叶节点与集合关联的JID列表"}. -{"The maximum number of child nodes that can be associated with a collection","可以与集合关联的最大子节点数"}. {"The minimum number of milliseconds between sending any two notification digests","发送任何两个通知摘要之间的最小毫秒数"}. {"The name of the node","该节点的名称"}. {"The node is a collection node","该节点是集合节点"}. @@ -544,13 +542,12 @@ {"The type of node data, usually specified by the namespace of the payload (if any)","节点数据的类型, 如果有, 通常由有效负载的名称空间指定"}. {"The URL of an XSL transformation which can be applied to payloads in order to generate an appropriate message body element.","XSL转换的URL,可以将其应用于有效负载以生成适当的消息正文元素。"}. {"The URL of an XSL transformation which can be applied to the payload format in order to generate a valid Data Forms result that the client could display using a generic Data Forms rendering engine","XSL转换的URL, 可以将其应用于有效负载格式, 以生成有效的数据表单结果, 客户端可以使用通用数据表单呈现引擎来显示该结果"}. -{"The username is not valid","用户名无效"}. {"There was an error changing the password: ","修改密码出错: "}. {"There was an error creating the account: ","帐户创建出错: "}. {"There was an error deleting the account: ","帐户删除失败: "}. {"This is case insensitive: macbeth is the same that MacBeth and Macbeth.","此处不区分大小写: macbeth 与 MacBeth 和 Macbeth 是一样的."}. {"This page allows to register an XMPP account in this XMPP server. Your JID (Jabber ID) will be of the form: username@server. Please read carefully the instructions to fill correctly the fields.","本页面允许在此服务器上注册XMPP帐户. 你的JID (Jabber ID) 的形式如下: 用户名@服务器. 请仔细阅读说明并正确填写相应字段."}. -{"This page allows to unregister an XMPP account in this XMPP server.","此页面允许在此XMPP服务器上注销XMPP帐户"}. +{"This page allows to unregister an XMPP account in this XMPP server.","此页面允许在此 XMPP 服务器上注销 XMPP 帐户。"}. {"This room is not anonymous","此房间不是匿名房间"}. {"This service can not process the address: ~s","此服务无法处理地址: ~s"}. {"Thursday","星期四"}. @@ -565,7 +562,7 @@ {"Too many child elements","太多子元素"}. {"Too many elements","太多 元素"}. {"Too many elements","太多 元素"}. -{"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","来自IP地址(~p)的(~s)失败认证太多. 该地址将在UTC时间~s被禁用."}. +{"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","来自IP地址(~p)的(~s)失败认证太多。将在UTC时间 ~s 解除对该地址的封锁"}. {"Too many receiver fields were specified","指定的接收者字段太多"}. {"Too many unacked stanzas","未被确认的节太多"}. {"Too many users in this conference","该会议的用户太多"}. @@ -656,7 +653,7 @@ {"You need a client that supports x:data to register the nickname","您需要一个支持 x:data 的客户端来注册昵称"}. {"You need an x:data capable client to search","您需要一个兼容 x:data 的客户端来搜索"}. {"Your active privacy list has denied the routing of this stanza.","你的活跃私聊列表拒绝了在此房间进行路由分发."}. -{"Your contact offline message queue is full. The message has been discarded.","您的联系人离线消息队列已满. 消息已被丢弃"}. +{"Your contact offline message queue is full. The message has been discarded.","您的联系人离线消息队列已满。消息已被丢弃。"}. {"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","您发送给~s的消息已被阻止. 要解除阻止, 请访问 ~s"}. {"Your XMPP account was successfully registered.","你的XMPP帐户注册成功."}. {"Your XMPP account was successfully unregistered.","你的XMPP帐户注销成功."}. diff --git a/rebar.config b/rebar.config index a70ccfba3..0c18d9e7d 100644 --- a/rebar.config +++ b/rebar.config @@ -30,25 +30,31 @@ {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.45"}}}}, {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"}}}}, + {if_not_rebar3, + {luerl, ".*", {git, "https://github.com/rvirding/luerl", {tag, "1.0"}}} + }}, + {if_var_true, lua, + {if_rebar3, + {luerl, ".*", {git, "https://github.com/rvirding/luerl", {tag, "1.0.0"}}} + }}, {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,12 +64,12 @@ {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", "e943c0285aa85e3cbd4bfb9259f6b7de32b00395"}}, + {stun, ".*", {git, "https://github.com/processone/stun", {tag, "1.0.47"}}}}, + {xmpp, ".*", {git, "https://github.com/processone/xmpp", {tag, "1.5.6"}}}, {yconf, ".*", {git, "https://github.com/processone/yconf", {tag, "1.0.12"}}} ]}. -{gitonly_deps, [elixir, luerl]}. +{gitonly_deps, [elixir]}. {if_var_true, latest_deps, {floating_deps, [cache_tab, @@ -110,6 +116,7 @@ {if_var_true, sip, {d, 'SIP'}}, {if_var_true, stun, {d, 'STUN'}}, {if_have_fun, {erl_error, format_exception, 6}, {d, 'HAVE_ERL_ERROR'}}, + {if_have_fun, {uri_string, normalize, 1}, {d, 'HAVE_URI_STRING'}}, {src_dirs, [src, {if_rebar3, sql}, {if_var_true, tools, tools}, @@ -177,12 +184,10 @@ {sys_config, "./rel/sys.config"}, {vm_args, "./rel/vm.args"}, {overlay_vars, "vars.config"}, - {extended_start_script, true}, {overlay, [{mkdir, "var/log/ejabberd"}, - {mkdir, "var/lock"}, {mkdir, "var/lib/ejabberd"}, {mkdir, "etc/ejabberd"}, - {copy, "rel/files/erl", "\{\{erts_vsn\}\}/bin/erl"}, % in rebar2 this prepends erts- + {copy, "rel/files/erl", "erts-\{\{erts_vsn\}\}/bin/erl"}, {template, "ejabberdctl.template", "bin/ejabberdctl"}, {copy, "inetrc", "etc/ejabberd/inetrc"}, {copy, "tools/captcha*.sh", "lib/ejabberd-\{\{release_version\}\}/priv/bin/"}, @@ -193,6 +198,7 @@ {dev_mode, false}, {include_erts, true}, {include_src, true}, + {generate_start_script, false}, {overlay, [{copy, "sql/*", "lib/ejabberd-\{\{release_version\}\}/priv/sql/"}, {copy, "ejabberdctl.cfg.example", "etc/ejabberd/ejabberdctl.cfg"}, {copy, "ejabberd.yml.example", "etc/ejabberd/ejabberd.yml"}]}]}]}, @@ -201,6 +207,8 @@ {dev_mode, true}, {include_erts, true}, {include_src, false}, + {generate_start_script, true}, + {extended_start_script, true}, {overlay, [{copy, "ejabberdctl.cfg.example", "etc/ejabberd/ejabberdctl.cfg.example"}, {copy, "ejabberd.yml.example", "etc/ejabberd/ejabberd.yml.example"}, {copy, "test/ejabberd_SUITE_data/ca.pem", "etc/ejabberd/"}, diff --git a/rebar.config.script b/rebar.config.script index efd51d6ba..c83390fb5 100644 --- a/rebar.config.script +++ b/rebar.config.script @@ -387,8 +387,8 @@ Rules = [ ]}]), []}, {[plugins], IsRebar3 and (os:getenv("GITHUB_ACTIONS") == "true"), AppendList([{coveralls, {git, - "https://github.com/RoadRunnr/coveralls-erl.git", - {branch, "feature/git-info"}}} ]), []}, + "https://github.com/processone/coveralls-erl.git", + {branch, "addjsonfile"}}} ]), []}, {[overrides], [post_hook_configure], SystemDeps == false, AppendList2(GenDepsConfigure), [], []}, {[ct_extra_params], [eunit_compile_opts], true, diff --git a/rel/reltool.config.script b/rel/reltool.config.script index 459077964..951d55d28 100644 --- a/rel/reltool.config.script +++ b/rel/reltool.config.script @@ -89,7 +89,6 @@ Sys = [{lib_dirs, []}, Overlay = [ {mkdir, "var/log/ejabberd"}, - {mkdir, "var/lock"}, {mkdir, "var/lib/ejabberd"}, {mkdir, "etc/ejabberd"}, {mkdir, "doc"}, diff --git a/sql/lite.new.sql b/sql/lite.new.sql index 96c880358..cb8add8a1 100644 --- a/sql/lite.new.sql +++ b/sql/lite.new.sql @@ -330,6 +330,7 @@ CREATE TABLE muc_room_subscribers ( ); CREATE INDEX i_muc_room_subscribers_host_jid ON muc_room_subscribers(host, jid); +CREATE INDEX i_muc_room_subscribers_jid ON muc_room_subscribers(jid); CREATE UNIQUE INDEX i_muc_room_subscribers_host_room_jid ON muc_room_subscribers(host, room, jid); CREATE TABLE motd ( diff --git a/sql/lite.sql b/sql/lite.sql index 087035d7f..56e8c9151 100644 --- a/sql/lite.sql +++ b/sql/lite.sql @@ -302,6 +302,7 @@ CREATE TABLE muc_room_subscribers ( ); CREATE INDEX i_muc_room_subscribers_host_jid ON muc_room_subscribers(host, jid); +CREATE INDEX i_muc_room_subscribers_jid ON muc_room_subscribers(jid); CREATE UNIQUE INDEX i_muc_room_subscribers_host_room_jid ON muc_room_subscribers(host, room, jid); CREATE TABLE motd ( diff --git a/sql/mssql.sql b/sql/mssql.sql index bb7861527..f8b65edc4 100644 --- a/sql/mssql.sql +++ b/sql/mssql.sql @@ -150,6 +150,7 @@ CREATE TABLE [dbo].[muc_room_subscribers] ( CREATE UNIQUE CLUSTERED INDEX [muc_room_subscribers_host_room_jid] ON [muc_room_subscribers] (host, room, jid); CREATE INDEX [muc_room_subscribers_host_jid] ON [muc_room_subscribers] (host, jid); +CREATE INDEX [muc_room_subscribers_jid] ON [muc_room_subscribers] (jid); CREATE TABLE [dbo].[privacy_default_list] ( [username] [varchar] (250) NOT NULL, diff --git a/sql/mysql.new.sql b/sql/mysql.new.sql index 01aeffbc5..c4e021abf 100644 --- a/sql/mysql.new.sql +++ b/sql/mysql.new.sql @@ -347,6 +347,7 @@ CREATE TABLE muc_room_subscribers ( ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_muc_room_subscribers_host_jid USING BTREE ON muc_room_subscribers(host, jid); +CREATE INDEX i_muc_room_subscribers_jid USING BTREE ON muc_room_subscribers(jid); CREATE TABLE motd ( username varchar(191) NOT NULL, 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/mysql.sql b/sql/mysql.sql index 7feaf5d0a..751552e19 100644 --- a/sql/mysql.sql +++ b/sql/mysql.sql @@ -319,6 +319,7 @@ CREATE TABLE muc_room_subscribers ( ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_muc_room_subscribers_host_jid USING BTREE ON muc_room_subscribers(host, jid); +CREATE INDEX i_muc_room_subscribers_jid USING BTREE ON muc_room_subscribers(jid); CREATE TABLE motd ( username varchar(191) PRIMARY KEY, diff --git a/sql/pg.new.sql b/sql/pg.new.sql index b3473a1a0..5ffbedde3 100644 --- a/sql/pg.new.sql +++ b/sql/pg.new.sql @@ -311,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); @@ -495,6 +495,7 @@ CREATE TABLE muc_room_subscribers ( ); CREATE INDEX i_muc_room_subscribers_host_jid ON muc_room_subscribers USING btree (host, jid); +CREATE INDEX i_muc_room_subscribers_jid ON muc_room_subscribers USING btree (jid); CREATE UNIQUE INDEX i_muc_room_subscribers_host_room_jid ON muc_room_subscribers USING btree (host, room, jid); CREATE TABLE motd ( diff --git a/sql/pg.sql b/sql/pg.sql index 0e3d4c8b8..733856ede 100644 --- a/sql/pg.sql +++ b/sql/pg.sql @@ -320,6 +320,7 @@ CREATE TABLE muc_room_subscribers ( ); CREATE INDEX i_muc_room_subscribers_host_jid ON muc_room_subscribers USING btree (host, jid); +CREATE INDEX i_muc_room_subscribers_jid ON muc_room_subscribers USING btree (jid); CREATE UNIQUE INDEX i_muc_room_subscribers_host_room_jid ON muc_room_subscribers USING btree (host, room, jid); CREATE TABLE motd ( diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl index 0174cd7ff..9e72c7b36 100644 --- a/src/ejabberd_admin.erl +++ b/src/ejabberd_admin.erl @@ -120,7 +120,10 @@ get_commands_spec() -> module = init, function = restart, args = [], result = {res, rescode}}, #ejabberd_commands{name = reopen_log, tags = [logs], - desc = "Reopen the log files", + 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}}, @@ -178,6 +181,8 @@ get_commands_spec() -> result = {res, restuple}}, #ejabberd_commands{name = unregister, tags = [accounts], desc = "Unregister a user", + longdesc = "This deletes the authentication and all the " + "data associated to the account (roster, vcard...).", policy = admin, module = ?MODULE, function = unregister, args_desc = ["Username", "Local vhost served by ejabberd"], @@ -345,31 +350,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 +400,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 c00b1469a..ddf0d3c59 100644 --- a/src/ejabberd_commands.erl +++ b/src/ejabberd_commands.erl @@ -94,6 +94,7 @@ get_commands_spec() -> result_example = ok}, #ejabberd_commands{name = gen_markdown_doc_for_tags, tags = [documentation], desc = "Generates markdown documentation for ejabberd_commands", + note = "added in 21.12", module = ejabberd_commands_doc, function = generate_tags_md, args = [{file, binary}], result = {res, rescode}, diff --git a/src/ejabberd_ctl.erl b/src/ejabberd_ctl.erl index 04e383d53..77595cd54 100644 --- a/src/ejabberd_ctl.erl +++ b/src/ejabberd_ctl.erl @@ -378,7 +378,11 @@ format_arg("", string) -> format_arg(Arg, string) -> NumChars = integer_to_list(length(Arg)), Parse = "~" ++ NumChars ++ "c", - format_arg2(Arg, Parse). + format_arg2(Arg, Parse); +format_arg(Arg, Format) -> + S = unicode:characters_to_binary(Arg, utf8), + JSON = jiffy:decode(S), + mod_http_api:format_arg(JSON, Format). format_arg2(Arg, Parse)-> {ok, [Arg2], _RemainingArguments} = io_lib:fread(Parse, Arg), @@ -525,6 +529,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"}, 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">>, <>}, + {<<"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/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/gen_pubsub_node.erl b/src/gen_pubsub_node.erl index 625e490fc..3f83fe48f 100644 --- a/src/gen_pubsub_node.erl +++ b/src/gen_pubsub_node.erl @@ -133,6 +133,10 @@ {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/mod_admin_update_sql.erl b/src/mod_admin_update_sql.erl index 4e932fe83..02beb4bf8 100644 --- a/src/mod_admin_update_sql.erl +++ b/src/mod_admin_update_sql.erl @@ -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"), diff --git a/src/mod_caps.erl b/src/mod_caps.erl index c8f548169..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, diff --git a/src/mod_conversejs.erl b/src/mod_conversejs.erl new file mode 100644 index 000000000..8683d60ab --- /dev/null +++ b/src/mod_conversejs.erl @@ -0,0 +1,157 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_conversejs.erl +%%% Author : Alexey Shchepin +%%% Purpose : Serve simple page for Converse.js XMPP web browser client +%%% Created : 8 Nov 2021 by Alexey Shchepin +%%% +%%% +%%% 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], + [<<"">>, + <<"">>, + <<"">>, + <<"">>, + <<"">>, + <<"">>, + <<"">>, + <<"">>, + <<"">>, + <<"">>, + <<"">>]}; +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("This module is available since ejabberd 21.12."), "", + ?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_http_api.erl b/src/mod_http_api.erl index 427833584..023df39ca 100644 --- a/src/mod_http_api.erl +++ b/src/mod_http_api.erl @@ -30,6 +30,7 @@ -behaviour(gen_mod). -export([start/2, stop/1, reload/3, process/2, depends/2, + format_arg/2, mod_options/1, mod_doc/0]). -include_lib("xmpp/include/xmpp.hrl"). diff --git a/src/mod_mam.erl b/src/mod_mam.erl index abb2333cc..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). diff --git a/src/mod_mqtt_session.erl b/src/mod_mqtt_session.erl index ca025e3d2..e7737804e 100644 --- a/src/mod_mqtt_session.erl +++ b/src/mod_mqtt_session.erl @@ -1134,8 +1134,8 @@ is_expired(#publish{meta = Meta, properties = Props} = Pkt) -> %%% Authentication %%%=================================================================== -spec parse_credentials(connect()) -> {ok, jid:jid()} | {error, reason_code()}. -parse_credentials(#connect{client_id = <<>>}) -> - parse_credentials(#connect{client_id = p1_rand:get_string()}); +parse_credentials(#connect{client_id = <<>>} = C) -> + parse_credentials(C#connect{client_id = p1_rand:get_string()}); parse_credentials(#connect{username = <<>>, client_id = ClientID}) -> Host = ejabberd_config:get_myname(), JID = case jid:make(ClientID, Host) of diff --git a/src/mod_muc.erl b/src/mod_muc.erl index b2ebc5c61..72f386b00 100644 --- a/src/mod_muc.erl +++ b/src/mod_muc.erl @@ -69,6 +69,7 @@ get_online_rooms_by_user/3, can_use_nick/4, get_subscribed_rooms/2, + remove_user/2, procname/2, route/1, unhibernate_room/3]). @@ -122,6 +123,8 @@ start(Host, Opts) -> case mod_muc_sup:start(Host) of {ok, _} -> + ejabberd_hooks:add(remove_user, Host, ?MODULE, + remove_user, 50), MyHosts = gen_mod:get_opt_hosts(Opts), Mod = gen_mod:db_mod(Opts, ?MODULE), RMod = gen_mod:ram_db_mod(Opts, ?MODULE), @@ -133,6 +136,8 @@ start(Host, Opts) -> end. stop(Host) -> + ejabberd_hooks:delete(remove_user, Host, ?MODULE, + remove_user, 50), Proc = mod_muc_sup:procname(Host), supervisor:terminate_child(ejabberd_gen_mod_sup, Proc), supervisor:delete_child(ejabberd_gen_mod_sup, Proc). @@ -1122,6 +1127,32 @@ count_online_rooms(ServerHost, Host) -> RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), RMod:count_online_rooms(ServerHost, Host). +-spec remove_user(binary(), binary()) -> ok. +remove_user(User, Server) -> + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + Mod = gen_mod:db_mod(LServer, ?MODULE), + case erlang:function_exported(Mod, remove_user, 2) of + true -> + Mod:remove_user(LUser, LServer); + false -> + ok + end, + JID = jid:make(User, Server), + lists:foreach( + fun(Host) -> + lists:foreach( + fun({_, _, Pid}) -> + mod_muc_room:change_item_async( + Pid, JID, affiliation, none, <<"User removed">>), + mod_muc_room:change_item_async( + Pid, JID, role, none, <<"User removed">>) + end, + get_online_rooms(LServer, Host)) + end, + gen_mod:get_module_opt_hosts(LServer, mod_muc)), + ok. + opts_to_binary(Opts) -> lists:map( fun({title, Title}) -> @@ -1225,6 +1256,8 @@ mod_opt_type(user_message_shaper) -> econf:atom(); mod_opt_type(user_presence_shaper) -> econf:atom(); +mod_opt_type(cleanup_affiliations_on_start) -> + econf:bool(); mod_opt_type(default_room_options) -> econf:options( #{allow_change_subj => econf:bool(), @@ -1302,6 +1335,7 @@ mod_options(Host) -> {preload_rooms, true}, {hibernation_timeout, infinity}, {vcard, undefined}, + {cleanup_affiliations_on_start, false}, {default_room_options, [{allow_change_subj,true}, {allow_private_messages,true}, @@ -1580,6 +1614,11 @@ mod_doc() -> " -", " work: true", " street: Elm Street"]}]}}, + {cleanup_affiliations_on_start, + #{value => "true | false", + desc => + ?T("Remove affiliations for non-existing local users on startup. " + "The default value is 'false'.")}}, {default_room_options, #{value => ?T("Options"), desc => diff --git a/src/mod_muc_admin.erl b/src/mod_muc_admin.erl index 2abeee45c..ac2d887fe 100644 --- a/src/mod_muc_admin.erl +++ b/src/mod_muc_admin.erl @@ -40,8 +40,11 @@ change_room_option/4, get_room_options/2, set_room_affiliation/4, get_room_affiliations/2, get_room_affiliation/3, web_menu_main/2, web_page_main/2, web_menu_host/3, - subscribe_room/4, unsubscribe_room/2, get_subscribers/2, - web_page_host/3, mod_options/1, get_commands_spec/0, find_hosts/1]). + subscribe_room/4, subscribe_room_many/3, + unsubscribe_room/2, get_subscribers/2, + web_page_host/3, + mod_opt_type/1, mod_options/1, + get_commands_spec/0, find_hosts/1]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). @@ -281,7 +284,7 @@ get_commands_spec() -> #ejabberd_commands{name = send_direct_invitation, tags = [muc_room], desc = "Send a direct invitation to several destinations", - longdesc = "Since ejabberd 20.10, this command is " + longdesc = "Since ejabberd 20.12, this command is " "asynchronous: the API call may return before the " "server has send all the invitations.\n\n" "Password and Message can also be: none. " @@ -331,6 +334,26 @@ get_commands_spec() -> args = [{user, binary}, {nick, binary}, {room, binary}, {nodes, binary}], result = {nodes, {list, {node, string}}}}, + #ejabberd_commands{name = subscribe_room_many, tags = [muc_room], + desc = "Subscribe several users to a MUC conference", + longdesc = "This command accept up to 50 users at once (this is configurable with `subscribe_room_many_max_users` option)", + module = ?MODULE, function = subscribe_room_many, + args_desc = ["Users JIDs and nicks", + "the room to subscribe", + "nodes separated by commas: ,"], + args_example = [[{"tom@localhost", "Tom"}, + {"jerry@localhost", "Jerry"}], + "room1@conference.localhost", + "urn:xmpp:mucsub:nodes:messages,urn:xmpp:mucsub:nodes:affiliations"], + args = [{users, {list, + {user, {tuple, + [{jid, binary}, + {nick, binary} + ]}} + }}, + {room, binary}, + {nodes, binary}], + result = {res, rescode}}, #ejabberd_commands{name = unsubscribe_room, tags = [muc_room], desc = "Unsubscribe from a MUC conference", module = ?MODULE, function = unsubscribe_room, @@ -710,7 +733,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 @@ -860,7 +883,14 @@ get_online_rooms(ServiceArg) -> || {RoomName, RoomHost, Pid} <- mod_muc:get_online_rooms(Host)] end, Hosts). -get_all_rooms(Host) -> +get_all_rooms(ServiceArg) -> + Hosts = find_services(ServiceArg), + lists:flatmap( + fun(Host) -> + get_all_rooms2(Host) + end, Hosts). + +get_all_rooms2(Host) -> ServerHost = ejabberd_router:host_of_route(Host), OnlineRooms = get_online_rooms(Host), OnlineMap = lists:foldl( @@ -1324,6 +1354,18 @@ subscribe_room(User, Nick, Room, Nodes) -> throw({error, "Malformed room JID"}) end. +subscribe_room_many(Users, Room, Nodes) -> + MaxUsers = mod_muc_admin_opt:subscribe_room_many_max_users(global), + if + length(Users) > MaxUsers -> + throw({error, "Too many users in subscribe_room_many command"}); + true -> + lists:foreach( + fun({User, Nick}) -> + subscribe_room(User, Nick, Room, Nodes) + end, Users) + end. + unsubscribe_room(User, Room) -> try jid:decode(Room) of #jid{luser = Name, lserver = Host} when Name /= <<"">> -> @@ -1406,11 +1448,22 @@ find_hosts(ServerHost) -> [] end. -mod_options(_) -> []. +mod_opt_type(subscribe_room_many_max_users) -> + econf:int(). + +mod_options(_) -> + [{subscribe_room_many_max_users, 50}]. mod_doc() -> #{desc => [?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`_.")], + opts => + [{subscribe_room_many_max_users, + #{value => ?T("Number"), + desc => + ?T("How many users can be subscribed to a room at once using " + "the 'subscribe_room_many' command. " + "The default value is '50'.")}}]}. diff --git a/src/mod_muc_admin_opt.erl b/src/mod_muc_admin_opt.erl new file mode 100644 index 000000000..18ca64af7 --- /dev/null +++ b/src/mod_muc_admin_opt.erl @@ -0,0 +1,13 @@ +%% Generated automatically +%% DO NOT EDIT: run `make options` instead + +-module(mod_muc_admin_opt). + +-export([subscribe_room_many_max_users/1]). + +-spec subscribe_room_many_max_users(gen_mod:opts() | global | binary()) -> integer(). +subscribe_room_many_max_users(Opts) when is_map(Opts) -> + gen_mod:get_opt(subscribe_room_many_max_users, Opts); +subscribe_room_many_max_users(Host) -> + gen_mod:get_module_opt(Host, mod_muc_admin, subscribe_room_many_max_users). + diff --git a/src/mod_muc_opt.erl b/src/mod_muc_opt.erl index 760a5d7c8..4b9e8b806 100644 --- a/src/mod_muc_opt.erl +++ b/src/mod_muc_opt.erl @@ -9,6 +9,7 @@ -export([access_mam/1]). -export([access_persistent/1]). -export([access_register/1]). +-export([cleanup_affiliations_on_start/1]). -export([db_type/1]). -export([default_room_options/1]). -export([hibernation_timeout/1]). @@ -73,6 +74,12 @@ access_register(Opts) when is_map(Opts) -> access_register(Host) -> gen_mod:get_module_opt(Host, mod_muc, access_register). +-spec cleanup_affiliations_on_start(gen_mod:opts() | global | binary()) -> boolean(). +cleanup_affiliations_on_start(Opts) when is_map(Opts) -> + gen_mod:get_opt(cleanup_affiliations_on_start, Opts); +cleanup_affiliations_on_start(Host) -> + gen_mod:get_module_opt(Host, mod_muc, cleanup_affiliations_on_start). + -spec db_type(gen_mod:opts() | global | binary()) -> atom(). db_type(Opts) when is_map(Opts) -> gen_mod:get_opt(db_type, Opts); diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl index 035e851fd..aaf3e8895 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 @@ -48,6 +50,7 @@ set_config/2, get_state/1, change_item/5, + change_item_async/5, config_reloaded/1, subscribe/4, unsubscribe/2, @@ -76,6 +79,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). @@ -194,6 +203,11 @@ change_item(Pid, JID, Type, AffiliationOrRole, Reason) -> {error, notfound} end. +-spec change_item_async(pid(), jid(), affiliation | role, affiliation() | role(), binary()) -> ok. +change_item_async(Pid, JID, Type, AffiliationOrRole, Reason) -> + p1_fsm:send_all_state_event( + Pid, {process_item_change, {JID, Type, AffiliationOrRole, Reason}, undefined}). + -spec get_state(pid()) -> {ok, state()} | {error, notfound | timeout}. get_state(Pid) -> try p1_fsm:sync_send_all_state_event(Pid, get_state) @@ -298,7 +312,8 @@ init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts, QueueType]) room_shaper = Shaper}), add_to_log(room_existence, started, State), ejabberd_hooks:run(start_room, ServerHost, [ServerHost, Room, Host]), - {ok, normal_state, reset_hibernate_timer(State)}. + State1 = cleanup_affiliations(State), + {ok, normal_state, reset_hibernate_timer(State1)}. normal_state({route, <<"">>, #message{from = From, type = Type, lang = Lang} = Packet}, @@ -446,6 +461,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"), @@ -664,6 +681,16 @@ handle_event({set_affiliations, Affiliations}, StateName, StateData) -> NewStateData = set_affiliations(Affiliations, StateData), {next_state, StateName, NewStateData}; +handle_event({process_item_change, Item, UJID}, StateName, StateData) -> + case process_item_change(Item, StateData, UJID) of + {error, _} -> + {next_state, StateName, StateData}; + StateData -> + {next_state, StateName, StateData}; + NSD -> + store_room(NSD), + {next_state, StateName, NSD} + end; handle_event(_Event, StateName, StateData) -> {next_state, StateName, StateData}. @@ -712,6 +739,8 @@ handle_sync_event({process_item_change, Item, UJID}, _From, StateName, StateData case process_item_change(Item, StateData, UJID) of {error, _} = Err -> {reply, Err, StateName, StateData}; + StateData -> + {reply, {ok, StateData}, StateName, StateData}; NSD -> store_room(NSD), {reply, {ok, NSD}, StateName, NSD} @@ -1405,6 +1434,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()) -> @@ -1602,7 +1637,7 @@ do_get_affiliation_fallback(JID, StateData) -> -spec get_affiliations(state()) -> affiliations(). get_affiliations(#state{config = #config{persistent = false}} = StateData) -> - get_affiliations_callback(StateData); + get_affiliations_fallback(StateData); get_affiliations(StateData) -> Room = StateData#state.room, Host = StateData#state.host, @@ -1610,13 +1645,13 @@ get_affiliations(StateData) -> Mod = gen_mod:db_mod(ServerHost, mod_muc), case Mod:get_affiliations(ServerHost, Room, Host) of {error, _} -> - get_affiliations_callback(StateData); + get_affiliations_fallback(StateData); {ok, Affiliations} -> Affiliations end. --spec get_affiliations_callback(state()) -> affiliations(). -get_affiliations_callback(StateData) -> +-spec get_affiliations_fallback(state()) -> affiliations(). +get_affiliations_fallback(StateData) -> StateData#state.affiliations. -spec get_service_affiliation(jid(), state()) -> owner | none. @@ -1935,7 +1970,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). @@ -2485,9 +2520,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}), + 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), @@ -2536,7 +2572,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 @@ -3579,7 +3617,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 -> @@ -3667,6 +3706,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], @@ -3897,6 +3937,9 @@ 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}}; @@ -3927,6 +3970,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). @@ -3983,6 +4031,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)}, @@ -3990,6 +4039,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}]. @@ -4080,6 +4132,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), @@ -4119,6 +4172,77 @@ process_iq_disco_info(From, #iq{type = get, lang = Lang, DiscoInfo = make_disco_info(From, StateData), 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) -> @@ -4199,6 +4323,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)}. @@ -4441,6 +4605,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 @@ -4687,7 +5116,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 /= [] -> @@ -4703,7 +5132,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. @@ -4927,6 +5356,23 @@ muc_subscribers_put(Subscriber, MUCSubscribers) -> subscriber_nodes = NewSubNodes}. +cleanup_affiliations(State) -> + case mod_muc_opt:cleanup_affiliations_on_start(State#state.server_host) of + true -> + Affiliations = + maps:filter( + fun({LUser, LServer, _}, _) -> + case ejabberd_router:is_my_host(LServer) of + true -> + ejabberd_auth:user_exists(LUser, LServer); + false -> + true + end + end, State#state.affiliations), + State#state{affiliations = Affiliations}; + false -> + State + end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Detect messange stanzas that don't have meaningful content diff --git a/src/mod_muc_sql.erl b/src/mod_muc_sql.erl index 1310cde7b..8aa7ad62b 100644 --- a/src/mod_muc_sql.erl +++ b/src/mod_muc_sql.erl @@ -38,7 +38,7 @@ register_online_user/4, unregister_online_user/4, count_online_rooms_by_user/3, get_online_rooms_by_user/3, get_subscribed_rooms/3, get_rooms_without_subscribers/2, - find_online_room_by_pid/2]). + find_online_room_by_pid/2, remove_user/2]). -export([set_affiliation/6, set_affiliations/4, get_affiliation/5, get_affiliations/3, search_affiliation/4]). @@ -465,6 +465,13 @@ get_subscribed_rooms(LServer, Host, Jid) -> {error, db_failure} end. +remove_user(LUser, LServer) -> + SJID = jid:encode(jid:make(LUser, LServer)), + ejabberd_sql:sql_query( + LServer, + ?SQL("delete from muc_room_subscribers where jid=%(SJID)s")), + ok. + %%%=================================================================== %%% Internal functions %%%=================================================================== 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). + route_grouped(LServerS, LServiceS, FromJID, Groups, Rest, PacketStripped). --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 mark_as_delivered([address()]) -> [address()]. +mark_as_delivered(Addresses) -> + [A#address{delivered = true} || A <- Addresses]. --spec act_groups(jid(), stanza(), binary(), [{routing(), #group{}}]) -> 'ok'. -act_groups(FromJID, Packet_stripped, LServiceS, AGroups) -> +-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 @@ -425,245 +441,89 @@ strip_addresses_element(Packet) -> throw(eadsele) 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) + 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(), 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) - 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_pubsub.erl b/src/mod_pubsub.erl index 2e40d8f0e..d161ec10c 100644 --- a/src/mod_pubsub.erl +++ b/src/mod_pubsub.erl @@ -95,7 +95,7 @@ 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]). +-export([get_commands_spec/0, delete_old_items/1, delete_expired_items/0]). -export([route/1]). @@ -3431,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) -> @@ -3504,17 +3512,24 @@ decode_node_config(undefined, _, _) -> decode_node_config(#xdata{fields = Fs}, Host, Lang) -> try Config = pubsub_node_config:decode(Fs), - Max = get_max_items_node(Host), - case {check_opt_range(max_items, Config, Max), + MaxItems = get_max_items_node(Host), + MaxExpiry = get_max_item_expire_node(Host), + case {check_opt_range(max_items, Config, MaxItems), + check_opt_range(item_expire, Config, MaxExpiry), check_opt_range(max_payload_size, Config, ?MAX_PAYLOAD_SIZE)} of - {true, true} -> + {true, true, true} -> Config; - {true, false} -> + {true, true, false} -> erlang:error( {pubsub_node_config, {bad_var_value, <<"pubsub#max_payload_size">>, ?NS_PUBSUB_NODE_CONFIG}}); - {false, _} -> + {true, false, _} -> + erlang:error( + {pubsub_node_config, + {bad_var_value, <<"pubsub#item_expire">>, + ?NS_PUBSUB_NODE_CONFIG}}); + {false, _, _} -> erlang:error( {pubsub_node_config, {bad_var_value, <<"pubsub#max_items">>, @@ -3560,20 +3575,24 @@ decode_get_pending(#xdata{fields = Fs}, Lang) -> end. -spec check_opt_range(atom(), [proplists:property()], - non_neg_integer() | unlimited | undefined) -> boolean(). -check_opt_range(_Opt, _Opts, undefined) -> - true; + non_neg_integer() | unlimited | infinity) -> boolean(). check_opt_range(_Opt, _Opts, unlimited) -> true; +check_opt_range(_Opt, _Opts, infinity) -> + true; check_opt_range(Opt, Opts, Max) -> case proplists:get_value(Opt, Opts, Max) of max -> true; Val -> Val =< Max end. --spec get_max_items_node(host()) -> undefined | unlimited | 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) -> @@ -4181,16 +4200,63 @@ delete_old_items(N) -> 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", + note = "added in 21.12", 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", + note = "added in 21.12", + 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(). @@ -4204,6 +4270,8 @@ mod_opt_type(last_item_cache) -> econf:bool(); mod_opt_type(max_items_node) -> 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) -> @@ -4251,6 +4319,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, []}, @@ -4329,11 +4398,17 @@ 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", + note => "added in 21.12", + desc => + ?T("Specify the maximum item epiry time. Default value " + "is: 'infinity'.")}}, {max_items_node, #{value => "non_neg_integer() | infinity", desc => ?T("Define the maximum number of items that can be " - "stored in a node. Default value is: '10'.")}}, + "stored in a node. Default value is: '1000'.")}}, {max_nodes_discoitems, #{value => "pos_integer() | infinity", desc => 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_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 379318da6..b85efd57c 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}, @@ -661,6 +673,13 @@ 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, ...]", + note => "added in 21.12", + 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 => 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 0e216c81c..0cf4bcff8 100644 --- a/src/mod_register_web.erl +++ b/src/mod_register_web.erl @@ -85,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), @@ -290,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] -> @@ -312,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. @@ -502,11 +501,11 @@ 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) -> +register_account(Username, Host, Password, Ip) -> try mod_register_opt:access(Host) of Access -> case jid:make(Username, Host) of @@ -514,16 +513,15 @@ register_account(Username, Host, Password) -> JID -> case acl:match_rule(Host, Access, JID) of deny -> {error, not_allowed}; - allow -> register_account2(Username, Host, Password) + 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}}; @@ -579,12 +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, host_unknown}) -> ?T("Host unknown"); -get_error_text({error, not_allowed}) -> - ?T("Not allowed"); get_error_text({error, account_doesnt_exist}) -> ?T("Account doesn't exist"); get_error_text({error, account_exists}) -> @@ -594,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(_) -> []. 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 13ff90466..358a8df32 100644 --- a/src/mod_shared_roster.erl +++ b/src/mod_shared_roster.erl @@ -870,12 +870,15 @@ c2s_self_presence(Acc) -> Acc. -spec unset_presence(binary(), binary(), binary(), binary()) -> ok. -unset_presence(LUser, LServer, Resource, Status) -> +unset_presence(User, Server, Resource, Status) -> + LUser = jid:nodeprep(User), + LServer = jid:nameprep(Server), + LResource = jid:resourceprep(Resource), Resources = ejabberd_sm:get_user_resources(LUser, LServer), ?DEBUG("Unset_presence for ~p @ ~p / ~p -> ~p " "(~p resources)", - [LUser, LServer, Resource, Status, length(Resources)]), + [LUser, LServer, LResource, Status, length(Resources)]), case length(Resources) of 0 -> lists:foreach( diff --git a/src/mod_shared_roster_ldap.erl b/src/mod_shared_roster_ldap.erl index 08fbe8793..e842ab261 100644 --- a/src/mod_shared_roster_ldap.erl +++ b/src/mod_shared_roster_ldap.erl @@ -689,9 +689,9 @@ mod_doc() -> ?T("- Connection parameters: The module also accepts the " "connection parameters, all of which default to the top-level " "parameter of the same name, if unspecified. " - "See http://../database-ldap/#ldap-connection[LDAP Connection] " + "See http://../ldap/#ldap-connection[LDAP Connection] " "section for more information about them."), "", - ?T("Check also the http://../database-ldap/#configuration-examples" + ?T("Check also the http://../ldap/#ldap-examples" "[Configuration examples] section to get details about " "retrieving the roster, " "and configuration examples including Flat DIT and Deep DIT.")], @@ -721,13 +721,13 @@ mod_doc() -> "name of roster entries (usually full names of people in " "the roster). See also the parameters 'ldap_userdesc' and " "'ldap_useruid'. For more information check the LDAP " - "http://../database-ldap/#filters[Filters] section.")}}, + "http://../ldap/#filters[Filters] section.")}}, {ldap_filter, #{desc => ?T("Additional filter which is AND-ed together " "with \"User Filter\" and \"Group Filter\". " "For more information check the LDAP " - "http://../database-ldap/#filters[Filters] section.")}}, + "http://../ldap/#filters[Filters] section.")}}, %% Attributes: {ldap_groupattr, #{desc => @@ -785,7 +785,7 @@ mod_doc() -> #{desc => ?T("A regex for extracting user ID from the value of the " "attribute named by 'ldap_memberattr'. Check the LDAP " - "http://../database-ldap/#control-parameters" + "http://../ldap/#control-parameters" "[Control Parameters] section.")}}, {ldap_auth_check, #{value => "true | false", diff --git a/src/mod_stun_disco.erl b/src/mod_stun_disco.erl index 6e7592453..cbb671639 100644 --- a/src/mod_stun_disco.erl +++ b/src/mod_stun_disco.erl @@ -646,7 +646,7 @@ get_listener_ips(#{ip := {0, 0, 0, 0, 0, 0, 0, 1}} = Opts) -> {undefined, get_turn_ipv6_addr(Opts)}; get_listener_ips(#{ip := {_, _, _, _} = IP}) -> {IP, undefined}; -get_listener_ips(#{ip := {_, _, _, _, _,_, _, _, _} = IP}) -> +get_listener_ips(#{ip := {_, _, _, _, _, _, _, _} = IP}) -> {undefined, IP}. -spec get_turn_ipv4_addr(map()) -> inet:ip4_address() | undefined. diff --git a/src/node_flat.erl b/src/node_flat.erl index c597b9ce9..55dea0d8d 100644 --- a/src/node_flat.erl +++ b/src/node_flat.erl @@ -40,7 +40,7 @@ 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_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, @@ -432,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

Triggers item deletion.

%%

Default plugin: The user performing the deletion must be the node owner %% or a publisher, or PublishModel being open.

diff --git a/src/node_flat_sql.erl b/src/node_flat_sql.erl index 240dc3760..f9c8a209d 100644 --- a/src/node_flat_sql.erl +++ b/src/node_flat_sql.erl @@ -43,7 +43,7 @@ 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_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, @@ -285,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), diff --git a/src/node_pep.erl b/src/node_pep.erl index 44388ca31..66431b948 100644 --- a/src/node_pep.erl +++ b/src/node_pep.erl @@ -36,7 +36,7 @@ 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_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, @@ -81,10 +81,12 @@ features() -> [<<"create-nodes">>, <<"auto-create">>, <<"auto-subscribe">>, + <<"config-node">>, <<"delete-nodes">>, <<"delete-items">>, <<"filtered-notifications">>, <<"modify-affiliations">>, + <<"multi-items">>, <<"outcast-affiliation">>, <<"persistent-items">>, <<"publish">>, @@ -142,6 +144,9 @@ 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 c0cf2b166..3bb66bc4c 100644 --- a/src/node_pep_sql.erl +++ b/src/node_pep_sql.erl @@ -38,7 +38,7 @@ 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_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, @@ -99,6 +99,9 @@ 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/prosody2ejabberd.erl b/src/prosody2ejabberd.erl index 3992a4034..8f5c35f84 100644 --- a/src/prosody2ejabberd.erl +++ b/src/prosody2ejabberd.erl @@ -118,7 +118,7 @@ eval_file(Path) -> case luerl:eval(NewData, State1) of {ok, _} = Res -> Res; - {error, Why} = Err -> + {error, Why, _} = Err -> ?ERROR_MSG("Failed to eval ~ts: ~p", [Path, Why]), Err end; diff --git a/src/rest.erl b/src/rest.erl index d724352f2..1bb5c5ef7 100644 --- a/src/rest.erl +++ b/src/rest.erl @@ -191,13 +191,26 @@ base_url(Server, Path) -> _ -> Url end. +-ifdef(HAVE_URI_STRING). +uri_hack(Str) -> + case uri_string:normalize("%25") of + "%" -> % This hack around bug in httpc >21 <23.2 + binary:replace(Str, <<"%25">>, <<"%2525">>, [global]); + _ -> Str + end. +-else. +uri_hack(Str) -> + Str. +-endif. + url(Url, []) -> Url; url(Url, Params) -> L = [<<"&", (iolist_to_binary(Key))/binary, "=", (misc:url_encode(Value))/binary>> || {Key, Value} <- Params], - <<$&, Encoded/binary>> = iolist_to_binary(L), + <<$&, Encoded0/binary>> = iolist_to_binary(L), + Encoded = uri_hack(Encoded0), <>. url(Server, Path, Params) -> case binary:split(base_url(Server, Path), <<"?">>) of 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.sh b/tools/captcha.sh index 9fa4a52c4..7885858a2 100755 --- a/tools/captcha.sh +++ b/tools/captcha.sh @@ -15,69 +15,62 @@ INPUT=$1 -if test -n ${BASH_VERSION:-''} ; then - get_random () - { - R=$RANDOM - } -else - for n in `od -A n -t u2 -N 48 /dev/urandom`; do RL="$RL$n "; done - get_random () - { - R=${RL%% *} - RL=${RL#* } - } -fi +for n in $(od -A n -t u2 -N 48 /dev/urandom); do RL="$RL$n "; done +get_random () +{ + R=${RL%% *} + RL=${RL#* } +} get_random -WAVE1_AMPLITUDE=$((2 + $R % 5)) +WAVE1_AMPLITUDE=$((2 + R % 5)) get_random -WAVE1_LENGTH=$((50 + $R % 25)) +WAVE1_LENGTH=$((50 + R % 25)) get_random -WAVE2_AMPLITUDE=$((2 + $R % 5)) +WAVE2_AMPLITUDE=$((2 + R % 5)) get_random -WAVE2_LENGTH=$((50 + $R % 25)) +WAVE2_LENGTH=$((50 + R % 25)) get_random -WAVE3_AMPLITUDE=$((2 + $R % 5)) +WAVE3_AMPLITUDE=$((2 + R % 5)) get_random -WAVE3_LENGTH=$((50 + $R % 25)) +WAVE3_LENGTH=$((50 + R % 25)) get_random -W1_LINE_START_Y=$((10 + $R % 40)) +W1_LINE_START_Y=$((10 + R % 40)) get_random -W1_LINE_STOP_Y=$((10 + $R % 40)) +W1_LINE_STOP_Y=$((10 + R % 40)) get_random -W2_LINE_START_Y=$((10 + $R % 40)) +W2_LINE_START_Y=$((10 + R % 40)) get_random -W2_LINE_STOP_Y=$((10 + $R % 40)) +W2_LINE_STOP_Y=$((10 + R % 40)) get_random -W3_LINE_START_Y=$((10 + $R % 40)) +W3_LINE_START_Y=$((10 + R % 40)) get_random -W3_LINE_STOP_Y=$((10 + $R % 40)) +W3_LINE_STOP_Y=$((10 + R % 40)) get_random -B1_LINE_START_Y=$(($R % 40)) +B1_LINE_START_Y=$((R % 40)) get_random -B1_LINE_STOP_Y=$(($R % 40)) +B1_LINE_STOP_Y=$((R % 40)) get_random -B2_LINE_START_Y=$(($R % 40)) +B2_LINE_START_Y=$((R % 40)) get_random -B2_LINE_STOP_Y=$(($R % 40)) -#B3_LINE_START_Y=$(($R % 40)) -#B3_LINE_STOP_Y=$(($R % 40)) +B2_LINE_STOP_Y=$((R % 40)) +#B3_LINE_START_Y=$((R % 40)) +#B3_LINE_STOP_Y=$((R % 40)) get_random -B1_LINE_START_X=$(($R % 20)) +B1_LINE_START_X=$((R % 20)) get_random -B1_LINE_STOP_X=$((100 + $R % 40)) +B1_LINE_STOP_X=$((100 + R % 40)) get_random -B2_LINE_START_X=$(($R % 20)) +B2_LINE_START_X=$((R % 20)) get_random -B2_LINE_STOP_X=$((100 + $R % 40)) -#B3_LINE_START_X=$(($R % 20)) -#B3_LINE_STOP_X=$((100 + $R % 40)) +B2_LINE_STOP_X=$((100 + R % 40)) +#B3_LINE_START_X=$((R % 20)) +#B3_LINE_STOP_X=$((100 + R % 40)) get_random -ROLL_X=$(($R % 40)) +ROLL_X=$((R % 40)) convert -size 180x60 xc:none -pointsize 40 \ \( -clone 0 -fill white \ diff --git a/vars.config.in b/vars.config.in index 9b3ac7585..04024fd73 100644 --- a/vars.config.in +++ b/vars.config.in @@ -51,9 +51,10 @@ {release, true}. {release_dir, "${SCRIPT_DIR%/*}"}. {sysconfdir, "{{release_dir}}/etc"}. +{erts_dir, "{{release_dir}}/erts-${ERTS_VSN#erts-}"}. {installuser, "@INSTALLUSER@"}. -{erl, "{{release_dir}}/{{erts_vsn}}/bin/erl"}. -{epmd, "{{release_dir}}/{{erts_vsn}}/bin/epmd"}. +{erl, "{{erts_dir}}/bin/erl"}. +{epmd, "{{erts_dir}}/bin/epmd"}. {localstatedir, "{{release_dir}}/var"}. {libdir, "{{release_dir}}/lib"}. {docdir, "{{release_dir}}/doc"}.